توثيق Nuclo
مرجع عملي لتثبيت Nuclo وبناء الواجهات بتحديثات صريحة وتنسيقها باستخدام CSS محدد الأنواع وعرض HTML على الخادم.
npm create nuclo@latestنظرة عامة
#Nuclo هي مكتبة DOM صغيرة لـ TypeScript. يمكنك إنشاء الصفحة بدوال عادية مثل div() وbutton()، وحفظ الحالة في متغيرات عادية، واستدعاء update() عندما يجب أن تتغير الصفحة.
Nuclo ليس لديه DOM افتراضي، ولا مصرّف، ولا بروكسيات ولا إشارات.
import 'nuclo' let count = 0 const App = () => div( h1(() => `Count: ${count}`), button('Add one', { onClick: () => { count++; update() } }), ) render(App, document.getElementById('app')!)
النموذج الذهني
- ابنِ الواجهة باستخدام دوال بناء الوسوم.
- قم بتمرير دالة لكل قيمة تعتمد على الحالة.
- قم بتغيير الحالة باستخدام JavaScript العادي.
- استدعِ
update(). يقوم Nuclo بتشغيل الدوال مرة أخرى ويقوم بتعديل DOM.
تغيير المتغير لا يمس DOM. تتغير الصفحة فقط عند استدعاء update().
البدء السريع
#ينشئ create-nuclo هيكل مشروع Vite وTypeScript جاهزًا لاستخدام Nuclo:
يطلب اسم المشروع. لتخطي المطالبة، قم بتمرير الاسم والأعلام. باستخدام npm، ضع -- قبل الأعلام:
$ npm create nuclo@latest my-app -- --template basic --yes
ثم قم بتثبيت وبدء تشغيل خادم التطوير:
$ cd my-app $ npm install $ npm run dev
الخيارات
[project-name]: المجلد الهدف. الافتراضي هوnuclo-app.-t, --template <name>: القالب المراد نسخه. فقطbasicموجود اليوم.-y, --yes: لا تطالب أبدًا، استخدم الإعدادات الافتراضية.-f, --force: اكتب في مجلد غير فارغ.-h, --help: اطبع المساعدة.
التثبيت والاستيراد
#لإضافة Nuclo إلى مشروع موجود:
$ npm install nuclo
أو استخدم pnpm add nuclo أو yarn add nuclo أو bun add nuclo. لا يعتمد Nuclo على حزم خارجية.
التوجيه ليس جزءًا من الحزمة الأساسية. يتوفر في حزمة مستقلة باسم nuclo-router، تثبّتها إلى جانب nuclo عند الحاجة.
الأسماء في النطاق العام
import 'nuclo' يضع هذه على globalThis:
- 175 دالة لبناء الوسوم: 112 لـ HTML (
div،span،button، ...) و63 لـ SVG، بلاحقةSvg(svgSvg،pathSvg، ...). راجع دوال بناء الوسوم. - 16 دالة مساعدة:
update،scope،when،list،region،into،on،render،hydrate،forceUpdate،css،cx،createCss،variants،keyframesوglobalStyle.
يجب أن يتم تشغيل الاستيراد قبل أي تعليمات برمجية تستدعي دالة البناء. تعمل وحدات ES بترتيب الاستيراد، لذلك يفشل هذا:
import { App } from './app' // app.ts calls div() → ReferenceError: div is not defined import 'nuclo'
ضع import 'nuclo' أولاً في ملف الإدخال الخاص بك، أو قم باستيراده في كل ملف يستخدم الأسماء في النطاق العام.
الاستيرادات المسماة
تتوفر الدوال المساعدة أيضًا كتصديرات مسماة، وهي الدوال نفسها المسجّلة في النطاق العام. أما getCssText() وresetStyles() فتتوفران كتصديرات مسماة فقط. دوال بناء الوسوم متاحة في النطاق العام فقط، لذا لا يعمل import { div } from 'nuclo'.
import { render, update, getCssText } from 'nuclo'
نقاط الدخول
nuclo: يسجل الأسماء في النطاق العام ويصدر 16 مساعدًا بالإضافة إلىgetCssTextوresetStyles.nuclo/ssr:renderToString،renderManyToString،renderToStringWithContainerوgetCssText. راجع التصيير على الخادم.nuclo/polyfill: تنفيذ صغير لـ DOM في Node وBun. انظر nuclo/polyfill.nuclo/types: الأنواع فقط. انظر TypeScript.
TypeScript
#يتضمن Nuclo تعريفات أنواعه. تحتاج إلى شيئين:
"moduleResolution": "bundler".node16وnodenextغير مدعومين حتى الآن.- مكتبة
DOM. يقوم TypeScript بتضمينه بشكل افتراضي عندما لا تقوم بضبطlib.
إذا قام أي ملف في المشروع باستيراد 'nuclo'، فسيرى كل ملف الأنواع العامة. لا تحتاج إلى أي شيء آخر. إذا لم يستورده أي ملف، على سبيل المثال في ملف tsconfig منفصل للاختبارات، فأضف الأنواع بنفسك:
{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "types": ["nuclo/types"] } }
أو أضف /// <reference types="nuclo/types" /> إلى ملف .d.ts.
أنواع مفيدة
هذه الأنواع متاحة في النطاق العام، لذا يمكنك استخدامها دون استيراد:
NodeModFn<Tag>: نوع القيمة التي تُرجعها دالة بناء الوسوم. استخدمه كنوع إرجاع للمكون.NodeModLike<Tag>: أي شيء تقبله دالة بناء الوسوم كوسيطة.ExpandedElement<Tag>: نوع القيمة التي تُرجعهاrender()وhydrate().ElementTagName: أي اسم وسم HTML.ExpandedElementAttributes<Tag>: كائن السمة الذي تقبله دالة بناء الوسوم، بما في ذلك معالجاتon*وonMountوonDestroy. استخدمه للمكونات التي تمرر السمات من خلالها.MountCallbackوDestroyCallback: معالجاتonMountوonDestroy.WhenBuilder،ListModifier: نوعا القيمتين اللتين تُرجعهماwhen()وlist().RegionOptions،RegionType: الخيارات التي تقبلهاregion().StyleResult،Style،ThemeConfig،CssInstanceوأنواع التنسيق الأخرى. يمكنك أيضًا استيراد هذه:import type { StyleResult } from 'nuclo'.
function Badge(text: string): NodeModFn<'span'> { return span(text) }
يقوم ExpandedElement<Tag> بوضع علامة على كل عضو في DOM كاختياري. استخدم تحويل النوع عندما تحتاج إلى نوع العنصر الكامل: render(App, root) as HTMLDivElement.
دوال بناء الوسوم
#كل وسم HTML له دالة عامة بنفس الاسم: div، span، input، ... الاستثناء الوحيد هو <var>، وهو var_ لأن var هي كلمة محجوزة. تحمل دالة كل وسم SVG اللاحقة Svg: svgSvg، pathSvg، circleSvg، ...
لا يؤدي استدعاء دالة بناء الوسوم إلى إنشاء عنصر بعد. تقوم div('Hi') بإرجاع دالة بناء: دالة تقوم بإنشاء العنصر لاحقًا. يستدعيه Nuclo عندما تقوم بتركيبه باستخدام render() أو وضعه داخل دالة بناء أخرى، when() أو list(). يُنشئ كل استخدام عنصرًا جديدًا.
let name = 'Ada' const Card = () => div( { id: 'card', className: 'card' }, h2('Profile'), p(() => `Hello, ${name}`), button('Rename', { onClick: () => { name = 'Grace'; update() } }), ) render(Card, document.body)
update().فتح ↗Profile
Hello, Ada
الوسائط
تقبل دالة البناء أي عدد من الوسائط ويطبقها من اليسار إلى اليمين:
- string، number، bigint: عقدة نصية.
() => value: النص الذي يتغير علىupdate(). راجع القيم الديناميكية .null،undefined: تم تخطيه.true،false: تُعرض كنص "true" أو "false". استخدمwhen()للشروط.- كائن عادي: السمات. راجع السمات.
- نتيجة
css()أوcx()أوvariants(): تضيف فئاتها. on(...): مستمع الأحداث أو خطاف دورة الحياة.- دالة بناء أخرى،
when()أوlist(): عنصر فرعي. - عقدة DOM من نوع
Node: ملحق كما هو. (el, index) => …: معدل مخصص. انظر أدناه.
محاذير شائعة
div(isAdmin && span('Admin'))يعرض النص "false". اكتبdiv(when(() => isAdmin, span('Admin'))).- المصفوفات ليست عناصر فرعية بحد ذاتها. انشر عناصرها:
ul(...names.map((n) => li(n))). بالنسبة للقوائم التي تتغير، استخدمlist(). - يخفي المتغير المحلي الذي يحمل اسم الوسم دالة البناء. داخل
function Row(label: string)،label(...)غير قابل للاستدعاء. غيّر أسماء المتغيرات المحلية مثلlabel،title،data،p،aوi. - تتيح المتصفحات كل عنصر يحمل
idكمتغير عام. إذا احتوت الصفحة على<main id="main">قبل تحميل Nuclo، يُبقي Nuclo هذا المتغير كما هو، لذا يرميmain(...)خطأTypeError. استخدم معرّفات أخرى.
معدّلات مخصصة
يتم تشغيل الدالة التي تعلن عن معلمة واحدة على الأقل مرة واحدة، بترتيب الوسائط، أثناء إنشاء العنصر. وسيطتها الأولى هي العنصر. والثانية هي فهرس داخلي فلا تعتمد عليه. إذا أعادت سلسلة أو رقمًا، فسيصبح ذلك نصًا. إذا أعادت Node أُلحقت العقدة، وإذا أعادت كائنًا طُبّق كسمات.
const field = input((el) => { el.autocomplete = 'off' })
الحصول على العنصر
render() ترجع العنصر الجذر. داخل الشجرة، استخدم on("mount", (el) => …) أو معدّل مخصص. يمكنك أيضًا استدعاء دالة الإنشاء بنفسك لإنشاء عنصر منفصل:
const el = div('Hello')() as HTMLDivElement document.body.append(el)
قم بإرفاقه قبل update() التالي. العقدة غير الموجودة في المستند عند تشغيل update() تتوقف عن التحديث للأبد.
SVG
تقبل دوال بناء SVG الوسائط نفسها. يعيّن Nuclo كل سمة SVG باستخدام setAttribute، لذلك تعمل الأسماء مثل "stroke-width" كما هو مكتوب.
const Icon = () => svgSvg( { viewBox: '0 0 24 24', width: 24, height: 24, className: 'icon' }, circleSvg({ cx: 12, cy: 12, r: 10, fill: 'currentColor' }), )
جميع البُناة
HTML: a abbr address area article aside audio b base bdi bdo blockquote body br button canvas caption cite code col colgroup data datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins kbd label legend li link main map mark menu meta meter nav noscript object ol optgroup option output p picture pre progress q rp rt ruby s samp script search section select slot small source span strong style sub summary sup table tbody td template textarea tfoot th thead time title tr track u ul var_ video wbr
SVG: aSvg animateSvg animateMotionSvg animateTransformSvg circleSvg clipPathSvg defsSvg descSvg ellipseSvg feBlendSvg feColorMatrixSvg feComponentTransferSvg feCompositeSvg feConvolveMatrixSvg feDiffuseLightingSvg feDisplacementMapSvg feDistantLightSvg feDropShadowSvg feFloodSvg feFuncASvg feFuncBSvg feFuncGSvg feFuncRSvg feGaussianBlurSvg feImageSvg feMergeSvg feMergeNodeSvg feMorphologySvg feOffsetSvg fePointLightSvg feSpecularLightingSvg feSpotLightSvg feTileSvg feTurbulenceSvg filterSvg foreignObjectSvg gSvg imageSvg lineSvg linearGradientSvg markerSvg maskSvg metadataSvg mpathSvg pathSvg patternSvg polygonSvg polylineSvg radialGradientSvg rectSvg scriptSvg setSvg stopSvg styleSvg svgSvg switchSvg symbolSvg textSvg textPathSvg titleSvg tspanSvg useSvg viewSvg
المكوّنات
#المكون هو دالة بسيطة تقوم بإرجاع منشئ، مثل button(...). لا توجد واجهة API خاصة بالمكوّنات. لا يُنشأ شيء حتى تصيّره أو تضعه في شجرة.
function Counter(label: string) { let count = 0 // one per call return button(() => `${label}: ${count}`, { onClick: () => { count++; update() }, }) } render(() => div(Counter('A'), Counter('B')))
تخص الحالة المعرّفة داخل الدالة ذلك المثيل وحده، أما الحالة المعرّفة خارجها فتشترك فيها جميع المثيلات. يعيد forceUpdate() استدعاء المكوّنات، فتُعاد تهيئة الحالة المعرّفة داخلها.
العناصر الفرعية مجرد وسائط:
function Card(heading: string, ...children: NodeModLike<'div'>[]) { return div({ className: 'card' }, h2(heading), ...children) } render(() => Card('Hi', p('one'), p('two')))
قم بتمرير كائن عادي لتعيين السمات. يمكنك تمرير عدة كائنات. لنفس المفتاح، تفوز القيمة الأحدث، باستثناء className وstyle، اللذين يتم دمجهما. يمكن أن تكون القيمة ثابتة، أو دالة بدون معلمات يتم تشغيلها مرة أخرى على كل update().
let busy = false const save = button( { type: 'submit', disabled: () => busy, 'aria-busy': () => String(busy), 'data-id': 42 }, 'Save', )
update()؛ فتتبع سمات disabled وaria-busy لزر Save هذه القيمة.فتح ↗كيفية تعيين القيمة
- اذا كان العنصر HTML يحتوي على خاصية بهذا الاسم (
value،checked،disabled،htmlFor،tabIndex، ...)، يعيّن Nuclo الخاصية. وإلا فإنه يستدعيsetAttribute(key, String(value))، على سبيل المثال"aria-*"و"data-*". - في عناصر SVG، يستدعي Nuclo دائمًا
setAttribute. - تم تخطي
nullوundefined. القيمة الديناميكية التي تُرجعnullأوundefinedتحافظ على القيمة القديمة. لمسح قيمة، قم بإرجاع""أوfalseلخاصية منطقية. - تعمل القيم المنطقية مع خصائص DOM المنطقية، مثل
disabledفي الأزرار وcheckedوhiddenوrequired. وفي غير ذلك، تُكتبtrueوfalseكسلسلتين نصيتين "true" و"false". - استخدم مفاتيح
"data-*".datasetغير مدعوم. - لا تُحوَّل محارف
innerHTMLالخاصة إلى كيانات HTML، ولا يظهر محتواه في مخرجات التصيير على الخادم. تجنّب استخدامه.
أصناف CSS
استخدم className. يتم دمج سلاسل الفئات الثابتة ونتائج css() وclassName الديناميكية. تحافظ className الديناميكية على الفئات الثابتة، وعندما تقوم بإرجاع "" تبقى الفئات الثابتة فقط.
let active = false const base = css({ p: 8 }) const item = li(base, { className: 'item' }, { className: () => (active ? 'active' : '') }, 'Home') // class="<base> item", then "<base> item active" after active = true; update()
لا تستخدم مفتاح class على عناصر HTML. يقوم بتعيين السمة الأولية واستبدال كل فئة تم تطبيقها قبلها، بما في ذلك فئات css().
التنسيقات المضمّنة
style يأخذ كائنًا بمفاتيح camelCase، أو دالة تُرجع واحدًا.
let progress = 30 const bar = div({ style: () => ({ width: `${progress}%`, backgroundColor: 'teal' }) })
- Nuclo لا يضيف أي وحدات. اكتب
"12px"، وليس12. الخصائص بدون وحدات مثلopacityوzIndexتقبل الأرقام. ""وnullوundefinedتمسح الخاصية.- يقوم النمط الديناميكي بتعيين المفاتيح التي يُرجعها فقط. المفتاح الذي يتوقف عن العودة يحتفظ بقيمته القديمة، لذا قم بإرجاع
""لمسحه. - سلاسل الأنماط (
"color: red") والخصائص المخصصة ("--gap") غير مدعومة. استخدمcss()بدلاً من ذلك. يقبل الخصائص المخصصة من خلالraw.
الأحداث
#هناك طريقتان للاستماع إلى الأحداث: سمات on* ومعدل on().
سمات on*
في كائن السمة، تعتبر الدالة الموجودة أسفل مفتاح مثل onClick (on متبوعًا بحرف كبير) معالج حدث، وليست قيمة ديناميكية.
let query = '' const search = input({ placeholder: 'Search', value: () => query, onInput: (e) => { query = e.currentTarget.value; update() }, onKeyDown: (e) => { if (e.key === 'Escape') { query = ''; update() } }, })
- يستنتج TypeScript نوع الحدث من المفتاح والعنصر.
e.currentTargetوthisهما العنصر. - يعيّن Nuclo الخاصية الأصلية، بحيث يصبح
onClickel.onclick. يوجد معالج واحد لكل حدث: يحلonClickالأحدث محل معالج سابق. onDoubleClickوonDblClickكلاهما يعنيdblclick. المفاتيح المكتوبة بأحرف صغيرة مثلonclickتسبب أخطاء في الأنواع.- الاسم الذي ليس حدثًا معروفًا يستمع إلى اسمه بالأحرف الصغيرة:
onMyEventيستمع إلى"myevent". - لا تُلتقط الاستثناءات التي يرميها المعالج.
Type something, then press Escape.
on()
تقوم on() بإرجاع مُعدِّل يستدعي addEventListener على العنصر.
const panel = div( on('scroll', onScroll, { passive: true }), on('click', logClick), on('click', closeMenu), )
- يضيف كل
on()مستمعًا مستقلًا، وتُنفَّذ جميع المستمعات بالترتيب. optionsينتقل مباشرة إلىaddEventListener:capture،once،passive،signal.- قم بتمرير
on()مباشرة إلى دالة البناء ويتم استنتاج نوع العنصر:input(on('input', (e) => e.currentTarget.value)). - إذا رمى أحد المستمعات استثناءً، يسجّل Nuclo الخطأ باستخدام
console.errorويواصل تنفيذ المستمعات الأخرى. - Nuclo يزيل المستمع عندما يزيل العنصر. على الخادم،
on()لا يفعل شيئًا. - يمكنك إعادة استخدام قيمة
on()واحدة على العديد من العناصر.
بالنسبة لحدث مخصص، قم بتمرير نوع الحدث كنوع عام:
type PickEvent = CustomEvent<{ id: number }> const picker = ul(on<'pick', PickEvent>('pick', (e) => choose(e.detail.id)))
أيهما تستخدم؟
استخدم سمات on* للحالة المعتادة. استخدم on() عندما تحتاج إلى أكثر من مستمع واحد لحدث ما، أو خيارات المستمع، أو اسم حدث مخصص مثل "my-event".
يُنفَّذ onMount مرة واحدة بعد إدراج العنصر، ويُنفَّذ onDestroy مرة واحدة عند إزالة Nuclo للعنصر. اكتبهما كسمات أو باستخدام on(). يعمل الشكلان بالطريقة نفسها، ويُستنتج نوع العنصر من وسمه.
let seconds = 0 const Timer = () => span( { onMount: () => { const id = setInterval(() => { seconds++; update() }, 1000) return () => clearInterval(id) // runs when the span is removed }, }, () => `${seconds}s`, ) const search = input(on('mount', (el) => el.focus()))
onMount مؤقتًا متكررًا ويُرجع دالة تنظيفه. تنفّذ إزالة التركيب دالة التنظيف، فيتوقف العد حتى يُركَّب المؤقت مجددًا.فتح ↗متى تُنفَّذ؟
- يُنفَّذ
onMountفي نهاية استدعاءrender()أوhydrate()أوupdate()أوforceUpdate()الذي أدرج العنصر. لا يُنفَّذ أثناء بناء الشجرة. - إذا قام
onMountبإرجاع دالة، فسيتم تشغيل هذه الدالة عند التدمير، بعد دوالonDestroyللعنصر. - يتم تشغيل
onDestroyعندما يقوم Nuclo بإزالة العنصر: تتم إزالة صفlist()، أو يقوم فرعwhen()بتبديله، أو يتركinto()منطقته، أو يقومforceUpdate()بإسقاطه. يتم تشغيله قبل الإزالة مباشرةً، لذا يظل العنصر موجودًا في المستند.
إزالة عناصر DOM مباشرة
إذا قمت بإزالة عنصر بنفسك، باستخدام node.remove() أو innerHTML = ''، فلن يتم تشغيل onDestroy وعمليات التنظيف الخاصة به مطلقًا، وسيستمر تشغيل المؤقتات. Nuclo ليس لديه unmount(). لتدمير تطبيق ما بالتنظيف، قم بتغليفه في when():
let mounted = true render(() => div(when(() => mounted, App())), root) mounted = false update() // App's onDestroy hooks and cleanups run
الترتيب والأخطاء
- يمكنك تسجيل أي عدد من الخطافات بأي من الشكلين، وتُنفَّذ بترتيب تسجيلها.
- تُنفَّذ خطافات
onDestroyللعناصر الفرعية أولًا، ثم للعنصر الأب. - تُركَّب العناصر حسب ترتيب أول خطاف
onMountلكل منها، وهو ترتيب الوسائط، وتُنفَّذ جميع خطافات العنصر معًا. مرّر خطاف العنصر الأب قبل عناصره الفرعية إذا كانت تعتمد عليه. - إذا رمى خطاف استثناءً، يُسجَّل الخطأ باستخدام
console.errorويستمر تنفيذ الخطافات الأخرى. - يجب أن تُرجع دالة الاستدعاء إما لا شيء أو دالة تنظيف، لذا لا تجعله
async. لا يتم استخدام الوعد الذي تم إرجاعه أبدًا كتنظيف. ابدأ العمل غير المتزامن بداخله:{ onMount: (el) => { void load(el) } }. - لا تُنفَّذ الخطافات على الخادم مطلقًا. وعلى العميل تُنفَّذ بعد
hydrate().
Nuclo لا يقوم أبدًا بتحديث الصفحة من تلقاء نفسه. قم بتغيير حالتك، ثم استدعِ update().
let count = 0 const Counter = () => button(() => `Clicked ${count} times`, { onClick: () => { count++; update() }, }) render(Counter)
update() التالي، سواء من الزر أو أي شيء آخر في الصفحة.فتح ↗Clicked 0 times
قم بتغيير الحالة كما تريد، ثم استدعِ update() مرة واحدة:
let first = 'Ada' let last = 'Lovelace' render(() => p(() => `${first} ${last}`)) first = 'Grace' last = 'Hopper' update() // one pass, both changes
استدعِ update() من معالجات الأحداث أو المؤقتات أو دوال معالجة نتائج الجلب أو onMount. لا تستدعها من داخل دالة قيمة ديناميكية.
الدالة التي لا تقبل معلمات هي قيمة ديناميكية. يشغّلها Nuclo مرة عند بناء العنصر، ثم عند كل update(). استخدمها للنصوص وقيم السمات وstyle وclassName. أما العناصر الفرعية التي تظهر أو تختفي أو تتكرر، فاستخدم لها when() وlist().
let name = 'Alice' let busy = false const Profile = () => div( h1('Welcome'), // static p(() => `Hello, ${name}`), // dynamic text button({ disabled: () => busy }, 'Save'), )
update(). يعيد forceUpdate() بناء الشجرة، فيُحدَّث النص الثابت أيضًا.فتح ↗static: Hello, Alice
dynamic: Hello, Alice
يتحقق Nuclo من عدد المعلمات (fn.length). الدالة التي تعرّف معلمة هي معدّل مخصص وتُنفَّذ مرة واحدة فقط. تبلغ قيمة length للدالتين (x = 0) => … و(...args) => … 0، لذا فهما ديناميكيان.
النص الديناميكي
- يتم عرض سلسلة أو رقم أو قيمة bigint أو قيمة منطقية على أنها
String(value). يعرض() => ok && 'Saved'"false"، لذا اكتب() => (ok ? 'Saved' : ''). - تُعرض
nullوundefinedكنص فارغ. - إذا أعادت الدالة نتيجة
css()أوcx()عند أول تشغيل، تُعامل كمصدر لصنف CSS ديناميكي بدلًا من نص. انظرcx(). - العقدة أو دالة البناء أو المصفوفة أو الكائن العادي لا يعرض شيئًا ولا يتم تحديثه أبدًا.
السمات الديناميكية
- Nuclo يكتب قيمة فقط عندما تختلف عن القيمة الأخيرة التي كتبها Nuclo. ولا تتم مقارنتها بـ DOM المباشر، لذلك إذا قام المستخدم بالكتابة في أحد المدخلات وأرجعت دالتك نفس القيمة كما كانت من قبل، فسيظل النص الذي كتبه موجودًا.
- نتيجة
nullأوundefinedلا تكتب شيئًا، لذا تبقى القيمة القديمة.classNameهو الاستثناء: أي قيمة falsy تزيل الفئات الديناميكية وتحتفظ بالفئات الثابتة. - تقوم الدالة
styleبتطبيق الكائن الذي يتم إرجاعه في كل تحديث.
الأخطاء
إذا رمت دالة قيمة ديناميكية استثناءً، يلتقط Nuclo الخطأ ويواصل بقية التحديث. يصبح النص فارغًا أو يحتفظ بقيمته السابقة، وتحتفظ السمة بقيمتها السابقة. يعود كلاهما للعمل عند أول update() ناجح. الاستثناء هو دالة النص التي ترمي استثناءً عند تشغيلها لأول مرة: يظل نصها فارغًا دائمًا.
يقوم بتشغيل تمريرة متزامنة واحدة عبر كل قيمة ديناميكية في الصفحة. باستخدام المعرفات، فإنه يقوم فقط بتحديث ما هو موجود داخل العناصر المميزة بمعرفات scope(). يقوم كل تمرير بتنفيذ هذه الخطوات بالترتيب:
list(): يقرأ العناصر مرة أخرى ويضيف أو ينقل أو يزيل الصفوف.when(): التحقق من الشروط مرة أخرى. تتم إعادة بناء الفرع فقط عندما يصبح فرع مختلف نشطًا.- السمات الديناميكية، بما في ذلك
styleوclassName. - النص الديناميكي.
onMountللعناصر التي أُدرجت في الخطوتين 1 و2.
- إنه يقوم بتشغيل كل شيء. يتم تشغيل كل قيمة ديناميكية في كل استدعاء، حتى عندما لا تتغير بياناتها. يتم تخطي الكتابة إلى DOM فقط. حافظ على القيم الديناميكية سريعة وخالية من الآثار الجانبية.
- العقد الموجودة في المستند فقط. إذا لم تكن العقدة في المستند عند تشغيل
update()، يتوقف Nuclo عن تتبّعها وتحديثها نهائيًا، حتى إذا أُرفقت لاحقًا. ينطبق ذلك أيضًا علىlist()وwhen(). صيّر داخل حاوية موجودة في المستند. - القيم الثابتة لا تتغير أبدًا. تُحدَّد السلاسل النصية الثابتة وقيم السمات عند إنشاء العنصر. لإعادة بنائها، استخدم
forceUpdate(). - الأخطاء. تُلتقط أخطاء النص والسمات وتستمر دورة التحديث. إذا رمى شرط
when()استثناءً، يُسجَّل الخطأ ويتوقفwhen()عن التحديث. وإذا رمت دالةitemsأوrenderItemفيlist()استثناءً، يخرج الخطأ منupdate()ولا تُنفَّذ الخطوات المتبقية. - على الخادم.
update()لا يقوم بأي شيء على الخادم.
update() جميع القيم الديناميكية حتى لو لم تتغير بياناتها. تزيد العروض الأخرى في هذه الصفحة العدّاد أيضًا.فتح ↗This dynamic value has run 1 times.
scope()
#يمنح العنصر الذي تمرّره إليه معرّف نطاق واحدًا أو أكثر. بعدها يحدّث update('id') ذلك العنصر ومحتواه فقط، بما في ذلك سمات العنصر نفسه.
let cartItems: string[] = [] let user = 'Ada' const App = () => div( header(() => `Hi, ${user}`), aside(scope('cart'), span(() => `Items: ${cartItems.length}`)), ) render(App) cartItems.push('apple') user = 'Grace' update('cart') // "Items: 1", the header still says "Hi, Ada" update() // everything, the header now says "Hi, Grace"
- يمكن أن يحتوي العنصر على عدة معرفات:
scope('cart', 'sidebar'). update('a', 'b')يحدّث كل شيء داخلaأوb. إذا كانت عدة عناصر تستخدم نفس المعرف، فسيتم تحديثها جميعًا.- مع النطاقات المتداخلة، يتضمن
update('outer')النطاق الداخلي.update('inner')لا يلمس الجزء الخارجي. - إذا لم يحمل أي عنصر في المستند المعرّف المطلوب، فلن يُحدَّث أي شيء ولن يظهر خطأ، لذا تحقّق من كتابة المعرّف.
scope()لا يفعل شيئًا على الخادم.
يعرض المحتوى بناءً على شرط ما. يقوم Nuclo بالتحقق من الشروط مرة أخرى عند كل update(). الشرط الحقيقي الأول يفوز. إذا لم يكن أي شيء صحيحًا، فسيتم عرض محتوى .else(). بدون .else()، لن يتم عرض أي شيء.
let status: 'loading' | 'error' | 'ready' = 'loading' const Status = () => div( when(() => status === 'loading', p('Loading…')) .when(() => status === 'error', p('Something went wrong')) .else(p('Ready')), )
Loading…
الشروط
- بالنسبة للحالة، قم بتمرير دالة. تتم قراءة القيمة المنطقية البسيطة مرة واحدة:
when(open, …)لا يتغير أبدًا. - في TypeScript، يجب أن تقوم الدالة بإرجاع قيمة منطقية. اكتب
() => items.length > 0أو() => user != null.
ما الذي يُحدَّث؟
- بينما يظل نفس الفرع نشطًا، يتم الاحتفاظ بعناصره. تستمر القيم الديناميكية والقوائم وكتل
when()الموجودة داخلها في التحديث. - عندما يصبح فرع مختلف نشطًا، تتم إزالة المحتوى القديم وتشغيل خطافات
onDestroyالخاصة به. تم بناء الفرع الجديد من الصفر ويتم تشغيل خطافاتonMountالخاصة به. يتم فقدان الحالة في العناصر القديمة، مثل نص الإدخال أو التركيز.
محتوى الفرع
ضع العناصر الفرعية في فرع: النص، والنص الديناميكي، والبناة، وlist() وwhen() المتداخلة. تمرير منشئي العقد مثل p('Hi')، وليس عقد DOM: تتوقف عقدة DOM عن التحديث بعد إخفاء فرعها مرة واحدة.
تنطبق كائنات السمات وon() الموجودة في الفرع على العنصر الأصلي، وتظل بعد إخفاء الفرع. بالنسبة للسمة الشرطية، استخدم سمة ديناميكية: { className: () => (active ? 'active' : '') }.
ربط الشروط
تُرجع .when() و.else() دالة بناء جديدة ولا تغيّران الأصل. إذا استدعيت .else() مرتين، يُستخدم الاستدعاء الأخير.
list()
#يعرض صفًا واحدًا لكل عنصر. عند كل update()، يستدعي Nuclo items() مرة أخرى ويطابق العناصر الجديدة مع الصفوف الموجودة حسب الهوية (===).
let fruits = ['Apple', 'Banana'] const Fruits = () => ul(list(() => fruits, (fruit) => li(fruit))) render(Fruits) fruits.push('Cherry') update()
كيفية مطابقة الصفوف
- العنصر الذي لا يزال موجودًا يحتفظ بصفه. يقوم Nuclo بنقل الصف إذا لزم الأمر، ولا يقوم باستدعاء
renderItemمرة أخرى. - عنصر جديد يحصل على صف جديد. يفقد العنصر الذي تمت إزالته صفه، ويتم تشغيل خطافات
onDestroyالخاصة به. - لكل ظهور مكرر للعنصر صف خاص به.
- يمكنك تغيير المصفوفة الموجودة في مكانها (
push،splice،sort) أو إرجاع مصفوفة جديدة.
لا تُستدعى دالة بناء الصف المحفوظ مجددًا، لذا استخدم القيم الديناميكية للحقول التي تتغير:
const todos = [{ text: 'Write docs', done: false }] const Todos = () => ul( list(() => todos, (todo) => li({ className: () => (todo.done ? 'done' : '') }, () => todo.text), ), ) render(Todos) todos[0].done = true update() // same <li>, its class is now "done"
الكائنات الجديدة عناصر جديدة من حيث الهوية، لذا فإن استبدال المصفوفة بـ todos.map((t) => ({ ...t })) يعيد بناء كل صف.
ما تُرجعه renderItem
- تقوم بإرجاع عنصر واحد، مثل
li(...)أوtr(...)أو دالة بناء SVG. قم بإرجاعnullأوundefinedلتخطي أحد العناصر. السلاسل والعقد النصية والأجزاء لا تقدم شيئًا. - لا تقم بإرجاع
when()أوlist()مباشرة. ضعهم داخل عنصر الصف:li(todo.text, when(() => todo.done, ' ✓')). - لإخفاء العناصر، قم بالتصفية في
items()، وليس فيrenderItem. - حافظ على
renderItemخاليًا من الآثار الجانبية. Nuclo قد يستدعيه أكثر من مرة لنفس العنصر.
الفهرس
index هو موضع العنصر عند إنشاء الصف الخاص به. ولا يتم تحديثه عند نقل الصفوف. بالنسبة للموضع الحالي، استخدم قيمة ديناميكية: li(() => `${names.indexOf(name) + 1}. ${name}`).
الكائنات القابلة للتكرار
يمكن استخدام أي كائن قابل للتكرار: المصفوفات وSet والمولّدات وmap.values(). يمكن إرجاع المصفوفة أو Set نفسها في كل مرة. أما المكرّر، مثل المولّد أو map.values()، فيُستهلك بعد مرور واحد، لذا أنشئه داخل items() في كل استدعاء. يؤدي تكرار Map مباشرة إلى إنشاء مصفوفات [key, value] جديدة في كل مرة، فيُعاد بناء جميع الصفوف. استخدم map.values() بدلًا منه.
موضع الصفوف
تظهر الصفوف في موضع list() بين العناصر المجاورة له، بين علامتي تعليق. لا يتم لمس العناصر الفرعية الأخرى للعنصر الأب. يعمل list() أيضًا داخل دوال بناء SVG.
region() يمثل مكانًا مسمىًا في الشجرة. into() يملأها من أي مكان آخر، وبأي عمق. لا يتلقى التخطيط المحتوى كخاصية، ولا يعرف المكون الذي يبني المحتوى مكان وصوله.
const Layout = () => div( aside(nav(a({ href: '/' }, 'Home'))), main(region({ id: 'main', empty: p('Nothing open.') })), ) // Anywhere in the tree, before or after the region const Article = () => into('main', article(h1('Hello'))) render(() => div(Layout(), Article()))
كيف يعمل
into()لا يترك سوى تعليق حيث تم كتابته. يتم عرض محتواه بين علامات المنطقة.- لا يهم ترتيب البناء. إذا بُني
into()قبل منطقته، ينتظر ويظهر فور بناء المنطقة. تظهر العروض بترتيب تعريفها. - يبقى المحتوى طوال عمر العرض الذي ينتمي إليه. عند إغلاق
when()الذي يحتوي علىinto()، أو عند إزالة صفlist()الخاص به، يغادر المحتوى المنطقة. يؤدي فتحه مرة أخرى إلى إضافة نسخة واحدة، وليس نسختين. - عندما تختفي منطقة ما، على سبيل المثال يتم إغلاق
when()، تنتظر عروضها مرة أخرى. تعرضها المنطقة التي تم إنشاؤها لاحقًا بنفس المعرف. into()الذي لا يتطابق معرفه مع أي منطقة لا يعرض شيئًا ولا يسبب أي خطأ، لذا تحقق من التهجئة.- المعرفات فريدة في الشجرة. تقوم المنطقة الثانية التي تم إنشاؤها تحت معرّف لمنطقة لا تزال موجودة بتسجيل تحذير.
- تعمل المناطق مع التصيير على الخادم. في
hydrate()، يعيد كل عرض استخدام HTML القادم من الخادم بدلاً من إنشائه مرة أخرى.
الخيارات
id: الاسم الذي تستخدمه استدعاءاتinto()للعثور على المنطقة.type: يعرض'latest'(الافتراضي) العرض الأحدث فقط. يعرض'stack'جميع العروض بترتيب وصولها.empty: المحتوى الذي يُعرض عندما لا تحتوي المنطقة على أي عروض.
المناطق المكدّسة
مع type: 'stack'، لا يؤدي فتح عرض فوق آخر إلى إعادة بناء العرض الموجود تحته. تبقى عناصر DOM والتركيز وحالة النموذج كما هي. استخدم هذا لطبقة مثل مربع حوار فوق الصفحة.
let open = false const App = () => div( region({ id: 'main', type: 'stack' }), into('main', form(input({ placeholder: 'Keeps its text' }))), when(() => open, into('main', div('On top'))), )
عدة مناطق دفعة واحدة
قم بتمرير كائن لملء أكثر من منطقة من into() واحد. يمكن أن تكون القيمة عنصرًا فرعيًا واحدًا أو مجموعة من العناصر الفرعية. يتداخل كلا النموذجين: يستهدف into() داخل محتوى طريقة عرض أخرى منطقته الخاصة.
const Page = () => into({ main: article(h1('Eager route')), sidebar: [h2('Related'), ul(li('One'), li('Two'))], })
فتح حوار من عمق الشجرة
تقع منطقة التراكب في الجذر. أي مكون، بأي عمق، يفتح مربع حوار عن طريق تغليف into() في when(). يؤدي إغلاق when() إلى إزالة مربع الحوار من المنطقة.
let confirming = false const App = () => div( Page(), div({ className: 'overlay' }, region({ id: 'overlay' })), ) // Deep inside Page() const DeleteButton = () => div( button('Delete', { onClick: () => { confirming = true; update() } }), when(() => confirming, into('overlay', dialog({ open: true }, p('Delete this item?'), button('Cancel', { onClick: () => { confirming = false; update() } }), ), ), ), )
إشعارات عابرة من صفوف القائمة
كل صف من list() هو عرض. تعرض المنطقة المكدسة العناصر بالترتيب، وإزالة عنصر من المصفوفة يؤدي إلى إزالة الإشعار المرتبط به.
const toasts: { text: string }[] = [] const notify = (text: string) => { toasts.push({ text }) update() } const dismiss = (toast: { text: string }) => { toasts.splice(toasts.indexOf(toast), 1) update() } const App = () => div( button('Save', { onClick: () => notify('Saved.') }), div({ className: 'toasts' }, region({ id: 'toasts', type: 'stack' })), list(() => toasts, (toast) => into('toasts', div(toast.text, button('×', { onClick: () => dismiss(toast) }))), ), )
عنوان تحدده الصفحة لنفسها
باستخدام type: 'latest' الافتراضي، يفوز العرض الأحدث. يحدد التخطيط موضع العنوان، وتحدد الصفحة المفتوحة نصّه. الصفحة التي لا تحتوي على into('title', …) تترك النص empty.
let page: 'home' | 'settings' = 'home' const Layout = () => div( h1(region({ id: 'title', empty: 'My app' })), when(() => page === 'home', Home()).else(Settings()), ) const Home = () => p('Welcome.') const Settings = () => div( into('title', 'Settings'), form(/* … */), )
قيم ديناميكية داخل العرض
محتوى into() هو محتوى Nuclo عادي. الدوال فيه هي قيم تعتمد على الحالة ويتم تحديثها على update()، أينما كانت المنطقة.
const selected = new Set<string>() const Layout = () => div( header(region({ id: 'status' })), Table(), ) const Table = () => div( into('status', () => `${selected.size} selected`), list(() => rows, (row) => label( input({ type: 'checkbox', onChange: (e) => { e.currentTarget.checked ? selected.add(row.id) : selected.delete(row.id) update() } }), row.name, ), ), )
منطقة تظهر وتختفي
يُعرَّف into() الخاص بلوحة المعلومات مرة واحدة. ما دام المستخدم غير مسجّل الدخول، لا توجد منطقة main، فينتظر العرض. عند تسجيل الدخول، تُنشأ المنطقة ويظهر العرض. وعند تسجيل الخروج، تُزال المنطقة ويعود العرض إلى الانتظار.
let user: { name: string } | null = null const App = () => div( when(() => user !== null, main(region({ id: 'main' })), ).else( LoginForm(), ), into('main', Dashboard()), )
عرض بمنطقته الخاصة
يمكن أن يتضمن العرض region() تستهدفها عروض أخرى. هنا تتضمن صفحة المحرر منطقة tabs التي تملؤها علامات التبويب. عند إغلاق المحرر، تُزال منطقته معه.
let tab: 'code' | 'preview' = 'code' const Editor = () => into('main', div( nav( button('Code', { onClick: () => { tab = 'code'; update() } }), button('Preview', { onClick: () => { tab = 'preview'; update() } }), ), section(region({ id: 'tabs' })), ), when(() => tab === 'code', into('tabs', textarea())) .else(into('tabs', iframe({ src: '/preview' }))), )
مثال مباشر
import 'nuclo' let page: 'home' | 'article' | null = 'home' let dialog = false const go = (next: typeof page) => () => { page = next dialog = false update() } const Layout = () => div( nav( button('Home', { onClick: go('home') }), button('Article', { onClick: go('article') }), button('Close page', { onClick: go(null) }), ), main( region({ id: 'main', type: 'stack', empty: p('Nothing open.') }), ), aside(region({ id: 'side', empty: p('No related links.') })), ) // Pages fill the layout's regions from outside it. const Home = () => into('main', input({ placeholder: 'Type here, then open the dialog' }), button('Open dialog', { onClick: () => { dialog = true; update() }, }), ) const Article = () => into({ main: article(h2('Article'), p('One into() fills both regions.')), side: [h2('Related'), ul(li('One'), li('Two'))], }) // Stacks over Home, so Home keeps its DOM and the text you typed. const Dialog = () => into('main', div( p('On top of Home.'), button('Close', { onClick: () => { dialog = false; update() } }), ), ) export const RegionApp = () => div( Layout(), when(() => page === 'home', Home()), when(() => page === 'article', Article()), when(() => dialog, Dialog()), )
into() واحد.فتح ↗No related links.
update() يقوم بتشغيل القيم الديناميكية فقط. تُحدَّد القيم الثابتة، مثل النص الموجود في h1(labels[lang].title)، عند إنشاء الشجرة. يستدعي forceUpdate() مكوناتك مرة أخرى ويعدّل شجرة DOM الفعلية لتطابق النتيجة، مع تضمين القيم الثابتة. استخدمه لإجراء تغييرات نادرة على مستوى التطبيق، مثل تبديل اللغة. استخدم update() لكل شيء آخر.
const labels = { en: { title: 'Hello' }, pt: { title: 'Olá' } } let lang: keyof typeof labels = 'en' const App = () => div(h1(labels[lang].title)) render(App, root) lang = 'pt' forceUpdate() // same <h1> element, now "Olá"
update() سطر lang الديناميكي فقط. يستدعي forceUpdate() الدالة App مجددًا ويعدّل عنصر <h1> نفسه. يعيد الشكل الصريح بناء هذه الشجرة وحدها، لا الصفحة بأكملها.فتح ↗lang = 'en'
Hello
Static text, fixed on build.
من دون وسائط، يعيد forceUpdate() بناء كل جذر صُيّر باستخدام render(App) أو hydrate(App)، ويتخطى الجذور المصيّرة باستخدام render(App()). إذا رمى أحد المكوّنات استثناءً، يُسجَّل الخطأ ويحتفظ الجذر بشجرة DOM السابقة. ويُسجَّل أيضًا أي خطأ يحدث لاحقًا أثناء تطبيق البنية الجديدة، مع بقاء التعديلات التي طُبّقت بالفعل.
ما يبقى وما يتغير
- تتم مطابقة العناصر الفرعية بالترتيب حسب العلامة. تتم إعادة استخدام العنصر المطابق، لذلك تظل قيم التركيز والإدخال المكتوبة، ولا تعمل خطافات
onMountالخاصة به مرة أخرى. بمجرد اختلاف وسم عنصر فرعي واحد، يتم بناء هذا العنصر وكل عنصر يليه من جديد. - النص والسمات والفئات والأنماط والمستمعات وفروع
when()وصفوفlist()تتبع البنية الجديدة. - يتم إنشاء عناصر جديدة وتشغيل خطافات
onMountالخاصة بها. تتم إزالة العناصر التي لم يعد البناء الجديد يصنعها، ويتم تشغيل خطافاتonDestroyالخاصة بها. - تُطابق صفوف
list()حسب الموضع أيضًا، وليس حسب العنصر، لذلك يمكن أن يؤدي عنصر الصف المُعاد استخدامه إلى إظهار عنصر مختلف. - لا تُزال السمات التي لم تعد البنية الجديدة تعيّنها.
نموذج صريح. forceUpdate(App(), parent) يعيد بناء شجرة واحدة في parent ويعيد جذرها. ولا يسجل الجذر للاستدعاءات اللاحقة. مثل hydrate()، فإنه يتوقع أن يكون الجذر هو العقدة الأولى في parent. إذا كانت العقدة الأولى تحتوي على وسم مختلف، فسيتم استبدالها.
على الخادم، forceUpdate() لا يفعل شيئا.
render()
#يبني شجرتك ويلحق عنصرها الجذري بـ parent. parent الافتراضي هو document.body. يقوم بإرجاع العنصر الجذر.
import 'nuclo' import { App } from './app.ts' render(App, document.getElementById('app')!)
- يمكنك تمرير المكون (
render(App)) أو دالة البناء الذي يقوم بإرجاعه (render(App())). DOM هو نفسه. فقطrender(App)يسمح لـforceUpdate()بإعادة بناء الجذر لاحقًا، لذا تفضل ذلك. - يتم إلحاقه. لا يقوم بمسح
parent، لذا فإن استدعاءه مرتين يضيف نسختين. - تم تشغيل كل
onMountفي الشجرة عند إرجاعrender(). - صيّر داخل عنصر موجود في المستند. راجع
update(). - يجب أن يكون الجذر عنصرًا، مثل
div(...)، وليسwhen()أوlist(). indexداخلي. لا يختار مكان إدراج العنصر. اتركه.
يجب ألا يقبل المكوّن الممرّر إلى render(App) أي معلمات. المعلمة الإلزامية تسبب خطأ في الأنواع. أما المعلمة الاختيارية فتجتاز فحص الأنواع، لكن render() يرمي استثناءً أثناء التشغيل. غلّف المكوّن الذي يقبل خصائص بدالة:
const Page = (props: { name: string }) => div(h1('Hi ', props.name)) render(() => Page({ name: 'Ana' }), root)
hydrate()
#يفعّل HTML المصيَّر بواسطة renderToString(). فهو يعيد استخدام العناصر الموجودة، ويرفق مستمعي الأحداث، ويربط القيم الديناميكية وكتل when() وقوائم list()، لذلك يعمل update() بعد ذلك. ثم يقوم بتشغيل خطافات onMount. parent الافتراضي هو document.body. يقوم بإرجاع العنصر الجذر.
import 'nuclo' import { App } from './app.ts' hydrate(App, document.getElementById('root')!)
- استخدم نفس المكون ونفس البيانات مثل الخادم.
- نفّذ الترطيب في حاوية مخصصة. يجب أن يكون جذر التطبيق هو العقدة الأولى، مع السماح بمسافات بيضاء قبله. إذا جاءت عقدة أخرى أولاً، فإن Nuclo تستبدل تلك العقدة وتترك نسخة الخادم خلفها.
- كما في
render()، مرّرAppبدلًا منApp()لتتمكنforceUpdate()من إعادة بناء الجذر لاحقًا. يجب ألا تقبلAppأي معلمات. إذا قبلت معلمة اختيارية، فلن يربطhydrate()شيئًا ولن يُظهر خطأ. غلّف المكوّن الذي يقبل خصائص بدالة:hydrate(() => Page(props), root). - إذا لم يوجد HTML مصيَّر على الخادم، استخدم
render().
عندما يختلف الخادم والعميل
تُعتمد بنية العميل:
- وسم جذر مختلف: يتم استبدال تلك العقدة بـ DOM الجديدة. حاوية فارغة: تم إلحاق DOM جديد.
- تم تعديل النص المختلف. تتبع عناصر
list()المختلفة وفروعwhen()العميل. - تتم إزالة العناصر الفرعية الزائدة في نسخة الخادم، ويتم إنشاء العناصر الفرعية الناقصة في نسخة الخادم.
- يتم ترك السمات التي كتبها الخادم ولكن لم يقم العميل بتعيينها كما هي.
يقوم renderToString() بكتابة تعليقات HTML كعلامات، مثل <!-- text-0 --> قبل كل عقدة نصية. hydrate() يحتاج إليها، لذا لا تقم بإزالة التعليقات من HTML الذي أنشأه الخادم. بدون العلامات، يؤدي الترطيب إلى حذف الصفوف list()، ويتوقف list() وwhen() عن التحديث.
العرض على الخادم
#صيّر مكوّنًا إلى سلسلة HTML على الخادم. في المتصفح، استدعِ hydrate() بنفس المكون لجعل HTML تفاعليًا. لا تحتوي أوقات تشغيل الخادم على document، لذا قم باستيراد nuclo/polyfill أولاً.
let count = 0 const heading = css({ color: 'red' }) export const App = () => main( h1(heading, () => `Count: ${count}`), button({ onClick: () => { count++; update() } }, '+1'), )
import 'nuclo/polyfill' import 'nuclo' import { renderToString, getCssText } from 'nuclo/ssr' import { App } from './app.ts' const html = renderToString(App) const page = `<!doctype html> <html> <head><style id="nuclo-styles">${getCssText()}</style></head> <body> <div id="root">${html}</div> <script type="module" src="/client.js"></script> </body> </html>`
import 'nuclo' import { App } from './app.ts' hydrate(App, document.getElementById('root')!)
احتفظ بـ id="nuclo-styles" على وسم التنسيق. يعيد العميل استخدام هذا العنصر ويضيف قواعد جديدة فقط. انظر getCssText().
ما يعمل على الخادم
- يقوم كل استدعاء
renderToString()بإنشاء الشجرة وتشغيل كل قيمة ديناميكية مرة واحدة. - لا يتم تشغيل معالجات الأحداث،
onMountوonDestroyأبدًا.update()وscope()وforceUpdate()لا يفعلون شيئًا. - متزامن. قم بتحميل بياناتك أولاً، ثم اعرضها.
- لا شيء يبقى مسجلاً بعد التصيير، لذلك يمكن لعملية واحدة أن تخدم العديد من الطلبات. تمرير بيانات الطلب كوسائط، مثل
renderToString(Page(user)). تتم مشاركة المتغيرات على مستوى الوحدة النمطية بواسطة كافة الطلبات. - لا تقم باستدعاء
render()على الخادم. يتم إلحاقه بـdocument.bodyالمشترك.
renderToString()
يأخذ منشئ (App())، مكون (App) أو عقدة DOM. null وundefined يعيدان "". يجب أن يكون الجذر عنصرًا. يجب ألا يأخذ المكون الذي تم تمريره كـ renderToString(Page) أي معلمات، لذا قم بتمرير Page(user) للمكون الذي يأخذ البيانات.
renderToString(p('Hello')) // '<p><!-- text-0 -->Hello</p>'
- تُحوَّل المحارف الخاصة في النصوص وقيم السمات إلى كيانات HTML. يُكتب النص داخل
script()وstyle()كما هو، مع تقسيم وسوم الإغلاق فقط، لذا لا تضع فيه مدخلات المستخدم مطلقًا. - لا يرمي استثناءات. إذا رمى المكوّن أو دالة
list()أو شرطwhen()استثناءً، أو غاب polyfill، يُسجَّل الخطأ وتُعاد"". تحقّق من النتيجة الفارغة إذا أردت اعتبار الطلب فاشلًا. أما دالة النص أو السمة التي ترمي استثناءً فتنتج قيمة فارغة لذلك الموضع فقط.
renderManyToString()
نفس inputs.map(renderToString).
renderToStringWithContainer()
يغلّف المخرجات بعنصر واحد. القيمة الافتراضية لـ containerTag هي "div"، ولـ containerAttrs هي {}. تُحوَّل المحارف الخاصة في قيم السمات إلى كيانات HTML. تُكتب أسماء السمات كما تمرّرها، لذا استخدم أسماء HTML مثل class. لا تستخدم مدخلات المستخدم كاسم وسم مطلقًا.
renderToStringWithContainer(p('Hi'), 'section', { id: 'main', class: 'page' }) // '<section id="main" class="page"><p><!-- text-0 -->Hi</p></section>'
nuclo/polyfill
#DOM صغير حتى يتمكن Nuclo من بناء الأشجار في Node وBun. قم باستيراده مرة واحدة، قبل العرض الأول. تحتاج معظم التطبيقات فقط إلى الاستيراد العادي.
- عندما لا يكون هناك
window، فإنه يقوم بتعيينglobalThis.documentوNodeوElementوHTMLElement، ولكن فقط تلك المفقودة. لا يقوم بإنشاءwindow. - في المتصفح، أو ضمن jsdom، لا يفعل شيئًا.
EventوCustomEventهما المُنشئان الأصليان في بيئة التشغيل.Node،ElementوHTMLElementهي أسماء مستعارة لفئات polyfill.
هو DOM لبناء الأشجار وليس متصفح. لا يحتوي على innerHTML ولا getElementById، querySelector() يُرجع null، ويتم تجاهل مستمعي الأحداث. استخدم renderToString() للحصول على HTML.
يحول كائن تنسيق محدد الأنواع إلى فئات CSS التي تم إنشاؤها. قم بتمرير النتيجة إلى أي دالة بناء الوسوم. في المتصفح، تنتقل القواعد إلى عنصر <style id="nuclo-styles"> واحد. على الخادم، يقوم getCssText() بإرجاعها.
const card = css({ p: 16, rounded: 8, bg: '#fff7ed', hover: { bg: '#ffedd5' }, }) const Card = () => div(card, 'Simple card')
- يقوم
css()بإنشاء فئة واحدة لتصريحات CSS الأساسية، بالإضافة إلى فئة واحدة لكل مفتاح زائف ومحدد وكتلة وسائط. البطاقة أعلاه تحصل على فئتين. - أسماء الأصناف مشتقة من تجزئة المحتوى. تشترك التنسيقات المتطابقة في الصنف نفسه، وينتج الخادم والعميل الأسماء نفسها.
- تعريف الأنماط مرة واحدة، على مستوى الوحدة. يقوم
css()بالتخزين المؤقت حسب الكائن، لذلك تتم معالجة كائن جديد في كل استدعاء مرة أخرى في كل مرة. - تغيير كائن النمط بعد تمريره إلى
css()لا يؤدي إلى أي شيء. - يظل كل نمط مسجلاً طوال عمر الصفحة أو عملية الخادم. بالنسبة للقيم التي تتغير لكل عنصر، مثل العرض من البيانات، استخدم السمة
style.
تنسيقات مسماة
قم بتمرير الاسم أولاً للحصول على اسم فئة قابل للقراءة:
const pageTitle = css('page-title', { text: 24, hover: { color: 'red' } }) pageTitle.className // "page-title page-title-<hash>"
- الفئة الأساسية هي الاسم بالضبط. الكتل الأخرى تحصل على
name-<hash>. - يجب أن يكون الاسم اسم فئة صالحًا. وإلا فإن Nuclo يحذر ويولد واحدًا.
- عالمية. إذا كان هناك نمطان مختلفان يستخدمان نفس الاسم، يحذر Nuclo.
استخدام النتيجة
const card = css({ p: 8 }) div(card) // best div(card, { className: 'featured' }) // className merges div(card, { class: 'featured' }) // replaces: only "featured" is left
يقوم كل من String(card) وcard.className بإرجاع أسماء الفئات.
كائنات التنسيق
#يستخدم الكائن الذي تمرره إلى css() خصائص CSS الشائعة في camelCase، ومجموعة من الاختصارات، والمفاتيح المتداخلة للحالات الزائفة، والمحددات، واستعلامات الوسائط. يقوم TypeScript بالإكمال التلقائي لكل منهم. الخصائص خارج مجموعة الخصائص المحددة الأنواع، مثل borderTopLeftRadius أو fill أو paddingInline، تنتقل إلى raw (انظر أدناه).
القيم
- تصبح الأرقام
px، باستثناء0والخصائص بدون وحدة:zIndex،opacity،fontWeight،lineHeight،flex،flexGrow،flexShrink،order،aspectRatio،gridColumnوgridRow. فيraw،zoom،scale،column-count،orphans،widows،tab-sizeوanimation-iteration-countهي أيضًا بدون وحدات. - تُكتب السلاسل النصية كما هي. تُتخطى قيم
nullوundefinedوfalse. lineHeight(وleading) ليس له وحدة، لذا فإنleading: 24يعني 24 ضعف حجم الخط. اكتب"24px"للبكسل.
css({ p: 16, m: 0, opacity: 0.5, leading: 1.5, z: 10, w: '50%' }) // padding:16px; margin:0; opacity:0.5; line-height:1.5; z-index:10; width:50%
الاختصارات
| المفتاح | CSS |
|---|---|
p، pt، pr، pb، pl | padding، وكل منهما الجانب |
px / py | الحشو اليسار واليمين / أعلى وأسفل |
m، mt، mr، mb، ml، mx، my | نفس الشيء بالنسبة للهامش |
w، h، minW، maxW، minH، maxH | العرض والارتفاع والحد الأدنى والحد الأقصى |
size | العرض والارتفاع |
bg | الخلفية |
text / font / weight | font-size / font-family / font-weight |
leading / tracking | line-height / letter-spacing |
align / items / justify | text-align / align-items / justify-content |
z / rounded / shadow / select | z-index / border-radius / box-shadow / user-select |
تأخذ هذه المفاتيح true:
| المفتاح | CSS |
|---|---|
row | display: flex; flex-direction: row |
col | display: flex; flex-direction: column |
center | align-items: center; justify-content: center (لا يتم ضبط العرض) |
truncate | overflow: hidden; text-overflow: ellipsis; white-space: nowrap |
الحالات الزائفة
hover، focus، focusVisible، focusWithin، active، visited، disabled، enabled، checked، required، invalid، valid، readOnly، first (:first-child)، last (:last-child)، only (:only-child)، odd، even، empty، placeholderShown، placeholder، before، after، selection، marker، firstLine و firstLetter. يمكن تداخلها: hover: { focus: {…} } يعطي :hover:focus.
المحددات وقواعد at
المفتاح الذي يبدأ بـ & هو محدد، وكل & يصبح الفئة. المفتاح الذي يبدأ بـ @ هو قاعدة at: @media أو @container أو @supports.
const menu = css({ p: 8, '& > li': { py: 4 }, '& > li:hover': { color: 'red' }, '&:is(.dark *)': { color: 'white' }, '@media (min-width: 768px)': { p: 16 }, })
- يجب أن يبدأ المفتاح بـ
&. يتم تجاهل مفتاح مثل".dark &"، لذا اكتب"&:is(.dark *)". - يتم دمج كتلتين
@mediaالمتداخلتين معand. يتم وضع الاستعلام الذي يقوده نوع الوسائط (print،only screen) أولاً، لأن القواعد النحوية تسمح بالنوع فقط في البداية. الزوج الذي لا يمكن تسطيحه - نوعان من الوسائط، أوnotالبادئ - يحذر ويستخدم الاستعلام الداخلي وحده. أي قاعدة متداخلة أخرى تحل محل القاعدة الخارجية.
raw
استخدم raw لأي خاصية، مع كتابة الاسم تمامًا كما هو الحال في CSS. لا تزال الأرقام تحصل على px، لذا قم بتمرير السلاسل:
css({ raw: { '--gap': '8px', '-webkit-line-clamp': '2' } })
يتم تحويل أي مفتاح مسطح آخر إلى صيغة kebab-case ويتم كتابته كما هو، لذا فإن الخطأ المطبعي مثل colr: 'red' يصبح إعلانًا يسقطه المتصفح. يتم تجاهل الكائن المتداخل ضمن مفتاح ليس حالة زائفة أو شاشة أو مفتاح & أو @. يُبلغ TypeScript عن كليهما.
يجمع بين الأنماط. عندما يقوم مدخلان بتعيين نفس الخاصية، يفوز الإدخال الأحدث.
const base = css({ p: 8, color: 'red' }) const blue = css({ color: 'blue' }) cx(base, blue) // padding: 8px; color: blue cx(base, false, null, 'extra') // base's classes plus "extra" div(cx(base, blue), 'Blue text')
- تُتخطى المدخلات ذات القيم falsy، ويتم تسوية المصفوفات، ويتم تقسيم السلاسل وإلغاء تكرارها.
- يتم دمج الفئات التي تم إنشاؤها لنفس الكتلة (القاعدة، نفس الزائفة، نفس استعلام الوسائط) في فئة واحدة جديدة. تمر أسماء الفئات الأخرى دون تغيير.
- فقط
cx()يعرف ترتيب الوسيطة. باستخدامdiv(base, blue)، تتم إضافة كلا الفئتين، وتفوز القاعدة التي تم إنشاؤها مؤخرًا. ضع الأنماط التي تتجاوز بعضها البعض في استدعاءcx()واحدة. - عندما يتم دمج كتلة نمط مسمى مع كتلة إدخال أخرى، تكون الفئة المدمجة هي
name-<hash>، وليس الاسم المجرد. الكتل التي لا يتم دمجها تحتفظ بفئتها، لذا فإنcx(pageTitle, 'extra')لا يزال لديهpage-title.
أصناف CSS الديناميكية
قم بتمرير دالة تُرجع cx(...) دائمًا:
const baseButton = css({ px: 12, py: 8, border: '1px solid #ccc' }) const activeButton = css({ bg: '#ff3f00', color: 'white' }) let active = false const Toggle = () => button( () => cx(baseButton, active && activeButton), () => (active ? 'Active' : 'Inactive'), { onClick: () => { active = !active; update() } }, )
يجب أن تقوم الدالة بإرجاع نتيجة النمط عند تشغيلها لأول مرة. يعرض () => active && activeButton النص "false" بدلاً من فئة.
تقوم بإرجاع css وcx وvariants وkeyframes وglobalStyle الخاصة بها والمرتبطة بسمة. يعطي السمة أسماء للقيم: باستخدام السمة أدناه، يكتب borderColor: 'border' #e5e7eb، ويقوم TypeScript بإكمال الأسماء تلقائيًا. يصبح كل إدخال في screens مفتاح نمط لاستعلام الوسائط، مثل md: {…}.
export const { css, cx, variants } = createCss({ colors: { primary: '#ff3f00', border: '#e5e7eb' }, radii: { card: '12px' }, screens: { md: '(min-width: 768px)' }, }) const panel = css({ p: 12, rounded: 'card', border: '1px solid', borderColor: 'border', md: { p: 24 }, })
| مفتاح السمة | مستخدم بواسطة |
|---|---|
colors | bg، backgroundColor، color، borderColor وكل جانب، outlineColor، caretColor، textDecorationColor |
fonts | font, fontFamily |
shadows | shadow, boxShadow |
radii | rounded, borderRadius |
screens | مفاتيح النمط الجديدة، مثل md: {…} |
- يتم حل اسم السمة فقط عندما يكون هو القيمة بأكملها. لم يتم حل
border: "1px solid primary". - تصبح قيمة الشاشة بدون
@@media <value>. يتم استخدام القيمة التي تبدأ بـ@كما هو مكتوب، على سبيل المثال"@container (min-width: 400px)". - مفاتيح الشاشات متاحة فقط في المثيلات التي تنشئها
createCss(). لا تتوفر هذه المفاتيح فيcss()العامة، لكنها تقبل مفاتيح@media. - تتشارك كافة المثيلات والمساعدين العموميين في ورقة أنماط واحدة. يتم إخراج قواعد وسائط الشاشة بالترتيب الذي تم به الإعلان عن الشاشات، وليس الترتيب الذي تستخدمها به، لذا قم بإدراج شاشات
min-widthمن الصغيرة إلى الكبيرة. استخدم مثيلcreateCss()واحد لكل تطبيق، بحيث يكون هذا الترتيب هو الموجود في قالبك. themeعلى النتيجة هو كائن السمة الذي مررته.
ينشئ دالة تقوم بإرجاع نمط لمجموعة من الخيارات، مثل وظيفة الزر وحجمه.
const buttonClass = variants({ base: { px: 12, py: 8, rounded: 6 }, variants: { intent: { primary: { bg: '#2563eb' }, danger: { bg: '#dc2626' } }, size: { sm: { text: 12 }, lg: { text: 18 } }, block: { true: { w: '100%' } }, }, defaultVariants: { intent: 'primary', size: 'sm' }, compoundVariants: [{ intent: 'danger', size: 'lg', css: { weight: 700 } }], }) button(buttonClass(), 'Save') // primary, sm button(buttonClass({ intent: 'danger', size: 'lg', block: true }), 'Delete') // bold, full width
- يتم دمج الأنماط بهذا الترتيب:
base، القيمة المختارة لكل مجموعة، ثم كل مركب مطابق. تفوز الأنماط اللاحقة لكل خاصية. - يستخدم الخيار
undefinedأوnullالخيار الافتراضي. يتم تخطي المجموعة التي ليس لها أي إعدادات افتراضية. - المجموعة التي تحتوي على مفاتيح
trueوfalseفقط تأخذ قيمة منطقية. - يتم تطبيق المركب عندما تتطابق كل مجموعة يدرجها، بما في ذلك الإعدادات الافتراضية.
- يتم إنشاء كل تحديد مرة واحدة وتخزينه مؤقتًا.
keyframes() يسجل رسمًا متحركًا ويعيد اسمه الذي تم إنشاؤه. مفاتيح الإطار هي from وto والنسب المئوية، مثل "50%" أو "0%, 100%". تأخذ الإطارات خصائص مسطحة فقط. الإطارات المتطابقة ترجع نفس الاسم.
const fadeIn = keyframes({ from: { opacity: 0 }, to: { opacity: 1 }, }) const panel = css({ animation: `${fadeIn} 200ms ease-out` })
globalStyle() يضيف قاعدة لأي محدد:
globalStyle('*, *::before, *::after', { boxSizing: 'border-box' }) globalStyle('body', { m: 0, font: 'system-ui, sans-serif' })
- يتم استخدام المحدد تمامًا كما هو مكتوب.
- تعمل الخصائص المسطحة فقط. يتم تجاهل المفاتيح الزائفة والشاشات ومفاتيح
@، لذا لا يمكن لـglobalStyle()كتابة قواعد الوسائط. - تتم إضافة نفس القاعدة مرة واحدة. يضيف النمط المختلف لنفس المحدد قاعدة ثانية، وتفوز القاعدة اللاحقة.
getCssText() وresetStyles()
#getCssText() تقوم بإرجاع كل قاعدة أنشأتها css() وcx() وvariants() وkeyframes() وglobalStyle() في هذه العملية: القواعد الأساسية أولاً، ثم @media و@container و كتل @supports. استخدمها للتصيير على الخادم.
- استدعها بعد
renderToString(). يتم تضمين القواعد التي تم إنشاؤها أثناء العرض. - يغطي العملية بأكملها، وليس صفحة واحدة أو طلبًا واحدًا، وينمو فقط. وهذا أمر آمن، لأن أسماء الفئات هي تجزئات للمحتوى.
- ضعه في
<style id="nuclo-styles">. يعيد العميل استخدام هذا العنصر ويتخطى القواعد الموجودة فيه بالفعل. باستخدام معرف آخر، أو لا شيء، يضيف العميل ورقة أنماط ثانية مع كل قاعدة مرة أخرى. - لا تُحوَّل المحارف الخاصة في النص إلى كيانات HTML، لذلك لا تقم أبدًا بإدخال مدخلات غير موثوقة في قيم النمط.
resetStyles() هو مساعد اختبار. فهو يزيل كل قاعدة وعنصر #nuclo-styles. تحتفظ الأنماط التي قمت بإنشائها سابقًا بأسماء فئاتها ولكنها تفقد CSS الخاصة بها. لا تستدعها في التطبيق أو على الخادم.
import { resetStyles } from 'nuclo' beforeEach(() => resetStyles())
nuclo-router ليست جزءًا من الحزمة الأساسية Nuclo. قم بتثبيته بجوار nuclo. إنه إصدار معاينة، لذلك قد يتغير API الخاص به.موجّه لـ Nuclo يدعم التصيير على الخادم والترطيب وتقسيم كود المسارات والتحميل المسبق وقت الخمول، ومبني على list() وupdate(). حجمه نحو 4.9 كيلوبايت بعد ضغط gzip، ولا يعتمد إلا على Nuclo نفسه. وبفضل خلوّه من الآثار الجانبية، تستطيع أدوات البناء استبعاده عند عدم استخدامه.
$ npm install nuclo nuclo-router
أو استخدم pnpm add أو yarn add أو bun add بنفس الاسمين. يحتاج إلى nuclo 0.3 أو أحدث باعتباره تبعية نظير.
يحدد الموجّه ما يُحمَّل، ويحدد التطبيق موضع عرضه باستخدام router.outlet(): استدعاء list() واحد يعرض مكدس الطبقات، بصف لكل طبقة، فتكون الصفحة العادية صفًا واحدًا. يبني الخادم والعميل الشجرة نفسها، لذا يعيد hydrate() استخدام عقد الخادم. ويعتمد ما تبقّى على history ومستمع click مفوّض واحد وrequestIdleCallback.
استيراد واحد
export const router = createRouter(routes) // define once, import anywhere await router.start() // resolve the current URL render(App, document.getElementById('app')!) // App places router.outlet() and reads router.path, router.go(), …
ينتظر start() تحميل وحدات المسار المطابق قبل اكتماله، فتكون الشجرة الأولى مكتملة على الخادم والعميل. بعد ذلك، تقرأ خصائص الموجّه ودواله المسار الذي بدأه: router.path وrouter.pending وrouter.go().
تطبيق كامل
// isomorphic: imported by the browser and the server import { createRouter } from 'nuclo-router' export const router = createRouter({ '/': () => import('./pages/Home.ts'), '/docs': () => import('./pages/Docs.ts'), '/blog/:slug': () => import('./pages/Post.ts'), '*': () => import('./pages/NotFound.ts'), }) export const App = () => div( header(a({ href: router.href('/') }, 'Home'), a({ href: router.href('/docs') }, 'Docs')), main( when(() => router.pending, Spinner()), when(() => router.error !== null, ErrorView()), router.outlet(), // where the pages land ), footer('© nuclo'), )
import type { RouteContext } from 'nuclo-router' import { router } from '../routes.ts' // A page is plain content export default function Post(ctx: RouteContext) { // A query-only change keeps the page, so the query is read live. return article(h1(ctx.params.slug), p(() => (router.search.has('preview') ? 'draft' : ''))) }
import 'nuclo' import { router, App } from './routes.ts' await router.start() hydrate(App, document.getElementById('app')!) // or render() without SSR
import 'nuclo/polyfill' import 'nuclo' import { renderToString } from 'nuclo/ssr' import { router, App } from './routes.ts' // one Route per request const route = await router.start(request.url) const html = route.run(() => renderToString(App))
هذا هو التكامل بأكمله. تعمل الروابط لأن <a href> هو مجرد <a href>. يعترض مستمع واحد مفوض على document الروابط التي يمتلكها الموجّه ويترك كل الروابط الأخرى للمتصفح.
على الخادم، تقوم start() بإرجاع Route لكل طلب وتترك الموجّه بمفرده، لذلك لا يمكن للطلبات المتزامنة رؤية مسار بعضها البعض. route.run() يجعل ذلك المسار هو الذي يقرأه الموجّه، وبالتالي المنفذ، طوال مدة العرض. يوجد في المتصفح مسار واحد لكل تحميل صفحة، ويجعله start() ملكًا للموجّه. ليست هناك حاجة إلى run().
تتلقى الصفحة RouteContext وPageProps، كـ (ctx, { layer, data, outlet, beforeLeave })، وقد تأخذ وسائط أقل. تقوم بإرجاع أي شيء يقبله list(). لملء جزء آخر من التخطيط أيضًا، قم بإرجاع into() من داخل الصفحة، مثل div(article(…), into('sidebar', nav(…))). يغادر هذا المحتوى مع الصفحة. راجع الصفحات & المناطق .
| النمط | ما يطابقه | params |
|---|---|---|
/، /docs/intro | بالضبط ذلك المسار | {} |
/blog/:slug | /blog/hello | { slug: 'hello' } |
/u/:id/edit | /u/7/edit | { id: '7' } |
/files/*rest | /files/a/b.txt | { rest: 'a/b.txt' } |
* | أي شيء لا مثيل له، ويعرض صفحة 404 | { '*': '…' } |
تُفحص المسارات الثابتة أولًا، ثم الديناميكية بترتيب تعريفها، ثم المسارات الشاملة مع تقديم أطول بادئة. لذا فإن '*' يطابق فقط ما لم يطابقه أي مسار آخر ويمكن وضعه في أي موضع من الجدول. تتم تسوية الخطوط المائلة الزائدة والشرطة المائلة المكررة وترميز النسبة المئوية، ويتم فك تشفير القيم المعلمية. المطابقة هي مقارنة مقطعية لكل نمط. لم يتم إنشاء أو تشغيل أي تعبيرات عادية.
المسارات النسبية
النمط المكتوب بـ ./ يتعلق بالأصل الذي يعلن عنه، و href المكتوب بـ ./ يتعلق بالمسار النشط. يصنعون معًا قسمًا قابلاً لإعادة الاستخدام ويعني نفس الشيء تحت كل والد. قيمة الجدول هي إما محمل أو جدول آخر، مفاتيحه مرتبطة به:
import type { RouteTable } from 'nuclo-router' // a section, with no idea where it lives export const recordSection: RouteTable = { '/': () => import('./Record.ts'), // the parent path itself './preview': () => import('./Preview.ts'), './preview/raw': () => import('./Raw.ts'), }
// declare it wherever it belongs createRouter({ '/': () => import('./pages/Home.ts'), '/invoices/:id': recordSection, '/customers/:id': recordSection, '*': () => import('./pages/NotFound.ts'), })
ينتج عن هذين السطرين جميع الأنماط الستة دون كتابة أي مسار مرتين. يحصل كلا العناصر الأبوية على نفس كائنات المُحمل، لذلك يتشاركان في وحدة تخزين مؤقت واحدة ووحدة import() واحدة.
- داخل الجدول الفرعي،
'./x'،'/x'و'x'جميعها تعني نفس الشيء،'/'تعني المسار الأصلي نفسه، و'*rest'يعطي القسم خاصية التقاط الكل الخاصة به. التعشيش عميق كما تريد. - يتم استخدام مفاتيح المستوى الأعلى كما هو مكتوب، لذا يبقى
'*''*'. - تؤدي إضافة مسار فرعي تحت مسار أبوي من نوع
*catch-allإلى خطأ فيcreateRouter()، لأن الجزء الشامل يجب أن يكون في نهاية النمط.
الصفحات الأبوية وoutlet()
تشكّل الصفحة الأبوية وصفحاتها الفرعية تسلسلًا هرميًا حقيقيًا. تُصيَّر الصفحة الأبوية ويُصيَّر مسارها الفرعي داخلها في موضع استدعاء outlet():
import type { PageProps, RouteContext } from 'nuclo-router' // a parent page takes its outlet from (ctx, { outlet }) export default function Record(ctx: RouteContext, { outlet }: PageProps) { return div(Header(ctx), input({ id: 'notes' }), main(outlet())) }
التنقل بين العناصر الفرعية لا يؤدي إلى إعادة بناء الأصل. عقدة DOM الخاصة بها هي نفس العقدة، لذا فإن تركيزها وموضع التمرير الخاص بها والمدخلات نصف المكتوبة وأي حالة في إغلاقها كلها باقية.
- الصفحة التي لا تحتوي على مسار فرعي لا تعرض أي شيء من منفذها، لذلك يمكن للتخطيط استدعائها دون قيد أو شرط. إذا لم تستدعِ الصفحة الأبوية
outlet()، فلن يظهر مسارها الفرعي. - لا توجد مسارات فهرس.
'/'هو التخطيط نفسه، لذا فإن المسار الأصلي يعرض التخطيط بمنفذ فارغ بدلاً من صفحة فهرس منفصلة بداخله. ضع المحتوى المقصود في التخطيط، أو أعطه مسارًا فرعيًا حقيقيًا.
تعمل الاستدعاء نفسه من أي من العناصر الأبوية، ومن صفحة لا تعرف الصفحة الموجودة فيها:
router.href('./preview') // '/invoices/42/preview' or '/customers/7/preview' await router.go('./preview') await layer.push('./preview') // the usual shape: a child layer
./x، ../x، . و.. يتم حلها وفقًا لمسار المسار الحالي، وليس "دليل" عنوان URL. من /invoices/42، ./preview هو /invoices/42/preview، بينما يحلّها المتصفح إلى /invoices/preview، ولا تغير الشرطة المائلة اللاحقة على عنوان URL شيئًا. ينتقل ../ إلى أعلى المقطع ويتوقف عند الجذر. يُحتفَظ بالاستعلام والجزء #hash، وإعادة إضافة base، والمسار المطلق الذي تم حله هو ما يدخل في السجل. هذا هو نفس الانحراف عن حل عنوان URL الذي يقوم به React Router لـ <Link to="./x">.
- فقط
./و../مرتبطان بالمسار. يتم تركpreviewالعاري في href لقواعد المتصفح العادية. - أنشئ روابط نسبية باستخدام
router.href('./preview')بدلاً من كتابةhref="./preview"يدويًا. يتم حلhref()في وقت العرض، وبالتالي فإن العلامات التي يتوفرها تكون مطلقة ولا تزال صحيحة للزاحف، وللنقرة بالزر الأوسط، وعند تعطيل JavaScript.
ما يُحتفَظ به وما يُعاد بناؤه
| العقدة | عند التنقل |
|---|---|
| صفحة أبوية لم يتغير نمطها أو مسارها | يُحتفَظ بها: العقدة نفسها، ولا تُشغَّل load() الخاصة بها مجددًا |
| الصفحة المطابقة | يُحتفَظ به عندما لا يتغير نمطها ومسارها ولا تحتوي على load()، لذلك يحتفظ بها الاستعلام أو تغيير التجزئة. يتم تحديث المسار المزود بالمحمل في كل زيارة. |
| الوالد الذي تغير مساره | أعيد بناؤه، بالإضافة إلى كل شيء تحته |
| الطبقة المضافة | تعرض الصفحة المطابقة وحدها. تم تثبيت أصولها بالفعل في الصف الموجود أسفلها. |
تحافظ الصفحة المحفوظة على ctx التي تم إنشاؤها بها. هذا هو معنى ctx في كل مكان: اقرأ router.* للقيم الحية. الحالة المستندة إلى الاستعلام، مثل المرشحات وتقسيم الصفحات وعلامات التبويب، تقرأ router.search داخل قيمة ديناميكية وتكتب باستخدام router.go('?page=2', { replace: true }). تبقى الصفحة وتركيزها ومدخلاتها نصف المكتوبة في مكانها.
الصفحات والمناطق
#router.outlet() يضع الصفحة في مكان واحد. الصفحة التي تحتاج أيضًا إلى شريط جانبي أو شريط أدوات أو عنوان في مكان آخر في التخطيط، تملأها بـ into(). يحدد التخطيط المكان بـ region()، وتقوم الصفحة بإرجاع into() بجوار المحتوى الخاص بها. لا يتلقى التخطيط أبدًا محتوى الصفحة كخاصية، ولا يعيد التنقل بناءه أبدًا.
export const App = () => div( aside( input({ placeholder: 'Filter' }), // keeps its text across navigations region({ id: 'sidebar', empty: p('Nothing to show.') }), ), main(router.outlet()), )
import type { PageProps, RouteContext } from 'nuclo-router' export default function Post(_ctx: RouteContext, { data: post }: PageProps<Post>) { return div( article(h1(post.title), p(post.body)), into('sidebar', nav(h2('In this post'), ul(list(() => post.headings, (h) => li(h))))), ) }
محتوى الصفحة عبارة عن صف من list() الخاص بمنفذ العرض، ويعيش into() طالما أن الصف الذي يحمله. وبالتالي فإن محتوى الشريط الجانبي يصل مع الصفحة ويغادر معها: يؤدي الانتقال إلى صفحة بدون into('sidebar', …) إلى إظهار محتوى empty للمنطقة مرة أخرى. يمكن للصفحة استخدام ctx وdata لكلا الجزأين، حيث تم إنشاء كليهما بنفس الدالة.
ما يحدث عند التنقل
- الصفحة المحفوظة تحافظ على مناطقها. استعلام أو تغيير تجزئة يحافظ على الصفحة لا يمس محتوى المنطقة أيضًا. اقرأ
router.searchداخل قيمة ديناميكية إذا كان الشريط الجانبي يعتمد عليها. - تقوم الصفحة المعاد بناؤها بإعادة ملئها. عندما يتغير المسار، تتم إزالة الصف القديم وتتولى طرق عرض
into()للصفحة الجديدة. لم يبق شيء وراءهم. - تضيف الطبقة عرضًا جديدًا. الطبقة صف لاحق في منفذ العرض، لذا يمثل
into()الخاص بها عرضًا أحدث للمنطقة نفسها. مع القيمة الافتراضيةtype: 'latest'، يحل محل عرض الصفحة حتى إغلاق الطبقة، ثم يظهر عرض الصفحة مجددًا. ومعtype: 'stack'يظهر العرضان. - يمكن للصفحة الرئيسية أن تملأ المناطق أيضًا. تظل عروضها بينما تتغير فروعها، لأنه لم تتم إعادة بناء الصفحة الرئيسية.
- يعمل التصيير على الخادم مباشرة. يقوم الخادم بإنشاء الصفحة داخل
route.run()، لذا فإن عروضinto()الخاصة به موجودة في HTML، ويعيدhydrate()استخدام عقدها.
عدة مناطق من صفحة واحدة
قم بتمرير كائن لملء أكثر من منطقة في وقت واحد. لا يزال المحتوى الرئيسي للصفحة يمر عبر المنفذ، لذا قم بإعادته بجانب into():
export default function Report(ctx: RouteContext) { return div( table(…), into({ toolbar: [button('Export'), button('Print')], sidebar: nav(a({ href: router.href('./summary') }, 'Summary')), }), ) }
اختيار الأداة المناسبة للمهمة
outlet()مخصص لمسار فرعي: المحتوى الذي يقرره عنوان URL، ويتم عرضه داخل صفحته الأصلية. راجع المسارات المتداخلة .into()مخصص للصفحة المطابقة التي تنشر نفسها عبر التخطيط. ينتمي المحتوى إلى الصفحة ويستخدم بياناتها.matches()مخصص للتخطيط الذي يتم تحديده من خلال عنوان URL وحده، مع جدول صغير خاص به ولا يتضمن أي صفحة. انظرmatches().
تنتمي كل هذه العناصر إلى المسار المطابق الواحد. لا يقوم الموجّه بتوجيه منطقتين مستقلتين من عنوان URL واحد، لذا فإن الشريط الجانبي بمساراته الخاصة هو منطقة تملأها الصفحة، وليس منفذًا ثانيًا. الشريط الجانبي الذي يريد حقًا الموجّه الخاص به، والتحميل والرجوع، يبقيه غير قيد التشغيل ويتم حله باستخدام match() - راجع الموجّهات المتعددة & التحميل المشترك.
يمكن لوحدة الصفحة تصدير دالة load. تُنفَّذ قبل بناء الصفحة عند كل انتقال إلى مسارها، وتُمرَّر نتيجتها إلى الصفحة باسم data:
import type { DataLoader, PageProps, RouteContext } from 'nuclo-router' export const load: DataLoader<Post> = (ctx) => fetchPost(ctx.params.slug) export default function Post(_ctx: RouteContext, { data: post }: PageProps<Post>) { return article(h1(post.title), p(post.body)) }
لا يتغير جدول التوجيه؛ يبقى المحمّل مع وحدة الصفحة. يستقبل RouteContext المطابق، فتتوفر له params وsearch وpath. ويمكن أن يكون متزامنًا أو غير متزامن. يعمل على الخادم أيضًا، فتتضمن مخرجات HTML بيانات الصفحة بدلًا من جلبها بعد الترطيب.
تم تخزين الوحدة مؤقتًا. البيانات ليست كذلك. يُحمَّل جزء كود المسار مرة واحدة ويُعاد استخدامه، بينما يُنفَّذ محمّل البيانات عند كل انتقال. لذلك لا تختلط بيانات /blog/a و/blog/b، ويمكن ببساطة إعادة محاولة المحمّل إذا فشل.
| الحالة | السلوك |
|---|---|
| أثناء تشغيله | router.pending بقيمة true والصفحة التي كنت عليها تبقى معروضة |
| يرمي استثناءً أو يُرفض الوعد الذي يُرجعه | يُحفظ الخطأ في router.error. لا يُرفض وعد go()، ويمكن إعادة محاولة التنقل. |
أثناء start() | يُرفض وعد start() نفسه؛ فلا يوجد كائن Route بعد لحفظ الخطأ فيه. |
| الطبقة المضافة | تدير المُحمل الخاص بها، مع بياناتها الخاصة |
| التحميل المسبق وقت الخمول | يحمّل الوحدات فقط، ولا يجلب البيانات: لا يملك سياقًا يمرّره للمحمّل، وقد لا يزور المستخدم ذلك المسار أبدًا. |
| إعادة التنقل إلى نفس URL | يتم تشغيل المُحمل مرة أخرى وإعادة بناء الصف. المسار الذي لا يحتوي على محمّل يحتفظ بصفه. |
لإعادة التحقق، انتقل إلى المكان الذي تتواجد فيه بالفعل دون إضافة إدخال سجل:
await router.go(router.url, { replace: true }) // runs load() again
بدء تحميل البيانات مع جزء الكود
يمكن تشغيل load الذي تم تصديره بواسطة وحدة الصفحة فقط بمجرد وصول هذه الوحدة، لذا فإن التنقل البارد يجلب القطعة، ثم البيانات. لبدء تشغيلهما معًا، قم بتعريف المُحمل في الجدول باستخدام withLoad():
import { createRouter, withLoad } from 'nuclo-router' import { loadPost } from './data.ts' // small, in the main bundle createRouter({ '/blog/:slug': withLoad(() => import('./pages/Post.ts'), loadPost), })
يحل هذا المحمّل محل load الخاص بالوحدة ويعمل عند كل زيارة. وإذا فشل أثناء استمرار تحميل جزء الكود، يُحفظ الخطأ في router.error كغيره من أخطاء التحميل.
لا تُضمَّن بيانات الخادم بعد تسلسلها في HTML تلقائيًا، وهذا مقصود. يعمل load الخاص بالعميل مرة أخرى عند الترطيب. إذا كان ذلك مهمًا، فاطلب من الخادم إصدار route.data، على سبيل المثال window.__DATA__، وسيأخذه المُحمل أولاً. route.data هي بيانات الصفحة المطابقة فقط. نتيجة load() لأحد العناصر الأبوية ليست موجودة عليه.
تقسيم الكود والتحميل المسبق
قيمة المسار هي دائمًا دالة تُرجع مكون الصفحة، لذلك يُحمَّل المكوّن عند الحاجة:
'/a': () => import('./A.ts'), // default export (+ optional load) '/b': () => import('./B.ts').then(m => m.BPage), // named export '/c': () => CPage, // eager, not split '/d': () => ({ default: DPage, load: loadD }), // eager, module-shaped
فقط الأشكال على شكل وحدة يمكن أن تحمل تصدير load. يؤدي اختيار مكون مسمى باستخدام .then(m => m.BPage) إلى تسليم الدالة للموجّه وحده.
يتم استيراد كل وحدة مرة واحدة وتخزينها مؤقتًا. تشترك عمليات التنقل المتزامنة لنفس المسار في import() واحد. بمجرد أن يترك المستخدم الصفحة بمفردها لـ idleTimeout (ثانية افتراضيًا؛ بدون نقرات أو مفاتيح أو عجلة أو تمرير)، يتم تحميل المسارات المتبقية في الخلفية، مسار واحد لكل شريحة خاملة (requestIdleCallback). يؤدي أي إدخال أو تنقل ينتظره المستخدم إلى إيقافه مؤقتًا حتى تصبح الصفحة خاملة مرة أخرى.
المسار المحمَّل مسبقًا أو المتاح مباشرة لا يغيّر router.pending، ويظهر قبل رسم الإطار التالي، فلا يظهر مؤشر تحميل عابر ولا يتغير التخطيط بسببه. تعبّر router.pending فقط عن انتظار وحدة أو محمّل، وتظل الصفحة السابقة معروضة خلال الانتظار. يُحفظ فشل تحميل جزء الكود في router.error بدلًا من رفض وعد غير معالج، ويمكن إعادة المحاولة.
واجهة API للموجّه
#createRouter(table, options?)
table يقوم بتعيين نمط إلى محمّل، أو إلى جدول متداخل. الخيارات:
| الخيار | القيمة الافتراضية | |
|---|---|---|
base | '/' | URL البادئة التي يتم تقديم التطبيق ضمنها، مثل '/docs'. إنه يتطابق فقط عند حدود المقطع: /docs/intro موجود داخل '/docs'، /docsearch ليس كذلك. عناوين URL الموجودة خارجه لا تتطابق مع أي شيء، لذلك يرفض start() (404 من جانب الخادم الخاص بك) ويتم ترك الروابط إليها في المتصفح. |
preload | true | قم بتحميل وحدات المسارات المتبقية في الخلفية بمجرد أن تكون الصفحة خاملة. false يقوم بإيقاف تشغيله. |
idleTimeout | 1000 | ميلي ثانية بدون نقرات أو مفاتيح أو عجلة أو تمرير قبل بدء التحميل المسبق، أو استئنافه بعد أن كان المستخدم مشغول. |
onNavigate | (ctx, kind) => void، يتم استدعاؤه بعد كل تنقل تم حله في المتصفح، بما في ذلك التنقل الأولي، بعد مزامنة DOM. kind هو 'start' أو 'push' أو 'replace' أو 'pop' (للخلف أو للأمام أو close()) أو 'layer'. الخطاف الخاص بـ document.title والعلامات الوصفية والتركيز والتمرير والتحليلات. لم يتم الاتصال مطلقًا أثناء SSR. |
يوفر الموجّه start() وmatch() وoutlet() وmatches()، وجميع أعضاء Route باستثناء run(). تقرأ هذه الأعضاء المسار النشط: آخر مسار أعاده start() في المتصفح، أو المسار الذي يُنفَّذ run() له على الخادم. تؤدي قراءة عضو من دون مسار نشط إلى رمي استثناء. يحتفظ الموجّه بجدول المسارات والمطابِق المصرّف وذاكرة الوحدات المؤقتة، فتتشاركها الطلبات على الخادم.
router.start(url?)
يحل url (الافتراضي: location.href في المستعرض) إلى وحدات الصفحة الخاصة به ويعيد Promise<Route>. يتم الرفض عندما لا يتطابق أي شيء ولا يحتوي الجدول على مسار '*'. على الخادم، هذا هو 404 الخاص بك. في المتصفح، يقوم أيضًا بجعل المسار نشطًا للموجّه، وإرفاق popstate ومستمعي click المفوضين وجدولة التحميل المسبق. يوقف استدعاء start() الثاني المسار السابق. على الخادم، لا يمس أي شيء مشترك، لذا يتم عرضه داخل route.run().
router.match(url?)
يحل عنوان URL مقابل الجدول دون تحميل أي شيء: لا يوجد استيراد، ولا سجل، ولا مستمعون، ولا مسار. تقوم بإرجاع RouteContext، أو null عندما يكون عنوان URL خارج base، أو لا يتطابق مع أي نمط، أو في المتصفح، يكون من أصل آخر. عناوين URL المطلقة فقط، نظرًا لأن حل ./ تحتاج إلى مسار نشط.
// 404 before importing a single chunk if (!router.match(request.url)) return new Response(null, { status: 404 }) // "is this link ours?" for a custom link component or a prefetch rule const owned = router.match(href) !== null
router.outlet()
صفحات المسار النشط كـ list(). ضعه مرة واحدة، أينما ذهبت الصفحات. الطبقة المضافة هي صف لاحق منها. وهو فارغ قبل حل start() وبعد stop(). يعرض على الخادم المسار الذي يتم تنفيذ run() الخاص به.
router.matches(table)
يعرض عرض الجدول الثاني الذي يطابق المسار النشط، مثل list() الذي تضعه في أي مكان. انظر matches().
Route
كل عضو باستثناء run() موجود أيضًا على الموجّه، حيث يقرأه التطبيق.
| عضو | |
|---|---|
path, pattern, params, search, hash, url | المطابقة النشطة. search كائن URLSearchParams. search, hash وurl قيم حية. |
pending | التنقل في انتظار وحدة نمطية أو محمل |
error | آخر تحميل فاشل، تم مسحه بواسطة التنقل التالي |
data | نتيجة load() للصفحة المطابقة، لتمريرها من الخادم بعد تسلسلها. لا تتضمن بيانات الصفحات الأبوية. |
depth | عدد الطبقات الموجودة في المكدس (طبقة واحدة للصفحة العادية) |
go(href, { replace }) | ينتقل ويحمّل الوحدة عند الحاجة. لا يُرفض وعده مطلقًا: يُحفظ فشل الوحدة، سواء برفض الوعد أو رمي استثناء، في error. |
push(href) | يفتح المسار كطبقة جديدة في الأعلى، ويُرجع وعدًا يكتمل بنتيجة إغلاقها. يقبل ./. يُرفض الوعد عند غياب تطابق أو فشل الوحدة. |
href(path) | يضيف بادئة base إلى المسار لاستخدامه في a({ href }). يحل ./ و../ نسبةً إلى المسار النشط. |
stop() | يفصل المستمعات ويلغي الطبقات المفتوحة ويفرغ منفذ العرض. يمكن تكرار استدعائه بأمان. يحتفظ المسار المتوقف بآخر مطابقة، لكن go() لا يفعل شيئًا. |
run(fn) | استدعِ fn باستخدام هذا المسار باعتباره المسار النشط للموجّه وقم بإرجاع نتيجته: route.run(() => renderToString(App)) على الخادم. متزامن فقط. يتوقف المسار عن النشاط عند عودة fn. |
RouteContext
الوسيطة الأولى للصفحة: path، pattern، params، search، hash، url. إنه السياق الذي تم إنشاء الصفحة به. اقرأ router.* لمعرفة القيم الحية. وهي تختلف فقط عندما يتغير الاستعلام أو التجزئة دون إعادة البناء.
params هو { [name]: string } بشكل افتراضي. يمكن للصفحة استنتاج أنواعها من النمط الخاص بها باستخدام Params، والذي يحول '/blog/:slug' إلى { slug: string } و'/files/*rest' إلى { rest: string }:
import type { Params, RouteContext } from 'nuclo-router' export default function Post(ctx: RouteContext<Params<'/blog/:slug'>>) { return article(h1(ctx.params.slug)) // slug: string, nothing else exists }
PageProps
الوسيطة الثانية للصفحة. استخدم تفكيك الكائن لاستخراج ما تحتاجه الصفحة.
| الخاصية | |
|---|---|
layer | حيث توجد الصفحة في المكدس. انظر Layer أدناه. |
data | ما عاد به المسار load()، أو undefined بدون واحد. يُحدَّد نوعها باستخدام PageProps<TData>. |
outlet | حيث تعرض الصفحة الرئيسية مسارها الفرعي. راجع المسارات المتداخلة. |
beforeLeave | يسجّل دالة تحقق تُستدعى قبل مغادرة الصفحة. راجع حواجز المغادرة. |
Layer
حيث توجد الصفحة في المكدس، وطريقها للخروج.
| عضو | |
|---|---|
depth | 0 للصفحة الأساسية، 1 وما فوق للطبقات المضافة |
push(href) | افتح طبقة أخرى من داخل الصفحة، مثل Route.push() |
close(result?) | يغلق هذه الطبقة وكل ما فوقها، ويُكمل وعد push() الذي فتحها. من دون وسيطة، تُعدّ الطبقة ملغاة وتكون النتيجة undefined. لا يفعل شيئًا عند العمق 0. |
DataLoader وwithLoad()
DataLoader هو (ctx: RouteContext) => TData | Promise<TData>، وهو تصدير اختياري لوحدة الصفحة load. يقوم withLoad(loader, load) بإنشاء مسار يبدأ load بجانب استيراد جزء الكود الخاص به بدلاً من أن يبدأ بعده. راجع محمّلات المسارات .
الأنواع
كل نوع هو تصدير مسمى لـ nuclo-router، لذلك يمكن كتابة صفحة أو مُحمل أو حاجز مغادرة دون الاستيراد من المسار.
| النوع | ما هو |
|---|---|
Params<'/blog/:slug'> | كائن المعلمات للنمط. RouteParams هو الشكل العام دون استنتاج من النمط، وMatch هي نتيجة المطابق. |
PageComponent | الصفحة: (ctx, { layer, data, outlet, beforeLeave }) => … |
RouteModule | وحدة الصفحة: المكون نفسه، أو { default, load? } كـ import() |
RouteLoader | () => RouteModule | Promise<RouteModule>، يحمل بشكل اختياري load من withLoad() |
RouteTable | يأخذ الجدول الذي يقبله createRouter(): النمط إلى المُحمل، أو إلى جدول متداخل |
RouterOptions | { base?, preload?, idleTimeout?, onNavigate? } |
NavigateOptions | { replace? }، الوسيطة الثانية لـ go() |
NavigationKind | 'start' | 'push' | 'replace' | 'pop' | 'layer'، الوسيطة الثانية لـ onNavigate |
Outlet | نوع outlet() من PageProps |
LeaveGuard | (to) => boolean | void | Promise<…>، ما يأخذ beforeLeave() |
MatchView | عرض لـ matches(): يستقبل المعلمات ويُرجع المحتوى أو import() لوحدة صفحة |
push() يفتح مسارًا أعلى المسار الحالي بدلاً من استبداله، ويُرجع وعدًا يكتمل بالنتيجة التي تُغلق بها تلك الطبقة. نظرًا لأن الصفحات عبارة عن list() واحدة فوق المكدس، فإن فتح الطبقة يعد إلحاقًا. لم تتم إعادة عرض الصفحة الموجودة بالأسفل، لذا فإن DOM وتركيزها ونموذجها نصف المملوء كلها لا تزال موجودة عند إغلاق الطبقة.
هذا يجعل تدفق "أحتاج إلى سجل غير موجود بعد" يعمل دون التخلص من النموذج الذي كان المستخدم يملؤه:
// the page underneath const created = await layer.push<Option>('/options/new') if (created) { options.push(created); selected = created.id; update() }
// the pushed page: (ctx, { layer }) layer.close(created) // resolves the push() above with the new option layer.close() // dismissed: resolves with undefined
يمكن تداخل الطبقات. مربع الحوار الذي يحتاج إلى مربع حوار خاص به يستدعي layer.push() وينتظره، دون الإشارة إلى المسار.
- عنوان URL يعبّر عن الطبقة العليا. تضيف
push()إدخال سجل، لذا تكون الطبقة قابلة للربط ويغلقها زر الرجوع، مما يؤدي إلى حلpush()الخاص بها بالقيمة undefined عند الإلغاء، تمامًا مثل الإلغاء. يعد Back وclose()مسارًا برمجيًا واحدًا: يسجلclose()نتائجه ويعود بالتاريخ، والعمق فيhistory.stateهو المرجع. تحتفظ المتصفحات بحوالي 50 إدخالًا في علامة تبويب، لذلك بعد السير لفترة طويلة عبر الطبقات، قد يختفي الإدخال الموجود أسفلها: ثم يغلقclose()الطبقات الموجودة في مكانها ويعيد كتابة الإدخال الحالي إلى الصفحة التي تركت تظهر. - التنقل العادي يمسح المكدس. النقر على الرابط أو
go()ليس طبقة. فهو يستبدل كل شيء ويستبعد كل طبقة مفتوحة، لذلك لا يبقى أي متصل في انتظاره. - فتح الرابط مباشرة يعرض صفحة مستقلة. يقوم عنوان URL بتسمية الطبقة العليا ولكن ليس المجموعة الموجودة أسفلها، لذا فإن فتح عنوان URL الخاص بالطبقة يعرضها مباشرة كصف واحد، مع
layer.depth === 0وclose()الخامل. يجب أن يعمل المسار المفتوح كطبقة عند فتحه مستقلًا أيضًا. استخدم شرطًا علىlayer.depthلعرض رابط بدلًا من زر إلغاء لا يعمل. - لا يبقى الوعد بعد إعادة تحميل الصفحة ، لأنه يُحفظ في إغلاق برمجي (closure). تعامل مع النتيجة باعتبارها طريقًا مختصرًا عبر التدفق الذي يعمل أيضًا على طول الطريق.
- لم يبق شيء في الانتظار.
stop()يغلق كل طبقة مفتوحة من دون نتيجة، ويتم حل الطبقة المهجورة بواسطة التنقل باستخدامundefined.
حواجز المغادرة
الصفحة التي تحتوي على شيء يستحق الاحتفاظ به، مثل نموذج نصف مملوء، تقوم بتسجيل حاجز مغادرة. يتم طلبه قبل أن يتم تجاهل الصفحة، ويحتفظ إرجاع false (أو الوعد به) بالصفحة:
import type { PageProps, RouteContext } from 'nuclo-router' export default function Edit(_ctx: RouteContext, { beforeLeave }: PageProps) { let dirty = false beforeLeave(() => !dirty || confirm('Discard your changes?')) return form(input({ onInput: () => (dirty = true) }), …) }
- عند مغادرة الصفحة فقط. لا يُسأل التنقل الفرعي ضمن التخطيط، أو الاستعلام أو تغيير التجزئة الذي يحافظ على الصفحة. يعيش حاجز المغادرة ويموت مع صفحته، لذلك لا يوجد شيء لإلغاء تسجيله.
- كل مخرج. رابط،
go()، للخلف وللأمام،close()للطبقة، وgo()إلى عنوان URL لا يمتلكه الموجّه (مغادرة التطبيق، حيثtoهوnull). يتم الرجوع إلى الخلف أو الأمام المرفوضين، لذلك يبقى شريط العناوين على الصفحة. إغلاق علامة التبويب أو إعادة التحميل هي مهمةbeforeunload. toهوRouteContextالذي يتجه إليه التنقل. تُنفّذ حواجز المغادرة بدءًا من الأعمق ويتوقفون عند الرفض الأول. إذا رمى أحدها استثناءً، تبقى الصفحة. يمرclose(result)عبر حاجز المغادرة أيضًا، لذا قم بمسح علمك قبل الإغلاق بنتيجة.- لا يستطيع الموجّه تطبيق حواجز المغادرة إلا على إدخالات السجل التي أنشأها. لا يُستدعى الحاجز عند الرجوع أو التقدم إلى إدخال أُنشئ خارجه، مثل رابط
#fragmentعادي أو استدعاءpushStateمن برنامج نصي آخر.
matches() والموجّهات الفرعية
#router.outlet() يعرض الصفحة. يعرض router.matches(table) شيئًا آخر يعتمد على مكان تواجدك، مثل الشريط الجانبي أو مسارات التنقل أو شريط الأدوات، من جدوله الصغير، دون تحميل مسار أو إضافة صفحة:
div( aside( router.matches({ '/': () => nav('Home links'), '/blog/new': () => nav('Drafting tips'), '/blog/:slug': ({ slug }) => nav(`More like ${slug}`), '*': () => import('./Sidebar.ts'), // code-split, rendered once loaded }), ), main(router.outlet()), )
- نفس المطابقة لجدول التوجيه. الأنماط والأسبقية ومطابقة المسار الأولي وتجريد
baseهي خاصة بالموجّه، لذا فإن العرض والصفحة يتفقان دائمًا على عنوان URL. يقرأ مسار الطبقة العليا، مثلrouter.path. - العرض هو
(params, ctx) => content، حيث تُستنتج أنواعparamsمن النمط الخاص به وctxهوRouteContextمعpatternوparamsللعرض. وقد يُرجع بدلاً من ذلكimport()لوحدة الصفحة، والتي يتم استدعاؤها باستخدامctxبمجرد وصولها. - لا يوجد شيء مطابق، لا شيء يتم عرضه. أضف طريقة عرض
'*'للرجوع. إنه فارغ قبلstart()وبعدstop()، مثل المنفذ. - يتم إعادة العرض فقط عندما يتغير المسار ، لذلك يحتفظ العرض بـ DOM عبر أي شيء يحتفظ بالصفحة.
- لا يعرض العرض المؤجل أي شيء حتى وصول الوحدة النمطية الخاصة به. تم تسجيل
import()الفاشل معconsole.error، ولم يتم تعيينrouter.error، وتمت إعادة محاولته في التنقل التالي. لا يقومupdate()العادي بإعادة المحاولة. - على الخادم ، لا تقدم العرض المؤجل شيئًا، نظرًا لأن الوحدة الخاصة بها تصل إلى العميل. استخدم عرضًا متزامنًا لأي شيء يجب أن يحتوي عليه HTML الأول.
الموجّهات الفرعية
تدعم المسارات التداخل، لذا يكفي عادةً جدول متداخل لإنشاء موجّه فرعي. يتولى موجّه واحد التنقل: تمثل صفحة '/' الخاصة بالقسم إطار واجهته، وتُعرض الصفحات الفرعية في outlet() الخاص بها، فيبقى الإطار عند التنقل داخل القسم:
createRouter({ '/': …, '/docs': docsRoutes, '*': … })
أو امنح الميزة موجّه خاصًا بها، تم إنشاؤه ولكن لم يتم تشغيله مطلقًا. يمتلك الوالد مُدخلًا واحدًا شاملاً، ويقوم الطفل بتحليل الذيل مقابل الأنماط والمعاملات والمسار الاحتياطي الخاصة به:
// parent table '/docs/*rest': () => import('./docs/Shell.ts'),
const docs = createRouter(docsRoutes, { base: '/docs' }) const hit = docs.match() // { pattern: '/:topic', … } const page = await docsRoutes[hit.pattern]() // the app's own lookup
يجب إبقاء هذا الموجّه من دون بدء تشغيله، لأن start() يربط مستمعي الأحداث، وتشغيل موجّهين يسبب تعارضًا. يتولى الموجّه الفرعي المطابقة فقط، فتستدعي أنت محمّلات البيانات. يُعاد بناء واجهة القسم عند تغير بقية المسار، لأن الموجّه الرئيسي يرى مسارًا واحدًا وتتولى واجهة القسم حل الباقي. يتيح الجدول المتداخل الاحتفاظ بالصفحة الأبوية مركّبة، لذا يُفضَّل إلا إذا احتاج القسم فعلًا إلى موجّه وbase ومسار احتياطي خاص به.
يمكن أن يحتوي التطبيق على عدد من الموجّهات بقدر ما يحتوي على مناطق. القاعدة التي تبقي هذا الأمر بسيطًا: ابدأ واحدًا بالضبط لكل مستند، واترك كل موجّه آخر يظل غير قيد التشغيل، وحل عناوين URL باستخدام match(). يمتلك الموجّه الذي تم تشغيله فقط عنوان URL والسجل ومستمع النقرات. أما الباقي فهو عبارة عن جداول تطرح أسئلة عليها - ويمكن لكل منطقة تحميلها وفق توقيتها الخاص، مع مؤشر تحميل مستقل.
تحميل مشترك: سلسلة من المحمّلات
قد تقوم كل صفحة في السلسلة المطابقة بتصدير load(). في التنقل البارد إلى مسار فرعي، يقوم محمل التخطيط ومحمل الصفحة بتشغيل بالتوازي، ويغطي router.pending السلسلة بأكملها: تظل الصفحة السابقة على الشاشة حتى يكتمل تحميل الاثنين، لذلك لا يتم رسم شجرة نصف محملة أبدًا. يتلقى كل مكون data الخاص به:
'/shop': { '/': () => import('./pages/Shop.ts'), // layout, has a load() './:section': () => import('./pages/Section.ts'), // page, has a load() },
import type { DataLoader, PageProps, RouteContext } from 'nuclo-router' export const load: DataLoader<ShopData> = async () => { await wait(500) // a slow API return { sections: ['toys', 'books', 'music'] } } // the layout gets its own data, and its outlet for the child page export default function Shop(_ctx: RouteContext, { data, outlet }: PageProps<ShopData>) { return div( h1('Shop'), nav(...data.sections.map((s) => a({ href: router.href(`/shop/${s}`) }, s))), main(outlet()), ) }
export const load: DataLoader<SectionData> = async (ctx) => { await wait(900) // slower than the layout's return { section: ctx.params.section, items: catalog[ctx.params.section] } } // the page gets its own data: the two loaders ran beside each other export default function Section(_ctx: RouteContext, { data }: PageProps<SectionData>) { return article(h2(data.section), ul(...data.items.map((i) => li(i)))) }
يبدأ كلا المحمّلين معًا، وليس واحدًا تلو الآخر. يكون router.pending صحيحًا أثناء انتظار أي منهما، وبقيمة false بقية الوقت - لا يغيّره المسار المحمَّل مسبقًا أو المتاح مباشرة. يؤدي التنقل بين الأقسام إلى الحفاظ على التخطيط، بحيث يعمل مُحمل القسم فقط مرة أخرى؛ تظل بيانات التخطيط وDOM والمدخلات نصف المكتوبة جميعها باقية. تؤدي إعادة التنقل إلى نفس عنوان URL إلى تشغيل مجددًا محمّل الصفحة الطرفية وإعادة بناء صفها، تمامًا كما هو الحال مع صفحة واحدة.
تحميل متعدد: موجّه ثانٍ بمؤشر تحميل مستقل
للمنطقة المستقلة، مثل الشريط الجانبي أو الدرج أو المنطقة الحية، موجّه خاص يُنشأ من دون تشغيله، ومؤشر تحميل خاص بها. تتبع router.pending الموجّه الرئيسي الذي بدأ تشغيله فقط، لذا يمكن تحميل المنطقتين بالتوازي، وقد تنتهي أي منهما أولًا:
import { createRouter } from 'nuclo-router' // Created but never started: start() is what attaches the history and click // listeners, and only the main router is started. match() attaches nothing. const standIn = () => ({ default: () => div() }) // never rendered const sidebarRouter = createRouter({ '/shop': standIn, '/shop/:section': standIn, '*': standIn, }, { base: '/docs' }) // the same base as the main router export const sidebar = { loading: false, info: null as SidebarInfo | null } export async function refreshSidebar(at: string) { const hit = sidebarRouter.match(at) // the second router resolves if (!hit || (hit.pattern !== '/shop' && hit.pattern !== '/shop/:section')) { sidebar.info = null; update(); return } sidebar.loading = true // its own flag, its own spinner sidebar.info = null; update() const data = await fetchSidebar(hit.params.section) sidebar.loading = false; sidebar.info = data update() }
createRouter(routes, { onNavigate: (ctx, kind) => { announce(kind) refreshSidebar(ctx.url) // every navigation re-resolves the sidebar }, })
يُستخدم الموجّه الثاني للمطابقة فقط: يقرأ match() عنوان URL ويُرجع السياق، من دون إضافة مسارات الشريط الجانبي إلى الجدول الرئيسي. تُنفَّذ onNavigate بعد اكتمال كل تنقل، بما فيه الأول، فتوفّر موضعًا واحدًا لمزامنة المنطقة المستقلة مع عنوان URL.
pendingتتبع الموجّه الذي بدأ تشغيله. لا يوجد مسار نشط للموجّه الذي لم يبدأ تشغيله، ولا توجد قيمتاpendingولاerror. تتتبع المنطقة حالتها الخاصة، ويمكن عرض مؤشر تحميلها بجانب مؤشر تحميل المسار. في العرض التوضيحي، يتم تحميل الشريط الجانبي لمدة 800 مللي ثانية بينما تعمل محمّلات المسار لمدة 900 مللي ثانية، بحيث يمكن أن تنتهي الصفحة قبل الشريط الجانبي أو العكس.match()لا يربط مستمعي أحداث. يمكن لأي عدد من الموجّهات التي لم يتم تشغيلها مشاركة المستند؛ يحتوي كل منها على جدول خاص به وقاعدة ووحدة تخزين مؤقت خاصة به، ولا يلمس أي منها السجل. لا تحتاج سوى جدول ودالة مطابقة.- لا تُشغَّل محمّلاته تلقائيًا. يقوم الموجّه الذي تم تشغيله بتشغيل
load()للصفحات التي تطابقها. يكتفي الموجّه الذي لم يبدأ تشغيله بمطابقة المسار؛ فإذا احتاجت عروضه إلى بيانات، فاجلبها بنفسك، كما فيrefreshSidebar()الخاص بالعرض التوضيحي. لإبقاء مسؤولية التحميل لدى الموجّه الرئيسي، استخدم جدولًا متداخلًا أو منطقة تملؤها الصفحة. - تجاهل الاستجابات القديمة. يجب ألا يصل طلب الشريط الجانبي البطيء إلى القسم الذي انتقلت بعيدًا عنه. قم بزيادة عداد جيل الطلب عند كل تحديث وإسقاط الردود التي لم تعد متطابقة، كما يتجاهل الموجّه نفسه عمليات التنقل التي حلّت محلها عمليات أحدث.
- عرض
matches()المؤجل لا يحتوي علىpendingأيضًا. فهو لا يعرض شيئًا حتى تصل الوحدة الخاصة به. ثلاثة أنظمة تحميل، وثلاث ساعات مستقلة:pendingللمسار، ومؤشرات التحميل الخاصة بك، والعرض المؤجل الذي لم يتم الوصول إليه بعد.
معالجة الروابط
#يتنقل المستمع المفوض بالنقر بزر الماوس الأيسر على <a href>، بما في ذلك SVG <a> وينقر على أي شيء متداخل داخل واحد، ويتنحى جانبًا لكل شيء آخر: نقرات التعديل والنقرات الوسطى، target بخلاف _self، download، rel="external"، و روابط href إلى أصول أخرى، وروابط التجزئة فقط في الصفحة الحالية، والمسارات خارج base، والمسارات التي لا يتطابق أي مسار معها (لذلك لا يزال من الممكن الوصول إلى 404 الذي يعرضه الخادم)، وأي حدث يقوم معالج التطبيق بالفعل باستدعاء preventDefault() عليه. قم بإلغاء الاشتراك بشكل صريح مع data-nuclo-router="off".
التمرير
يحافظ التنقل الذي يضيف إدخالًا إلى السجل على موضع التمرير الحالي. عندما يحتوي عنوان URL الجديد على #hash يطابق عنصرًا في الصفحة، يتم تمرير هذا العنصر إلى العرض. popstate يترك التمرير إلى المتصفح. للوصول إلى صفحات جديدة في الأعلى، قم بذلك في onNavigate، الذي يوضح كيفية حدوث التنقل:
let shown = '' createRouter(routes, { onNavigate: (ctx, kind) => { // Not on Back/Forward (the browser restores those), nor for a query change on the same page. if (kind === 'push' && ctx.path !== shown && !ctx.hash) window.scrollTo(0, 0) shown = ctx.path }, })
ملاحظات
- لا يحتفظ المسار بعناصر DOM. يحتفظ Route بدالة الصفحة وسياقها فقط. يحرر الانتقال إلى صفحة أخرى الصفحة السابقة. يزيل
stop()المستمعات ويتخلى عن أي تحميل جارٍ ويمسحpending، فلا يبقى مؤشر تحميل عالقًا لمسار متوقف. تُتجاهل بصمت أي وحدة يكتمل تحميلها بعدstop(). - عزل آمن للطلبات المتزامنة على الخادم. تقوم
start()بإرجاع حالة كل طلب وتقومrun()بنطاقها على عرض متزامن واحد، لذا لا يمكن للطلبات المتداخلة رؤية مسار بعضها البعض. تتم مشاركة ذاكرة التخزين المؤقت للوحدة فقط. ينتهي النطاق عندما ترجع الدالةrun().renderToString()متزامن، وهذا يكفي. سيحتاج العرض غير المتزامن أو المتدفق إلى المسار المنقول عبرawait، وهو ما لا يفعلهrun(). - استعادة التمرير على
popstateهي عملية للمتصفح (scrollRestoration)، وليست للموجّه. - قم بتشغيل موجّه واحد لكل مستند. يضيف كل
start()مستمعclickالمفوض الخاص به، ولا يقومstart()الخاص بالموجّه الثاني بسحب المستمع الأول. فقطstart()ثانٍ على نفس الموجّه يفعل ذلك. لحل عنوان URL مقابل جدول آخر، قم بإنشاء موجّه واستدعِmatch()فقط. لا يربط مستمعي أحداث. يمكن لمنطقة مستقلة تشغيل التحميل الخاص بها بجانبpendingللمسار بهذه الطريقة. راجع الموجّهات المتعددة & التحميل المشترك. - غير مضمن، حسب التصميم: تجزئة وسجل الذاكرة، نظرًا لأن المسار موجود في اسم المسار ويجب أن يخدم الخادم كل مسار؛ منطقتان موجهتان بشكل مستقل يتم تشغيلهما بواسطة عنوان URL واحد، نظرًا لأن الصفحة يمكن أن تملأ عدة مناطق بـ
into()ولكنها تنتمي جميعها إلى المسار المطابق الواحد - لا يزال بإمكان المنطقة المستقلة الاحتفاظ بالموجّه الخاص بها والتحميل غير المبدئي، كما هو الحال في الموجّهات المتعددة & التحميل المشترك؛ والتحويم المسبق، والذي تحصل عليه عن طريق استدعاء محمّل المسار الخاصة منmouseenter.
أمثلة
هناك مثالان قابلان للتشغيل في المستودع يكمل كل منهما الآخر، لذلك يمكن ملاحظة كل إعداد في مكان ما:
examples/router: جولة من جانب العميل في API بالكامل. كل نموذج نمط وقاعدة أسبقية، وإلغاء الاشتراك في معالجة الارتباط، وكل عضو في المسار مع عناصر تحكم مباشرة، وموجّه فرعي، وطبقة مكدسة ثلاثية عميقة، وقسم نسبي مثبت على العناصر الأبوية، ومحملات المسار معpendingوerror. يضبطpreload: falseبحيث تظل هذه الحالات مرئية.examples/router-ssr:renderToString()لكل طلب، ترطيب يعيد استخدام عقد الخادم، رموز الحالة 404 الحقيقية،baseغير الجذر، التحميل المسبق الخامل الذي يمكنك مشاهدته في لوحة الشبكة، والمحمل الذي يتم تسليم بياناته من الخادم إلى العميل حتى لا تُجلب البيانات مجددًا عند الترطيب.
لا يوفر Nuclo واجهة API خاصة بالقيم المحسوبة. استخدم دالة عادية واستدعها من دوال القيم الديناميكية:
let items = ['apple', 'banana', 'cherry'] let filter = '' const visible = () => items.filter((item) => item.includes(filter)) const Fruits = () => div( p(() => `Showing ${visible().length} of ${items.length}`), ul(list(visible, (item) => li(item))), ) filter = 'an' update()
استخدم async/await العادي، واستدعِ update() في كل مرة تتغير فيها الحالة. بالنسبة للجلب، فهذا يعني مرة واحدة قبل await ومرة واحدة بعد:
let status: 'idle' | 'loading' | 'done' | 'error' = 'idle' let result: unknown = null async function loadData() { status = 'loading' update() try { result = await fetch('/api/data').then((r) => r.json()) status = 'done' } catch { status = 'error' } update() }
أفضل الممارسات
#- جمّع التغييرات. قم بتغيير كل الحالة التي تحتاجها، ثم استدعِ
update()مرة واحدة. - حافظ على القيم الديناميكية سريعة ونقية. تعمل جميعها على كل
update(). - استخدم
scope()لإجراء التحديثات الصغيرة والمتكررة، مثل الكتابة أو المؤقتات. - صيّر داخل المستند. العقد غير الموجودة في المستند عند تشغيل
update()تتوقف عن التحديث. - استخدم
when()وlist()للعناصر الفرعية التي يتغيرون، وليس دالة تقوم بإرجاع العقد. - استخدم
className، وليسclass، واستخدم() => cx(...)أو{ className: () => … }للفئات الديناميكية. - عرّف التنسيقات مرة واحدة ، على مستوى الوحدة النمطية.
- التصيير على الخادم: صيّر أولًا، ثم ضع ناتج
getCssText()في<style id="nuclo-styles">. مرّر بيانات الطلب كوسائط، واحتفظ بتعليقات HTML في المخرجات.
فهرس API
#كل دالة عامة ومن أين تأتي.
nuclo (أسماء عامة وتصديرات مسماة)
update(...scopeIds): تشغيل كافة القيم الديناميكية، أو تلك الموجودة في النطاقات المحددة.scope(...ids): قم بإعطاء معرفات نطاق العنصر لـupdate(id).when(condition, ...content): المحتوى الشرطي، مع.when()و.else().list(items, renderItem): صف واحد لكل عنصر، تُطابق صفوفه بالهوية.region(options)وinto(id, ...content): مكان مسمى في الشجرة، والمحتوى الذي يملأه من أي مكان.on(type, listener, options?): مستمع الأحداث، أوon('mount')/on('destroy').render(App, parent?): البناء والإلحاق.hydrate(App, parent?): تفعيل HTML المصيَّر على الخادم.forceUpdate(): استدعاء المكونات مرة أخرى وتعديل DOM.css(name?, style): الفئات التي تم إنشاؤها من كائن النمط.cx(...inputs): الجمع بين الأنماط، يفوز لاحقًا.createCss(theme): نسخ مرتبطة بسمة منcss،cx،variants،keyframesوglobalStyle.variants(config): الأنماط من الخيارات.keyframes(frames)وglobalStyle(selector, style).
nuclo (تصديرات مسماة فقط)
nuclo/ssr
renderToString(input)،renderManyToString(inputs)،renderToStringWithContainer(input, tag?, attrs?)وgetCssText().
nuclo/polyfill
- استيراد ذو أثر جانبي ، بالإضافة إلى
document،Event،CustomEvent،Node،Element،HTMLElementوالفئاتNuclo*.
nuclo-router (حزمة مستقلة)
createRouter(table, options?): الموجّه، معstart()،match()،outlet()،matches()،go()،push()،href()وstop().withLoad(loader, load): مسار يبدأ تحميل بياناته بالتوازي مع استيراد جزء الكود الخاص به.- الأنواع:
RouteContext،PageProps،Params،DataLoader،RouteTableوالمزيد.
أسماء عامة فقط
- 175 دوال بناء الوسوم :
div،span، …،var_، وsvgSvg،pathSvg، …. - الأنواع:
NodeModFn،NodeModLike،ExpandedElementوالمزيد. يمكن أيضًا استيراد أنواع التنسيق مثلStyleResultمن'nuclo'.
مفاتيح خاصة
- السمات: معالجات
classNameوstyleوon*وonMountوonDestroy. - كائنات التنسيق: الاختصارات والحالات الزائفة ومحددات
&وقواعد@وraw.
الأسئلة الشائعة
#هل يؤدي تغيير الحالة إلى تحديث الواجهة؟
لا. يظل DOM كما هو حتى تقوم باستدعاء update().
هل يستخدم Nuclo إشارات أو بروكسيات أو DOM افتراضيًا؟
لا. تظل الحالة كائنات ومتغيرات JavaScript عادية. عندما يتم عرض شجرة، يقوم Nuclo بإنشاء عناصر حقيقية وعقد نصية. يقوم update() بتشغيل كل قيمة ديناميكية مسجلة، أو تلك الموجودة داخل النطاقات المحددة، ويقوم بتعديل نفس العقد التي تغيرت فيها القيمة. لا يوجد رسم بياني للتبعية ولا توجد شجرة افتراضية.
لماذا تعرض صفحتي "false"؟
يتم عرض القيم المنطقية كنص. يعرض div(ok && span('Done')) "false". استخدم when(() => ok, span('Done')) للعناصر الفرعية، وok ? 'Done' : '' للنص.
لماذا لا يتغير صف القائمة عندما أعدّل العنصر؟
يتم الاحتفاظ بصفوف العناصر التي لا تزال موجودة في القائمة، ولا يتم عرضها مرة أخرى. استخدم القيم الديناميكية مثل () => todo.text للحقول التي تتغير. انظر list().
لماذا يتوقف جزء من واجهتي عن التحديث؟
غالبًا لم تكن العقد موجودة في المستند عند تشغيل update(). يتوقف Nuclo عن تتبّعها حتى بعد إرفاقها لاحقًا، لذا صيّر داخل حاوية موجودة في المستند. وهناك سببان آخران: يتوقف when() عن التحديث إذا رمى شرطه استثناءً، وتبقى دالة النص فارغة إذا رمت استثناءً عند تشغيلها لأول مرة. راجع update().
متى أستخدم forceUpdate()؟
فقط للتغييرات النادرة للقيم الثابتة، مثل تبديل اللغة. لكل شيء آخر، استخدم update(). انظر forceUpdate().
كيف أضع صفحة في تخطيط دون تمريرها كخاصية؟
حدد المكان بـ region({ id: 'main' }) واطلب من الصفحة إرجاع into('main', …). انظر region() & into().
كيف ألغي تركيب تطبيق؟
غلّفه في when() واجعل الشرط غير متحقق. إزالة DOM يدويًا لا تشغّل onDestroy. راجع دورة الحياة.
هل يتضمن Nuclo موجّهًا؟
ليس في الحزمة الأساسية. يتوفر التوجيه بشكل منفصل كـ nuclo-router، مع عرض الخادم والترطيب ومسارات تقسيم التعليمات البرمجية والتحميل المسبق في وضع الخمول. قم بتثبيته باستخدام npm install nuclo nuclo-router. انظر nuclo-router.
هل يتضمن Nuclo تأثيرات؟
لا. onMount وonDestroy يعملان مرة واحدة لكل عنصر، ويرتبطان بالإدراج والإزالة. لا شيء يعمل مرة أخرى لأن بعض الحالات تغيرت.