توثيق 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')!)
النموذج الذهني
- قم ببناء UI باستخدام منشئي العلامات.
- قم بتمرير دالة لكل قيمة تعتمد على الحالة.
- قم بتغيير الحالة باستخدام 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. راجع عرض Server.nuclo/polyfill: DOM صغير للعقدة والكعكة. انظر 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') بإرجاع builder: دالة تقوم بإنشاء العنصر لاحقًا. يستدعيه 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: يتم تقديمه كنص "صحيح" أو "false". استخدمwhen()للشروط.- A كائن عادي : السمات. راجع Attributes.
- A
css()أوcx()أوvariants(): تضيف فئاتها. on(...): مستمع الأحداث أو خطاف دورة الحياة.- منشئ آخر،
when()أوlist(): طفل. - A 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، ولا يقوم عرض الخادم بإخراجه. تجنب ذلك.
أصناف 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). الدالة التي تعلن عن معلمة هي معدل مخصص ويتم تشغيلها مرة واحدة فقط. يبلغ طول (x = 0) => … و(...args) => … 0، لذا فهما ديناميكيان.
النص الديناميكي
- يتم عرض سلسلة أو رقم أو قيمة كبيرة أو منطقية على أنها
String(value). يعرض() => ok && 'Saved'"false"، لذا اكتب() => (ok ? 'Saved' : ''). nullوundefinedكنص فارغ.- نتيجة
css()أوcx()عند التشغيل الأول تجعل الدالة فئة ديناميكية، وليس نصًا. انظرcx(). - العقدة أو المنشئ أو المصفوفة أو الكائن العادي لا يعرض شيئًا ولا يتم تحديثه أبدًا.
السمات الديناميكية
- Nuclo يكتب قيمة فقط عندما تختلف عن القيمة الأخيرة التي كتبها Nuclo. ولا تتم مقارنتها بـ DOM المباشر، لذلك إذا قام المستخدم بالكتابة في أحد المدخلات وأرجعت وظيفتك نفس القيمة كما كانت من قبل، فسيظل النص الذي كتبه موجودًا.
- نتيجة
nullأوundefinedلا تكتب شيئًا، لذا تبقى القيمة القديمة.classNameهو الاستثناء: أي نتيجة خاطئة تزيل الفئات الديناميكية وتحتفظ بالفئات الثابتة. - تقوم الدالة
styleبتطبيق الكائن الذي يتم إرجاعه في كل تحديث.
الأخطاء
إذا ظهرت قيمة ديناميكية، فسيكتشف Nuclo الخطأ وسيستمر تشغيل بقية التحديث. يصبح النص فارغًا أو يحتفظ بقيمته القديمة، وتحتفظ السمة بقيمتها القديمة. كلاهما يتعافى في update() التالي الذي ينجح. الاستثناء: دالة النص التي يتم طرحها عند تشغيل first تظل فارغة إلى الأبد.
يقوم بتشغيل تمريرة متزامنة واحدة عبر كل قيمة ديناميكية في الصفحة. باستخدام المعرفات، فإنه يقوم فقط بتحديث ما هو موجود داخل العناصر المميزة بمعرفات scope(). يقوم كل تمرير بتنفيذ هذه الخطوات بالترتيب:
list(): يقرأ العناصر مرة أخرى ويضيف أو ينقل أو يزيل الصفوف.when(): التحقق من الشروط مرة أخرى. تتم إعادة بناء الفرع فقط عندما يصبح فرع مختلف نشطًا.- السمات الديناميكية، بما في ذلك
styleوclassName. - نص ديناميكي.
onMountللعناصر التي تم إدراج الخطوتين 1 و2 فيها.
- إنه يقوم بتشغيل كل شيء. يتم تشغيل كل قيمة ديناميكية في كل مكالمة، حتى عندما لا تتغير بياناتها. يتم تخطي الكتابة DOM فقط. حافظ على القيم الديناميكية سريعة وخالية من الآثار الجانبية.
- العقد فقط في الوثيقة. إذا لم تكن العقدة موجودة في الوثيقة عند تشغيل
update()، فسيتوقف Nuclo عن تتبعها. ولا يتم تحديثه مرة أخرى أبدًا، حتى بعد إرفاقه. وهذا ينطبق علىlist()وwhen()أيضًا. تقديم في حاوية موجودة في المستند. - القيم الثابتة لا تتغير أبدًا. يتم إصلاح السلاسل البسيطة وقيم السمات عند إنشاء العنصر. لإعادة بنائها، استخدم
forceUpdate(). - Errors. يتم اكتشاف أخطاء النص والسمات، ويستمر المرور. إذا ظهرت حالة
when()، فسيتم تسجيل الخطأ ويتوقفwhen()عن التحديث. في حالة ظهور دالةlist()itemsأوrenderItem، يفلت الخطأ منupdate()ولا يتم تشغيل الخطوات المتبقية. - Server.
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')لا يلمس الجزء الخارجي. - معرف عدم وجود أي عنصر في الوثيقة يحتوي على تحديثات nothing ولا يسبب أي خطأ، لذا قم بالتحقق من التهجئة.
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()الذي لا يتطابق معرفه مع أي منطقة لا يعرض شيئًا ولا يسبب أي خطأ، لذا تحقق من التهجئة.- المعرفات فريدة في الشجرة. تقوم المنطقة الثانية التي تم إنشاؤها تحت معرف مباشر بتسجيل تحذير.
- تعمل المناطق مع عرض server. في
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 مثل 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()لا يؤدي إلى أي شيء. - يظل كل نمط مسجلاً طوال عمر الصفحة أو عملية الخادم. بالنسبة للقيم التي تتغير لكل عنصر، مثل العرض من البيانات، استخدم السمة
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
المفتاح الذي يبدأ بـ & هو محدد، وكل & يصبح الفئة. المفتاح الذي يبدأ بـ @ هو القاعدة الأساسية: @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' } })
يتم تحويل أي مفتاح مسطح آخر إلى علبة كباب ويتم كتابته كما هو، لذا فإن الخطأ المطبعي مثل 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')
- يتم تخطي المدخلات الزائفة، ويتم تسوية المصفوفات، ويتم تقسيم السلاسل وإلغاء تكرارها.
- يتم دمج الفئات التي تم إنشاؤها لنفس الكتلة (القاعدة، نفس الزائفة، نفس استعلام الوسائط) في فئة واحدة جديدة. تمر أسماء الفئات الأخرى دون تغيير.
- فقط
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">. يعيد العميل استخدام هذا العنصر ويتخطى القواعد الموجودة فيه بالفعل. باستخدام معرف آخر، أو لا شيء، يضيف العميل ورقة أنماط ثانية مع كل قاعدة مرة أخرى. - لم يتم هروب النص، لذلك لا تقم أبدًا بإدخال مدخلات غير موثوقة في قيم النمط.
resetStyles() هو مساعد اختبار. فهو يزيل كل قاعدة وعنصر #nuclo-styles. تحتفظ الأنماط التي قمت بإنشائها سابقًا بأسماء فئاتها ولكنها تفقد CSS الخاصة بها. لا تسميها في التطبيق أو على الخادم.
import { resetStyles } from 'nuclo' beforeEach(() => resetStyles())
nuclo-router ليست جزءًا من الحزمة الأساسية Nuclo. قم بتثبيته بجوار nuclo. إنه إصدار معاينة، لذلك قد يتغير API الخاص به.لـ Nuclo: عرض الخادم، والترطيب، ومسارات تقسيم التعليمات البرمجية والتحميل المسبق الخامل، المبني على list() وupdate(). حول 4.9 KB gzipped، مع عدم وجود تبعيات تتجاوز Nuclo نفسها ولا توجد آثار جانبية، لذلك يتم هزها بعيدًا عند عدم استخدامها.
$ npm install nuclo nuclo-router
أو استخدم pnpm add أو yarn add أو bun add بنفس الاسمين. يحتاج إلى nuclo 0.3 أو أحدث باعتباره تبعية نظير.
يقرر الموجّه تحميل what. يقول التطبيق where مع 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 شيئًا. ينتقل ../ إلى أعلى المقطع ويتوقف عند الجذر. يتم تنفيذ الاستعلام والتجزئة، وإعادة إضافة base، والمسار المطلق الذي تم حله هو ما يدخل في السجل. هذا هو نفس الانحراف عن دقة عنوان URL الذي يقوم به React Router لـ <Link to="./x">.
- فقط
./و../مرتبطان بالمسار. يتم تركpreviewالعاري في href لقواعد المتصفح العادية. - أنشئ روابط نسبية باستخدام
router.href('./preview')بدلاً من كتابةhref="./preview"يدويًا. يتم حلhref()في وقت العرض، وبالتالي فإن العلامات التي يتم شحنها تكون مطلقة ولا تزال صحيحة للزاحف، وللنقرة الوسطى مع تعطيل JavaScript.
ما يُحتفَظ به وما يُعاد بناؤه
| العقدة | عند التنقل |
|---|---|
| a الأصل الذي لم يتغير نمطه ومساره | kept: نفس العقدة، وload() الخاص بها ليس كذلك إعادة تشغيل |
| الصفحة المطابقة | kept عندما لا يتغير نمطها ومسارها ولا تحتوي على load()، لذلك يحتفظ بها الاستعلام أو تغيير التجزئة. يتم تحديث المسار المزود بالمحمل في كل زيارة. |
| الوالد الذي تغير مساره | أعيد بناؤه، بالإضافة إلى كل شيء تحته |
| a الطبقة المدفوعة | تعرض الصفحة المطابقة وحدها. تم تثبيت أصولها بالفعل في الصف الموجود أسفلها. |
تحافظ الصفحة المحفوظة على 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 لكلا الجزأين، حيث تم إنشاء كليهما بنفس الدالة.
ما يحدث عند التنقل
- A الصفحة المحفوظة تحافظ على مناطقها. استعلام أو تغيير تجزئة يحافظ على الصفحة لا يمس محتوى المنطقة أيضًا. اقرأ
router.searchداخل قيمة ديناميكية إذا كان الشريط الجانبي يعتمد عليها. - تقوم الصفحة المعاد بناؤها A بإعادة ملئها. عندما يتغير المسار، تتم إزالة الصف القديم وتتولى طرق عرض
into()للصفحة الجديدة. لم يبق شيء وراءهم. - A عرضًا. الطبقة عبارة عن صف لاحق من المنفذ، لذا فإن
into()الخاص بها هو عرض أحدث لنفس المنطقة. باستخدامtype: 'latest'الافتراضي، فإنه يغطي عرض الصفحة حتى تغلق الطبقة، ثم يظهر عرض الصفحة مرة أخرى. معtype: 'stack'يظهر كلاهما. - A يمكن للصفحة الرئيسية أن تملأ المناطق أيضًا. تظل عروضها بينما تتغير فروعها، لأنه لم تتم إعادة بناء الصفحة الرئيسية.
- يعمل عرض Server كما هو. يقوم الخادم بإنشاء الصفحة داخل
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() - راجع الموجّهات المتعددة & Combined Loading.
قد تقوم وحدة الصفحة بتصدير دالة 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 صحيح والصفحة التي كنت عليها تظل مرتفعة |
| it يرمي أو يرفض | ويهبط على router.error. لا يزال go() يرفض أبدًا، ويمكن إعادة محاولة التنقل. |
أثناء start() | يخرج الرفض من start(). لا يوجد طريق بعد لوضعه. |
| a الطبقة المدفوعة | تدير المُحمل الخاص بها، مع بياناتها الخاصة |
| وحدة التحميل المسبق الخاملة | الوحدات النمطية الدافئة فقط. ليس له أي سياق لتمريره، وسيقوم بجلب البيانات لمسار قد لا تفتحه أبدًا. |
| إعادة التنقل إلى نفس 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 | a التنقل في انتظار وحدة نمطية أو محمل |
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() الذي فتحها. عدم وجود حجة يعني رفضها. عدم العمل على العمق 0. |
DataLoader وwithLoad()
DataLoader هو (ctx: RouteContext) => TData | Promise<TData>، وهو تصدير اختياري لوحدة الصفحة load. يقوم withLoad(loader, load) بإنشاء مسار يبدأ load بجانب استيراد القطعة الخاصة به بدلاً من أن يبدأ بعده. راجع لوادر المسار .
الأنواع
كل نوع هو تصدير مسمى لـ nuclo-router، لذلك يمكن كتابة صفحة أو مُحمل أو حارس دون الاستيراد من المسار.
| النوع | ما هو |
|---|---|
Params<'/blog/:slug'> | كائن المعلمات للنمط. RouteParams هو الشكل غير المكتوب، وMatch هي نتيجة المطابق. |
PageComponent | a الصفحة: (ctx, { layer, data, outlet, beforeLeave }) => … |
RouteModule | a وحدة الصفحة: المكون نفسه، أو { 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()الخاص بها على أنه مرفوض، تمامًا مثل الإلغاء. يعد Back وclose()مسارًا برمجيًا واحدًا: يسجلclose()نتائجه ويعود بالتاريخ، والعمق فيhistory.stateهو المرجع. تحتفظ المتصفحات بحوالي 50 إدخالًا في علامة تبويب، لذلك بعد السير لفترة طويلة عبر الطبقات، قد يختفي الإدخال الموجود أسفلها: ثم يغلقclose()الطبقات الموجودة في مكانها ويعيد كتابة الإدخال الحالي إلى الصفحة التي تركت تظهر. - التنقل العادي يمسح المكدس. النقر على الرابط أو
go()ليس طبقة. فهو يستبدل كل شيء ويستبعد كل طبقة مفتوحة، لذلك لا يبقى أي متصل في انتظاره. - A التحميل البارد عبارة عن صفحة مستقلة. يقوم عنوان URL بتسمية الطبقة العليا ولكن ليس المجموعة الموجودة أسفلها، لذا فإن فتح عنوان URL الخاص بالطبقة يعرضها مباشرة على العمق 1 باستخدام
layer.depth === 0وclose()الخامل. يجب أن يكون للطريق المدفوع معنى في حد ذاته. فرع علىlayer.depthلتقديم ارتباط بدلاً من زر الإلغاء الميت. - لا يمكن للوعد أن ينجو من إعادة التحميل ، لأنه يعيش في حالة إغلاق. تعامل مع النتيجة باعتبارها طريقًا مختصرًا عبر التدفق الذي يعمل أيضًا على طول الطريق.
- لم يبق شيء في الانتظار.
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. - A view هو
(params, ctx) => content، حيث يتم كتابةparamsمن النمط الخاص به وctxهوRouteContextمعpatternوparamsللعرض. وقد يُرجع بدلاً من ذلكimport()لوحدة الصفحة، والتي يتم استدعاؤها باستخدامctxبمجرد وصولها. - لا يوجد شيء مطابق، لا شيء يتم عرضه. أضف طريقة عرض
'*'للرجوع. إنه فارغ قبلstart()وبعدstop()، مثل المنفذ. - يتم إعادة العرض فقط عندما يتغير المسار ، لذلك يحتفظ العرض بـ DOM عبر أي شيء يحتفظ بالصفحة.
- A لا يعرض العرض البطيء أي شيء حتى وصول الوحدة النمطية الخاصة به. تم تسجيل
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() هو ما يعلق المستمعين، ويتعارض جهازا التوجيه. التجارة هي أن الطفل يمتلك الدقة، لذا فإن أدوات تحميل المسار، التي يقوم الموجّه بتشغيلها، هي ملكك للاتصال بها. يقوم هذا الشكل أيضًا بإعادة بناء الصدفة عندما يتغير الذيل، لأن الموجّه يرى مسارًا واحدًا ويقوم الصدفة بحل الباقي بنفسه. الجدول المتداخل هو ما يجعلك أحد الوالدين الذي يحافظ على الموجّه مثبتًا، لذا تفضله ما لم تكن الميزة تحتاج حقًا إلى الموجّه الخاص بها والقاعدة والاحتياطي.
يمكن أن يحتوي التطبيق على عدد من الموجّهات بقدر ما يحتوي على مناطق. القاعدة التي تبقي هذا الأمر بسيطًا: ابدأ واحدًا بالضبط لكل مستند، واترك كل جهاز توجيه آخر يظل غير قيد التشغيل، وحل عناوين 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()الخاص بالعرض التوضيحي. إذا كنت تريد أدوات التحميل، فإن جدول المتداخل أو منطقة تملأ الصفحة تحافظ على هذه المهمة مع الموجّه الذي تم تشغيله. - Guard ردود قديمة. يجب ألا يصل جلب الشريط الجانبي البطيء إلى القسم الذي انتقلت بعيدًا عنه. قم بزيادة عداد الإنشاء لكل تحديث وإسقاط الردود التي لم تعد متطابقة، بالطريقة التي يتقاعد بها الموجّه نفسه من التنقلات المتجاوزة.
- A عرض
matches()البطيء لا يحتوي علىpendingأيضًا. فهو لا يعرض شيئًا حتى تصل الوحدة الخاصة به. ثلاثة أنظمة تحميل، وثلاث ساعات مستقلة:pendingللمسار، والأعلام الخاصة بك، والمنظر البطيء الذي لم يتم الوصول إليه بعد.
معالجة الروابط
#يتنقل المستمع المفوض بالنقر بزر الماوس الأيسر على <a href>، بما في ذلك SVG <a> وينقر على أي شيء متداخل داخل واحد، ويتنحى جانبًا لكل شيء آخر: نقرات التعديل والنقرات الوسطى، target بخلاف _self، download، rel="external"، و hrefs ذات الأصل المشترك، وروابط التجزئة فقط في الصفحة الحالية، والمسارات خارج 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 أبدًا. يؤدي التنقل بعيدًا إلى تحرير الصفحة السابقة. يقوم
stop()بتحرير المستمعين وأي حمل أثناء الرحلة، ويمسحpending، وبالتالي فإن المسار المتقاعد لا يؤدي أبدًا إلى تشابك القرص الدوار. الوحدة النمطية التي يتم حلها بعد إسقاطstop()بصمت. - Concurrency-safe على الخادم. تقوم
start()بإرجاع حالة كل طلب وتقومrun()بنطاقها على عرض متزامن واحد، لذا لا يمكن للطلبات المتداخلة رؤية مسار بعضها البعض. تتم مشاركة ذاكرة التخزين المؤقت للوحدة فقط. ينتهي النطاق عندما ترجع الدالةrun().renderToString()متزامن، وهذا يكفي. سيحتاج العرض غير المتزامن أو المتدفق إلى المسار المنقول عبرawait، وهو ما لا يفعلهrun(). - استعادة التمرير على
popstateهي عملية للمتصفح (scrollRestoration)، وليست لالموجّه. - قم بتشغيل جهاز توجيه واحد لكل مستند. يضيف كل
start()مستمعclickالمفوض الخاص به، ولا يقومstart()الخاص بالموجّه الثاني بسحب المستمع الأول. فقطstart()ثانٍ على نفس الموجّه يفعل ذلك. لحل عنوان URL مقابل جدول آخر، قم بإنشاء جهاز توجيه واتصل بـmatch()فقط. لا يعلق شيئا. يمكن لمنطقة مستقلة تشغيل التحميل الخاص بها بجانبpendingللمسار بهذه الطريقة. راجع الموجّهات المتعددة & التحميل المشترك. - غير مضمن، حسب التصميم: تجزئة وسجل الذاكرة، نظرًا لأن المسار موجود في اسم المسار ويجب أن يخدم الخادم كل مسار؛ منطقتان موجهتان بشكل مستقل يتم تشغيلهما بواسطة عنوان URL واحد، نظرًا لأن الصفحة يمكن أن تملأ عدة مناطق بـ
into()ولكنها تنتمي جميعها إلى المسار المطابق الواحد - لا يزال بإمكان المنطقة المستقلة الاحتفاظ بالموجّه الخاص بها والتحميل غير المبدئي، كما هو الحال في الموجّهات المتعددة & Combined Loading؛ والتحويم المسبق، والذي تحصل عليه عن طريق استدعاء أداة تحميل المسار الخاصة من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()لإجراء التحديثات الصغيرة والمتكررة، مثل الكتابة أو أجهزة ضبط الوقت. - Render في الوثيقة. العقد غير الموجودة في الوثيقة عند تشغيل
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
- A استيراد التأثير الجانبي ، بالإضافة إلى
document،Event،CustomEvent،Node،Element،HTMLElementوالفئاتNuclo*.
nuclo-router (حزمة مستقلة)
createRouter(table, options?): الموجّه، معstart()،match()،outlet()،matches()،go()،push()،href()وstop().withLoad(loader, load): مسار يبدأ محمل البيانات الخاص به بجانب القطعة الخاصة به.- Types:
RouteContext،PageProps،Params،DataLoader،RouteTableوالمزيد.
أسماء عامة فقط
- 175 منشئو العلامات :
div،span، …،var_، وsvgSvg،pathSvg، …. - Types:
NodeModFn،NodeModLike،ExpandedElementوالمزيد. يمكن أيضًا استيراد أنواع التصميم مثلStyleResultمن'nuclo'.
مفاتيح خاصة
- السمات: معالجات
classNameوstyleوon*وonMountوonDestroy. - Styleobjects: الاختصارات والحالات الزائفة ومحددات
&وقواعد@و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 يعملان مرة واحدة لكل عنصر، ويرتبطان بالإدراج والإزالة. لا شيء يعمل مرة أخرى لأن بعض الحالات تغيرت.