المكونات

مكونات Carats شبيهة بمكونات React مع اختلافات جوهرية في طريقة تعاملها مع السمات والأحداث.

بنية المكوّن

المكونات ملفات ‎.tsx‎ تُصدِّر دالة افتراضية. تستقبل كائن props واحدًا، ويمكنها التصريح بـ defaultProps، وتُصيّر props.children حيثما يجب أن تظهر العناصر الفرعية. ويمكنها استيراد أي ملف يفهمه Vite، وهكذا يُرفَق ملف SASS بمكوّن.

import './_layout.sass';

export default function Layout(props: JSX.ComponentProps) {
    return (
        <>
            <header>Hello World</header>
            <main>{props.children}</main>
            <footer>Carats</footer>
        </>
    )
}

مكوّن نموذجي

import './home.sass';

export default function Home() {
    return (
        <div class="home">
            <h1>Welcome to Carats</h1>
            <p>Build amazing applications with JJSX</p>
        </div>
    );
}

يُستورد ملف SASS لأثره الجانبي، وهذا ما يُرفق ورقة الأنماط بالصفحة. ويمكن لأي مكوّن استيراد أي شيء يفهمه Vite: SASS أو CSS أو JSON أو الصور.

الخصائص والعناصر الفرعية

تُمرَّر الخصائص إلى المكونات كما هو متوقع، وتُحاط العناصر الفرعية بوسوم المكوّن. أما المكوّن المُبرَز فيستقبل قيمة دالته الخادمية كخصائص بدلًا من ذلك.

export default function Card(props: { title: string; children?: JSX.Element }) {
    return (
        <article class="card">
            <h2>{props.title}</h2>
            {props.children}
        </article>
    );
}

// Usage
<Card title="My Card">
    <p>Card content here</p>
</Card>

الخصائص الافتراضية

تملأ defaultProps الخصائص التي لم تُمرَّر. وهذه هي الطريقة المعتادة لجعل الصفحة قابلة للتصيير أثناء اختبار، حيث لا تتوفر خصائص خادم.

import { CaratsComponent } from '@carats/render';

const Profile: CaratsComponent<User> = function (user) {
    return <h1>{user.name}</h1>;
};

Profile.defaultProps = {
    id: '0',
    name: 'Guest',
    username: 'guest',
    email: '',
    phone: '',
    website: ''
};

export default Profile;

وسوم head الديناميكية

يمكن للمكونات توفير وسوم head ديناميكية (title وmeta) عبر الإسناد إلى this.head داخل دالة المكوّن. تُجمع الوسوم أثناء التصيير على الخادم وتُحقَن في العنصر <!--app-head--> من قالب HTML.

import { CaratsComponent } from '@carats/render';

export default function Home(this: CaratsComponent) {
    this.head = <>
        <title>Home Page</title>
        <meta name="description" content="Welcome to my app" />
    </>;
    return <div>Home</div>;
}

لا يوجد this إلا في مكوّن مصرَّح عنه بالكلمة المفتاحية function. فدالة السهم ليس لها this خاص، لذا لا تستطيع إسناد this.head؛ استخدم تصريحًا بدالة، أو احذف وسوم head وأبقِ المكوّن دالة سهم. يُصنَّف المكوّن العادي الذي هو دالة سهم بوصفه CaratsComponent عند استخدامه مباشرة في مسار، وبوصفه CaratsComponentWithThis عندما يلزم التصريح لتعبير دالة عادي.

يجب استيراد نوع `CaratsComponent`؛ فهو مُصدَّر من `@carats/render`. أما الوسيط `this` فهو مجرد تعليق على النوع ويُمحى وقت الترجمة.

سمات HTML

تُحفظ سمات HTML كما هي وتُعامَل كنصوص. وبخلاف React، لا يمكنك تمرير دالة إلى سمة حدث مثل onclick أو onsubmit؛ استخدم addEventListener داخل استدعاء afterMount بدلًا من ذلك. وهذا هو أكثر فرق شائع عند نقل شيفرة React.

غير صحيح (لن يعمل)

<button onclick={() => handleClick()}>Click me</button>

الدالة ليست قيمة صالحة لسمة HTML، لذا تنتهي في الترميز كنص بلا معنى ولا يُستدعى شيء إطلاقًا.

صحيح

import { afterMount } from '@carats/hooks';

export default function MyButton() {
    let handler: (() => void) | undefined;

    afterMount(() => {
        const button = document.getElementById('my-button');
        handler = () => console.log('Clicked!');
        button?.addEventListener('click', handler);
        return () => button?.removeEventListener('click', handler!);
    });

    return <button id="my-button">Click me</button>;
}

تنطبق القاعدة نفسها على onsubmit وoninput وonchange وكل سمات الأحداث الأخرى. أعطِ العنصر مُعرِّف id وابحث عنه داخل استدعاء الخطاف.

المكونات المُبرَزة

تجلب المكونات المُبرَزة بياناتها تلقائيًا من دالتها الخادمية. غلِّف مكوّنك بدالة Burnish من @carats/render لتفعيل هذه الخاصية. يطابق الغلاف مسار المكوّن مع دالة خادمية على المسار نفسه، لذا يجب تسجيل كليهما تحت سلسلة مسار متطابقة تمامًا.

مع سياق this

import { Burnish } from '@carats/render';

export default Burnish<User>(function (user) {
    this.head = <title>{user.name}</title>;
    return <h1>Hello {user.name}</h1>;
});

مع خيار recast

يخزّن الإطار أحدث خصائص خادم لكل مسار. مرِّر ‎{ recast: true }‎ لإعادة الجلب في كل تصيير بدلًا من إعادة استخدام القيمة المخزّنة.

export default Burnish<User>((user) => <h1>Hello {user.name}</h1>, { recast: true });

لا يزال المكوّن المُبرَز يقبل الخصائص كأي مكوّن آخر، وهذا ما يجعله قابلًا للاختبار دون خادم.

نقطة دخول العميل

تُركِّب نقطة الدخول الأوجه في المتصفح ثم تبدأ التصيير في جانب العميل. وclientRender() مطلوبة كلما كان التوجيه داخل التطبيق مُفعَّلًا، وينبغي استدعاؤها بعد mount.

import { mount, clientRender } from '@carats/csr';
import facets from './facets.cara';
import './base.sass';

mount(facets);
clientRender();

تُستورد ورقة الأنماط العامة هنا بدلًا من صفحة، لأنها تحوي رموز التصميم وعمليات التصفير الأساسية التي تعتمد عليها كل صفحة.

إعدادات Vite

يبني إعداد Vite للعميل من src/client ويُخرِج إلى dist/client مع ملف manifest، وهذا يتيح للخادم الإشارة إلى ملف الدخول المجزّأ.

import { defineConfig } from 'vite'
import path from 'path'

export default defineConfig({
  publicDir: path.resolve(import.meta.dirname, 'public'),
  root: path.resolve(import.meta.dirname, 'src/client'),
  base: '/',
  appType: 'custom',
  server: {
    middlewareMode: true
  },
  build: {
    outDir: path.resolve(import.meta.dirname, 'dist/client'),
    emptyOutDir: true,
    manifest: true,
    minify: true,
    rollupOptions: {
      treeshake: true
    }
  }
})