التوثيق
البنية المعمارية
هذه الصفحة موجهة لمطوري التطبيقات الذين يريدون معرفة ما يحدث بين count.set(1) وتغيّر نص على الشاشة،
في تطبيق مكتوب بلغة Rust أو بـ TSX.
لا تحتاج إليها لكتابة التطبيقات، لكنها تشرح لماذا يتصرف NSH على النحو الذي يتصرف به: لماذا تعمل المكوّنات مرة واحدة، ولماذا
تبقى الرسوم المتحركة سلسة بينما JS مشغول، ولماذا لا ينتظر الخيط الرئيسي شيفرتك أبدًا.
#الصورة الكبيرة
#البناء: شيفرتك وحدها تتحول إلى JS
nsh build وnsh dev وبقية الأوامر تجمّع تطبيقك في حزمة واحدة بسلسلة أدوات مكتوبة بلغة Rust (دون Node.js):
- Oxc يزيل أنواع TypeScript.
- مترجم JSX الخاص بـ NSH يحوّل JSX إلى استدعاءات مباشرة للمُصيِّر الأصلي، في المرحلة نفسها. الخصائص الثابتة والعناصر الأبناء تُضبط مرة واحدة؛ أما التعبيرات التي قد تتغير (التي تستدعي دالة أو تقرأ خاصية) فتصبح روابط (bindings) يتابعها المُصيِّر. وتتحول المكوّنات إلى استدعاءات دوال عادية مع كائن خصائص تكون فيه الخصائص المتغيرة دوال getter.
- rolldown يجمّع وحداتك في وحدة ES واحدة. استيرادات وحدات NSH (
nshوnsh/mobileوnsh/base…) تبقى استيرادات: فهذه الوحدات أصلية ويقدّمها وقت التشغيل. الحزمة تحتوي على شيفرة تطبيقك فقط؛ فمثال العدّاد أقل من 1 كيلوبايت من JS. - تُصرَّف الحزمة إلى شيفرة بايتية (
bundle.nshc)، وهي تُحمَّل أسرع من المصدر، وتُركَّب خريطة مصدر عبر جميع الخطوات بحيث تشير تتبّعات المكدس إلى أسطر.tsxالخاصة بك.
تُجمع الصور والخطوط التي تستوردها كأصول (assets)، وتُكتب تصريحات الأنواع لمحررك في
.nsh/types.
التطبيق المكتوب بلغة Rust لا يحتوي على حزمة: إذ يبني Cargo مُشغِّل كل هدف مع ربط حزمتك (crate) به، ويعمل
المكوّن الجذر الذي يسمّيه nsh.json على المحرك أصليًا. وكل ما يلي ينطبق عليه أيضًا، مع فارق أن شيفرة مكوّناتك
تعمل في Rust حيث تعمل شيفرة تطبيق TSX في QuickJS.
#التشغيل: خيطان لكل تطبيق
كل تطبيق قيد التشغيل (NshAppView على iOS، وNshView على Android، ونافذة التطبيق المُحزَّم) هو محرك واحد
له خيط محرك خاص به، ولتطبيقات TSX جهاز QuickJS افتراضي (VM) خاص به. لا توجد كائنات مفردة عامة (singletons)، لذلك يمكن تشغيل عدة تطبيقات جنبًا إلى جنب
داخل تطبيق مضيف واحد.
#خيط المحرك
كل ما يتعلق بمنطق تطبيقك يحدث هنا:
- شيفرتك تعمل هنا: المكوّنات ومعالجات الأحداث والمؤقتات والتأثيرات (effects). مكوّنات Rust تعمل أصليًا، وTSX يعمل في QuickJS.
- الرسم البياني التفاعلي (الإشارات signals، والقيم المحسوبة computed، والتأثيرات effects، والمالكون owners، والسياق context) أصلي. عُقده كائنات QuickJS من أصناف أصلية، فيرى مُجمِّع النفايات المراجع عبرها، لكن تتبّع الاعتماديات وجدولة التحديثات يتمّان في Rust (وتتبع خوارزمية Solid 1.9).
- المُصيِّر الذي تستدعيه شيفرة JSX المترجمة أصلي. ينشئ العُقد ويحوّل كل تغيير إلى عملية (op) تُدفع مباشرة إلى طابور أصلي: بلا JSON وبلا رسائل لكل خاصية.
- النواة تحتفظ بشجرة ظلّية لجميع العُقد. عند انتهاء دورة JS تأخذ كل العمليات المصطفّة دفعة واحدة وتُنفِّذ الالتزام (commit):
تُصيِّر مكوّنات Rust (مكوّنات
nsh/baseومكوّنات التطبيق نفسه)، وتحلّ هوامشenv()في الحشو (padding) والهامش (margin)، وتعيد بناء النصوص المتغيرة، وتجري التخطيط باستخدام Taffy (flexbox وgrid، مع اتجاه عمودي افتراضي وبلا انكماش)، وتقارن الإطارات والأنماط، وتنتج MountBatch واحدة: قائمة العمليات التي تحتاجها الواجهات الأصلية.
#الخيط الرئيسي
خيط المنصة الرئيسي (خيط الواجهة) يطبّق كل MountBatch في قفزة واحدة: ينشئ الواجهات ويُدرجها ويضبط الأنماط والإطارات ويتسلّم النص بعد تخطيطه. وبخلاف ذلك لا يفعل إلا ما يجب أن يحدث عليه:
- رسم النص الذي خُطِّط مسبقًا على خيط المحرك؛
- الرسوم المتحركة الأصلية: القيم المتحركة من
nshتُدار إطارًا بإطار (CADisplayLinkعلى iOS، ومُحرِّك على خيط الواجهة على Android) وتُكتب مباشرة في الواجهات؛ - الإيماءات والتمرير: تتحول اللمسات إلى أحداث تُرسل إلى خيط المحرك؛ و
scrollValueوpanتغذّيان القيم المتحركة في الإطار نفسه.
الخيط الرئيسي لا ينتظر JS أبدًا. عندما يحتاج النظام إلى جواب فوري (هل يجوز تغيير هذا النص، وهل يجوز لزر الرجوع إغلاق هذه الشاشة، وهل يجوز إغلاق هذه الورقة)، يكون الجواب قد رُكِّب مسبقًا كخاصية، ويعلم JS به لاحقًا عبر حدث. قد يؤخر خيط المحرك المشغول التحديثات، لكنه لا يستطيع تجميد التمرير أو الرسوم المتحركة أو إيماءات النظام.
#من اللمسة إلى الشاشة
main thread engine thread main thread
─────────── ───────────────────────────────────────────────────── ───────────
tap on a view ─ event ─▶ handler runs: count.set(count() + 1)
reactive graph: the bindings that read count() run again
renderer: one op per changed binding → native queue
JS turn ends → core commit: render, text, layout, diff
→ MountBatch (e.g. 1 Text item) ─ hop ─▶ apply: draw the new text
في مثال العدّاد، تكلّف اللمسة نحو 0.12 ms في إصدار release على جهاز Mac، شاملةً JS والالتزام معًا، وتنتج عددًا قليلًا من عناصر التركيب (انظر الاختبار).
في تطبيق Rust يكون المعالج إغلاقًا (closure) في Rust (count.set(count.get() + 1))، ويتبع ذلك الرسم البياني نفسه والالتزام والدفعة نفسها،
دون أي مفسِّر في الطريق.
تنتج ثلاث خصائص عن هذا التصميم:
- دورة واحدة تعني التزامًا واحدًا. مهما بلغ عدد الإشارات التي يضبطها المعالج، تتغير الواجهات مرة واحدة، معًا.
- المكوّنات تعمل مرة واحدة. الروابط وحدها تُعاد تشغيلها؛ لا إعادة تصيير للمكوّن ولا مقارنة DOM افتراضي. والمقارنة الوحيدة المتبقية هي مقارنة النواة للإطارات والأنماط، في Rust.
- التغييرات وحدها تنتقل. الأنماط والإطارات غير المتغيرة لا تُرسل؛ والواجهات الجديدة لا تُعطى قيمًا افتراضية.
#النص: يُخطَّط مرة واحدة، بعيدًا عن الخيط الرئيسي
النص غالبًا هو الجزء الأكثر كلفة في الواجهة. يخطّطه NSH على خيط المحرك باستخدام محرك النصوص الخاص بالمنصة، فيبدو تمامًا كالنص الأصلي:
- التحضير: يتحول
<Text>وما بداخله من نطاقات (spans) متداخلة إلى سلسلة منسّقة واحدة (CoreText على منصات Apple، وPrecomputedText/StaticLayoutعلى Android)؛ - القياس عند العروض التي يطلبها التخطيط؛ وتُخزَّن القياسات مؤقتًا بحسب المحتوى والعرض، فلا يعيد تغيير اللون وحده القياس؛
- التخطيط بالحجم النهائي.
يستلم الخيط الرئيسي النتيجة غير القابلة للتغيير ويرسمها فقط. النص المتداخل في <Text> هو نطاق (span) وليس واجهة؛
والسلاسل النصية خارج <Text> لا تُصيَّر شيئًا.
#القوائم
List مُفتَرَضة (virtualized) أصليًا. تقرر النواة أي الصفوف قريبة من منفذ العرض، وتحسب المواضع والأعمدة
والرؤوس الثابتة، وتعيد تدوير الخلايا. وعندما تحتاج خلية إلى عنصر مختلف، يربطها وقت التشغيل (يتغير الوصولان item() وindex()
في دالة الصف الخاصة بك)؛ أما الإطارات التي لا تكشف صفًا جديدًا فلا تستدعي JS إطلاقًا. انظر القوائم.
#كل ما يفعله وقت التشغيل أيضًا
- الشبكات:
fetchوواجهات API العامة الأخرى أصناف أصلية. تذهب الطلبات إلى حزمة HTTP الخاصة بالمنصة (NSURLSession،HttpURLConnection)، مع البث في الاتجاهين. - الصور تُنزَّل وتُفكّ ترميزها بعيدًا عن الخيط الرئيسي، بحجم الواجهة، مع ذاكرة تخزين مؤقت.
- الحدود والصلاحيات يفرضها وقت التشغيل، في Rust: انظر العزل والصلاحيات.
- استبدال الوحدات الفوري (Hot module replacement) أصلي أيضًا: يطبّق وقت التشغيل التصحيحات في أقل بكثير من ميلي ثانية (انظر خادم التطوير وتصحيح الأخطاء).
#ثلاث طبقات
تُبنى واجهة NSH على ثلاث طبقات، كما هي الحال في GPUI Kit:
| الطبقة | NSH | GPUI Kit | ما هي |
|---|---|---|---|
| العناصر الأولية (Primitives) | nsh-core ووقت التشغيل (nsh، nsh/mobile، nsh/desktop) | gpui | العناصر المضيفة والأنماط والتخطيط والتصيير والتفاعلية والبيئة |
| المكوّنات الأساسية | nsh-base (nsh/base) | gpui-base | مكوّنات بلا تنسيق تملك السلوك والحالة والتركيز ولوحة المفاتيح وإمكانية الوصول، وتأخذ مظهرها من التطبيق |
| مكتبة المكوّنات | مكتبة تطبيقك، أو مكتبة المجتمع | gpui-component | مكوّنات منسّقة: أزرار وبطاقات وصفوف ذات مظهر |
الطبقتان الأوليان هما Native Shell. أما الثالثة فهي لك: يبني التطبيق مكتبة المكوّنات الخاصة به على nsh/base وعلى
العناصر الأولية، بلغة TSX أو كمكوّنات Rust في حزمته الخاصة، أو يستخدم مكتبة ينشرها المجتمع، أو
ينسّق واجهته مباشرة داخل التطبيق.
#الحزم (crates)
NSH هو مساحة عمل (workspace) بلغة Rust. لا تحتاج إلى بنائه لكتابة التطبيقات (فالتوزيعة تأتي مع CLI وأوقات تشغيل مبنية مسبقًا)، لكن الأسماء تظهر في السجلات والوثائق:
| الحزمة | ما هي |
|---|---|
nsh-core | النواة المستقلة عن المنصة: الشجرة الظلّية، والأنماط، وتخطيط Taffy، ودمج النصوص، وافتراض القوائم، وعناصر التركيب؛ وقيم CSS وTailwind؛ ورسم الرسوم المتحركة الأصلية (القيم، والاستيفاء، وtiming / spring / decay)؛ وإطار مكوّنات Rust والسمة (theme)؛ ووحدات Rust الخاصة بوقت التشغيل (BackHandler الخاصة بـ nsh/mobile، …). |
nsh-base | nsh/base، المكوّنات الأساسية بلا تنسيق (gpui-base الخاصة بـ NSH): التنقل والحوارات وعناصر التحكم وشريط الحالة؛ وتستخدمها تطبيقات Rust باسم nsh_base. |
nsh-js | مضيف QuickJS ووقت تشغيل NSH: الرسم البياني التفاعلي، والمُصيِّر، وكل وحدة أصلية، وواجهات API العامة، واستبدال الوحدات الفوري، والحدود؛ وتصريحات TypeScript لكل ذلك. |
nsh-engine | خيط المحرك: يدير nsh-js وnsh-core ويسلّم دفعة واحدة لكل إطار إلى المنصة. |
nsh-package | الحزم الموقّعة: المفاتيح والشهادات والإبطال ومخزن الثقة. |
nsh-bundler | أداة التجميع: rolldown وOxc ومترجم JSX الخاص بـ NSH وخرائط المصدر وفحوص أصناف Tailwind. |
nsh-dev | خادم التطوير وعميله. |
nsh-platform-ios | واجهة UIKit الخلفية (iOS والمحاكي وMac Catalyst): تركيب الواجهات، ومحرك النصوص CoreText، والإدخال؛ وحزمة التضمين SDK. |
nsh-platform-android | الواجهة الخلفية لـ Android: libnsh.so ووقت تشغيل Java (NshView، والواجهات، والصور، والشبكات). |
nsh-cli | الأمر nsh. |
اتجاه الاعتمادية هو nsh-core ← nsh-js ← nsh-engine ← المنصات؛ وnsh-base مبنية على nsh-core. لا تعتمد النواة على أي منصة ولا على أي محرك JS،
وهذا ما يتيح لـ nsh test تشغيل وقت التشغيل الحقيقي دون واجهة على أي جهاز.