EinführungÜberblick
NUCLO / 12

اللغة

التوثيق

توثيق Nuclo

مرجع عملي لتثبيت Nuclo وبناء الواجهات بتحديثات صريحة وتنسيقها باستخدام CSS محدد الأنواع وعرض HTML على الخادم.

البدء السريعnpm create nuclo@latest
البدء السريع
v0.3.30
الإصدار الحالي
175
دوال بناء الوسوم
~18.4 KB
النواة، مضغوطة بـ gzip
صريح
نموذج التشغيل

نظرة عامة

#

Nuclo هي مكتبة DOM صغيرة لـ TypeScript. يمكنك إنشاء الصفحة بدوال عادية مثل div() وbutton()، وحفظ الحالة في متغيرات عادية، واستدعاء update() عندما يجب أن تتغير الصفحة.

Nuclo ليس لديه DOM افتراضي، ولا مصرّف، ولا بروكسيات ولا إشارات.

main.ts
import 'nuclo'

let count = 0

const App = () =>
  div(
    h1(() => `Count: ${count}`),
    button('Add one', { onClick: () => { count++; update() } }),
  )

render(App, document.getElementById('app')!)

النموذج الذهني

  1. ابنِ الواجهة باستخدام دوال بناء الوسوم.
  2. قم بتمرير دالة لكل قيمة تعتمد على الحالة.
  3. قم بتغيير الحالة باستخدام JavaScript العادي.
  4. استدعِ update(). يقوم Nuclo بتشغيل الدوال مرة أخرى ويقوم بتعديل DOM.

تغيير المتغير لا يمس DOM. تتغير الصفحة فقط عند استدعاء update().

البدء السريع

#

ينشئ create-nuclo هيكل مشروع Vite وTypeScript جاهزًا لاستخدام Nuclo:

$npm create nuclo@latest
$bun create nuclo
$pnpm create nuclo

يطلب اسم المشروع. لتخطي المطالبة، قم بتمرير الاسم والأعلام. باستخدام npm، ضع -- قبل الأعلام:

terminal
$ npm create nuclo@latest my-app -- --template basic --yes

ثم قم بتثبيت وبدء تشغيل خادم التطوير:

terminal
$ cd my-app
$ npm install
$ npm run dev

الخيارات

  • [project-name]: المجلد الهدف. الافتراضي هو nuclo-app.
  • -t, --template <name>: القالب المراد نسخه. فقط basic موجود اليوم.
  • -y, --yes: لا تطالب أبدًا، استخدم الإعدادات الافتراضية.
  • -f, --force: اكتب في مجلد غير فارغ.
  • -h, --help: اطبع المساعدة.

التثبيت والاستيراد

#

لإضافة Nuclo إلى مشروع موجود:

terminal
$ 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 بترتيب الاستيراد، لذلك يفشل هذا:

main.ts
import { App } from './app' // app.ts calls div() → ReferenceError: div is not defined
import 'nuclo'

ضع import 'nuclo' أولاً في ملف الإدخال الخاص بك، أو قم باستيراده في كل ملف يستخدم الأسماء في النطاق العام.

الاستيرادات المسماة

تتوفر الدوال المساعدة أيضًا كتصديرات مسماة، وهي الدوال نفسها المسجّلة في النطاق العام. أما getCssText() وresetStyles() فتتوفران كتصديرات مسماة فقط. دوال بناء الوسوم متاحة في النطاق العام فقط، لذا لا يعمل import { div } from 'nuclo'.

main.ts
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 منفصل للاختبارات، فأضف الأنواع بنفسك:

tsconfig.json
{
  "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'.
badge.ts
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(). يُنشئ كل استخدام عنصرًا جديدًا.

card.ts
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)
مباشريستقبل الباني سمات وعناصر فرعية ثابتة ونصًا ديناميكيًا. يغيّر Rename الحالة ثم يستدعي 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 أُلحقت العقدة، وإذا أعادت كائنًا طُبّق كسمات.

example.ts
const field = input((el) => {
  el.autocomplete = 'off'
})

الحصول على العنصر

render() ترجع العنصر الجذر. داخل الشجرة، استخدم on("mount", (el) => …) أو معدّل مخصص. يمكنك أيضًا استدعاء دالة الإنشاء بنفسك لإنشاء عنصر منفصل:

example.ts
const el = div('Hello')() as HTMLDivElement
document.body.append(el)

قم بإرفاقه قبل update() التالي. العقدة غير الموجودة في المستند عند تشغيل update() تتوقف عن التحديث للأبد.

SVG

تقبل دوال بناء SVG الوسائط نفسها. يعيّن Nuclo كل سمة SVG باستخدام setAttribute، لذلك تعمل الأسماء مثل "stroke-width" كما هو مكتوب.

icon.ts
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 خاصة بالمكوّنات. لا يُنشأ شيء حتى تصيّره أو تضعه في شجرة.

counter.ts
function Counter(label: string) {
  let count = 0 // one per call
  return button(() => `${label}: ${count}`, {
    onClick: () => { count++; update() },
  })
}

render(() => div(Counter('A'), Counter('B')))
مباشرالمكوّن دالة عادية. يحتفظ كل استدعاء Counter() بعدّاده الخاص.فتح ↗

تخص الحالة المعرّفة داخل الدالة ذلك المثيل وحده، أما الحالة المعرّفة خارجها فتشترك فيها جميع المثيلات. يعيد forceUpdate() استدعاء المكوّنات، فتُعاد تهيئة الحالة المعرّفة داخلها.

العناصر الفرعية مجرد وسائط:

card.ts
function Card(heading: string, ...children: NodeModLike<'div'>[]) {
  return div({ className: 'card' }, h2(heading), ...children)
}

render(() => Card('Hi', p('one'), p('two')))

السمات

#

قم بتمرير كائن عادي لتعيين السمات. يمكنك تمرير عدة كائنات. لنفس المفتاح، تفوز القيمة الأحدث، باستثناء className وstyle، اللذين يتم دمجهما. يمكن أن تكون القيمة ثابتة، أو دالة بدون معلمات يتم تشغيلها مرة أخرى على كل update().

example.ts
let busy = false

const save = button(
  { type: 'submit', disabled: () => busy, 'aria-busy': () => String(busy), 'data-id': 42 },
  'Save',
)
مباشريمكن أن تكون قيم السمات دوال. يبدّل الزر الثاني busy ويستدعي 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 الديناميكية على الفئات الثابتة، وعندما تقوم بإرجاع "" تبقى الفئات الثابتة فقط.

example.ts
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، أو دالة تُرجع واحدًا.

example.ts
let progress = 30

const bar = div({ style: () => ({ width: `${progress}%`, backgroundColor: 'teal' }) })
  • Nuclo لا يضيف أي وحدات. اكتب "12px"، وليس 12. الخصائص بدون وحدات مثل opacity وzIndex تقبل الأرقام.
  • "" وnull وundefined تمسح الخاصية.
  • يقوم النمط الديناميكي بتعيين المفاتيح التي يُرجعها فقط. المفتاح الذي يتوقف عن العودة يحتفظ بقيمته القديمة، لذا قم بإرجاع "" لمسحه.
  • سلاسل الأنماط ("color: red") والخصائص المخصصة ("--gap") غير مدعومة. استخدم css() بدلاً من ذلك. يقبل الخصائص المخصصة من خلال raw.
fnواجهة API العامة
// DOM events such as "click". El and Ev are inferred from the tag and the event name. function on(type: DOMEventName, listener: (this: El, e: Ev) => unknown, options?: boolean | AddEventListenerOptions): NodeModFn // Custom events function on<K extends string, E extends Event = Event>(type: K, listener: (this: El, e: E) => unknown, options?: boolean | AddEventListenerOptions): NodeModFn // Lifecycle, see Lifecycle function on(type: "mount", listener: MountCallback): NodeModFn function on(type: "destroy", listener: DestroyCallback): NodeModFn

هناك طريقتان للاستماع إلى الأحداث: سمات on* ومعدل on().

سمات on*

في كائن السمة، تعتبر الدالة الموجودة أسفل مفتاح مثل onClick (on متبوعًا بحرف كبير) معالج حدث، وليست قيمة ديناميكية.

search.ts
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 الخاصية الأصلية، بحيث يصبح onClick el.onclick. يوجد معالج واحد لكل حدث: يحل onClick الأحدث محل معالج سابق.
  • onDoubleClick وonDblClick كلاهما يعني dblclick. المفاتيح المكتوبة بأحرف صغيرة مثل onclick تسبب أخطاء في الأنواع.
  • الاسم الذي ليس حدثًا معروفًا يستمع إلى اسمه بالأحرف الصغيرة: onMyEvent يستمع إلى "myevent".
  • لا تُلتقط الاستثناءات التي يرميها المعالج.
مباشرonInput وonKeyDown سمتان عاديتان. اكتب ثم اضغط Escape للمسح.فتح ↗

Type something, then press Escape.

on()

تقوم on() بإرجاع مُعدِّل يستدعي addEventListener على العنصر.

panel.ts
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() واحدة على العديد من العناصر.

بالنسبة لحدث مخصص، قم بتمرير نوع الحدث كنوع عام:

picker.ts
type PickEvent = CustomEvent<{ id: number }>

const picker = ul(on<'pick', PickEvent>('pick', (e) => choose(e.detail.id)))

أيهما تستخدم؟

استخدم سمات on* للحالة المعتادة. استخدم on() عندما تحتاج إلى أكثر من مستمع واحد لحدث ما، أو خيارات المستمع، أو اسم حدث مخصص مثل "my-event".

fnواجهة API العامة
// As attributes { onMount?: (el: El) => void | (() => void), onDestroy?: (el: El) => void } // Or with on() function on(type: "mount", listener: (el: El) => void | (() => void)): NodeModFn function on(type: "destroy", listener: (el: El) => void): NodeModFn

يُنفَّذ onMount مرة واحدة بعد إدراج العنصر، ويُنفَّذ onDestroy مرة واحدة عند إزالة Nuclo للعنصر. اكتبهما كسمات أو باستخدام on(). يعمل الشكلان بالطريقة نفسها، ويُستنتج نوع العنصر من وسمه.

timer.ts
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():

main.ts
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().

counter.ts
let count = 0

const Counter = () =>
  button(() => `Clicked ${count} times`, {
    onClick: () => { count++; update() },
  })

render(Counter)
مباشرلا يغيّر count++ سوى المتغير. يُحدَّث النص عند استدعاء update() التالي، سواء من الزر أو أي شيء آخر في الصفحة.فتح ↗

Clicked 0 times

قم بتغيير الحالة كما تريد، ثم استدعِ update() مرة واحدة:

example.ts
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().

example.ts
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() ناجح. الاستثناء هو دالة النص التي ترمي استثناءً عند تشغيلها لأول مرة: يظل نصها فارغًا دائمًا.

fnواجهة API العامة
function update(...scopeIds: string[]): void

يقوم بتشغيل تمريرة متزامنة واحدة عبر كل قيمة ديناميكية في الصفحة. باستخدام المعرفات، فإنه يقوم فقط بتحديث ما هو موجود داخل العناصر المميزة بمعرفات scope(). يقوم كل تمرير بتنفيذ هذه الخطوات بالترتيب:

  1. list(): يقرأ العناصر مرة أخرى ويضيف أو ينقل أو يزيل الصفوف.
  2. when(): التحقق من الشروط مرة أخرى. تتم إعادة بناء الفرع فقط عندما يصبح فرع مختلف نشطًا.
  3. السمات الديناميكية، بما في ذلك style وclassName.
  4. النص الديناميكي.
  5. onMount للعناصر التي أُدرجت في الخطوتين 1 و2.
  • إنه يقوم بتشغيل كل شيء. يتم تشغيل كل قيمة ديناميكية في كل استدعاء، حتى عندما لا تتغير بياناتها. يتم تخطي الكتابة إلى DOM فقط. حافظ على القيم الديناميكية سريعة وخالية من الآثار الجانبية.
  • العقد الموجودة في المستند فقط. إذا لم تكن العقدة في المستند عند تشغيل update()، يتوقف Nuclo عن تتبّعها وتحديثها نهائيًا، حتى إذا أُرفقت لاحقًا. ينطبق ذلك أيضًا على list() وwhen(). صيّر داخل حاوية موجودة في المستند.
  • القيم الثابتة لا تتغير أبدًا. تُحدَّد السلاسل النصية الثابتة وقيم السمات عند إنشاء العنصر. لإعادة بنائها، استخدم forceUpdate().
  • الأخطاء. تُلتقط أخطاء النص والسمات وتستمر دورة التحديث. إذا رمى شرط when() استثناءً، يُسجَّل الخطأ ويتوقف when() عن التحديث. وإذا رمت دالة items أو renderItem في list() استثناءً، يخرج الخطأ من update() ولا تُنفَّذ الخطوات المتبقية.
  • على الخادم. update() لا يقوم بأي شيء على الخادم.
مباشريشغّل كل استدعاء update() جميع القيم الديناميكية حتى لو لم تتغير بياناتها. تزيد العروض الأخرى في هذه الصفحة العدّاد أيضًا.فتح ↗

This dynamic value has run 1 times.

fnواجهة API العامة
function scope(...ids: string[]): NodeModFn

يمنح العنصر الذي تمرّره إليه معرّف نطاق واحدًا أو أكثر. بعدها يحدّث update('id') ذلك العنصر ومحتواه فقط، بما في ذلك سمات العنصر نفسه.

example.ts
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"
مباشريضيف كلا الزرين عنصرًا ويغيّر اسم المستخدم. لا يحدّث update('cart') إلا اللوحة التابعة لذلك النطاق.فتح ↗
Hi, Ada
Items: 0
  • يمكن أن يحتوي العنصر على عدة معرفات: scope('cart', 'sidebar').
  • update('a', 'b') يحدّث كل شيء داخل a أو b. إذا كانت عدة عناصر تستخدم نفس المعرف، فسيتم تحديثها جميعًا.
  • مع النطاقات المتداخلة، يتضمن update('outer') النطاق الداخلي. update('inner') لا يلمس الجزء الخارجي.
  • إذا لم يحمل أي عنصر في المستند المعرّف المطلوب، فلن يُحدَّث أي شيء ولن يظهر خطأ، لذا تحقّق من كتابة المعرّف.
  • scope() لا يفعل شيئًا على الخادم.
fnواجهة API العامة
function when(condition: boolean | (() => boolean), ...content: NodeModLike[]): WhenBuilder interface WhenBuilder { when(condition: boolean | (() => boolean), ...content: NodeModLike[]): WhenBuilder else(...content: NodeModLike[]): WhenBuilder }

يعرض المحتوى بناءً على شرط ما. يقوم Nuclo بالتحقق من الشروط مرة أخرى عند كل update(). الشرط الحقيقي الأول يفوز. إذا لم يكن أي شيء صحيحًا، فسيتم عرض محتوى .else(). بدون .else()، لن يتم عرض أي شيء.

status.ts
let status: 'loading' | 'error' | 'ready' = 'loading'

const Status = () =>
  div(
    when(() => status === 'loading', p('Loading…'))
      .when(() => status === 'error', p('Something went wrong'))
      .else(p('Ready')),
  )
مباشريُختار أول شرط متحقق، وإلا تُعرض .else(). يُبنى كل فرع جديد من الصفر.فتح ↗

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() مرتين، يُستخدم الاستدعاء الأخير.

fnواجهة API العامة
function list<T>( items: () => readonly T[] | Iterable<T>, renderItem: (item: T, index: number) => ListRenderResult, ): ListModifier

يعرض صفًا واحدًا لكل عنصر. عند كل update()، يستدعي Nuclo items() مرة أخرى ويطابق العناصر الجديدة مع الصفوف الموجودة حسب الهوية (===).

fruits.ts
let fruits = ['Apple', 'Banana']

const Fruits = () => ul(list(() => fruits, (fruit) => li(fruit)))

render(Fruits)

fruits.push('Cherry')
update()

كيفية مطابقة الصفوف

  • العنصر الذي لا يزال موجودًا يحتفظ بصفه. يقوم Nuclo بنقل الصف إذا لزم الأمر، ولا يقوم باستدعاء renderItem مرة أخرى.
  • عنصر جديد يحصل على صف جديد. يفقد العنصر الذي تمت إزالته صفه، ويتم تشغيل خطافات onDestroy الخاصة به.
  • لكل ظهور مكرر للعنصر صف خاص به.
  • يمكنك تغيير المصفوفة الموجودة في مكانها (push، splice، sort) أو إرجاع مصفوفة جديدة.

لا تُستدعى دالة بناء الصف المحفوظ مجددًا، لذا استخدم القيم الديناميكية للحقول التي تتغير:

todos.ts
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 })) يعيد بناء كل صف.

مباشرتُطابق الصفوف بهوية الكائنات. حدّد مربع اختيار في صف ثم اضغط Reverse: ينتقل عنصر <li> نفسه بمربع الاختيار وكل محتوياته.فتح ↗

ما تُرجعه 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.

fnواجهة API العامة
function region(options: RegionOptions): NodeModFn function into(id: string, ...content: NodeModLike[]): NodeModFn function into(regions: Record<string, NodeModLike | NodeModLike[]>): NodeModFn interface RegionOptions { id: string type?: 'latest' | 'stack' // default: 'latest' empty?: NodeModLike }

region() يمثل مكانًا مسمىًا في الشجرة. into() يملأها من أي مكان آخر، وبأي عمق. لا يتلقى التخطيط المحتوى كخاصية، ولا يعرف المكون الذي يبني المحتوى مكان وصوله.

app.ts
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 والتركيز وحالة النموذج كما هي. استخدم هذا لطبقة مثل مربع حوار فوق الصفحة.

layers.ts
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() داخل محتوى طريقة عرض أخرى منطقته الخاصة.

page.ts
const Page = () =>
  into({
    main: article(h1('Eager route')),
    sidebar: [h2('Related'), ul(li('One'), li('Two'))],
  })

فتح حوار من عمق الشجرة

تقع منطقة التراكب في الجذر. أي مكون، بأي عمق، يفتح مربع حوار عن طريق تغليف into() في when(). يؤدي إغلاق when() إلى إزالة مربع الحوار من المنطقة.

dialog.ts
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() هو عرض. تعرض المنطقة المكدسة العناصر بالترتيب، وإزالة عنصر من المصفوفة يؤدي إلى إزالة الإشعار المرتبط به.

toasts.ts
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.

title.ts
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()، أينما كانت المنطقة.

status.ts
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، فينتظر العرض. عند تسجيل الدخول، تُنشأ المنطقة ويظهر العرض. وعند تسجيل الخروج، تُزال المنطقة ويعود العرض إلى الانتظار.

auth.ts
let user: { name: string } | null = null

const App = () =>
  div(
    when(() => user !== null,
      main(region({ id: 'main' })),
    ).else(
      LoginForm(),
    ),
    into('main', Dashboard()),
  )

عرض بمنطقته الخاصة

يمكن أن يتضمن العرض region() تستهدفها عروض أخرى. هنا تتضمن صفحة المحرر منطقة tabs التي تملؤها علامات التبويب. عند إغلاق المحرر، تُزال منطقته معه.

editor.ts
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' }))),
  )

مثال مباشر

region-app.ts
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()),
  )
مباشرتخطيط بمنطقة رئيسية مكدّسة ومنطقة جانبية تُملآن من خارجه. اكتب في Home، وافتح الحوار ثم أغلقه: يبقى النص. يملأ Article المنطقتين باستدعاء into() واحد.فتح ↗

No related links.

fnواجهة API العامة
function forceUpdate(): void function forceUpdate(nodeModFn: NodeModFn | (() => NodeModFn), parent?: Element): ExpandedElement

update() يقوم بتشغيل القيم الديناميكية فقط. تُحدَّد القيم الثابتة، مثل النص الموجود في h1(labels[lang].title)، عند إنشاء الشجرة. يستدعي forceUpdate() مكوناتك مرة أخرى ويعدّل شجرة DOM الفعلية لتطابق النتيجة، مع تضمين القيم الثابتة. استخدمه لإجراء تغييرات نادرة على مستوى التطبيق، مثل تبديل اللغة. استخدم update() لكل شيء آخر.

i18n.ts
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()

#
fnواجهة API العامة
function render(nodeModFn: NodeModFn | (() => NodeModFn), parent?: Element, index?: number): ExpandedElement

يبني شجرتك ويلحق عنصرها الجذري بـ parent. parent الافتراضي هو document.body. يقوم بإرجاع العنصر الجذر.

main.ts
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() يرمي استثناءً أثناء التشغيل. غلّف المكوّن الذي يقبل خصائص بدالة:

main.ts
const Page = (props: { name: string }) => div(h1('Hi ', props.name))

render(() => Page({ name: 'Ana' }), root)

hydrate()

#
fnواجهة API العامة
function hydrate(nodeModFn: NodeModFn | (() => NodeModFn), parent?: Element): ExpandedElement

يفعّل HTML المصيَّر بواسطة renderToString(). فهو يعيد استخدام العناصر الموجودة، ويرفق مستمعي الأحداث، ويربط القيم الديناميكية وكتل when() وقوائم list()، لذلك يعمل update() بعد ذلك. ثم يقوم بتشغيل خطافات onMount. parent الافتراضي هو document.body. يقوم بإرجاع العنصر الجذر.

client.ts
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() عن التحديث.

العرض على الخادم

#
fnواجهة API العامة
// import { ... } from 'nuclo/ssr' function renderToString(input: RenderableInput): string function renderManyToString(inputs: RenderableInput[]): string[] function renderToStringWithContainer(input: RenderableInput, containerTag?: string, containerAttrs?: Record<string, string>): string function getCssText(): string type RenderableInput = NodeModFn | (() => NodeModFn) | Element | Node | null | undefined

صيّر مكوّنًا إلى سلسلة HTML على الخادم. في المتصفح، استدعِ hydrate() بنفس المكون لجعل HTML تفاعليًا. لا تحتوي أوقات تشغيل الخادم على document، لذا قم باستيراد nuclo/polyfill أولاً.

app.ts
let count = 0
const heading = css({ color: 'red' })

export const App = () =>
  main(
    h1(heading, () => `Count: ${count}`),
    button({ onClick: () => { count++; update() } }, '+1'),
  )
server.ts
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>`
client.ts
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) للمكون الذي يأخذ البيانات.

example.ts
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. لا تستخدم مدخلات المستخدم كاسم وسم مطلقًا.

example.ts
renderToStringWithContainer(p('Hi'), 'section', { id: 'main', class: 'page' })
// '<section id="main" class="page"><p><!-- text-0 -->Hi</p></section>'

nuclo/polyfill

#
import 'nuclo/polyfill' // Named exports: // document, Event, CustomEvent, Node, Element, HTMLElement, // NucloDocument, NucloElement, NucloText, NucloNode

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()

#
fnواجهة API العامة
function css(style: Style): StyleResult function css(name: string, style: Style): StyleResult type StyleResult = { className: string }

يحول كائن تنسيق محدد الأنواع إلى فئات CSS التي تم إنشاؤها. قم بتمرير النتيجة إلى أي دالة بناء الوسوم. في المتصفح، تنتقل القواعد إلى عنصر <style id="nuclo-styles"> واحد. على الخادم، يقوم getCssText() بإرجاعها.

card.ts
const card = css({
  p: 16,
  rounded: 8,
  bg: '#fff7ed',
  hover: { bg: '#ffedd5' },
})

const Card = () => div(card, 'Simple card')
  • يقوم css() بإنشاء فئة واحدة لتصريحات CSS الأساسية، بالإضافة إلى فئة واحدة لكل مفتاح زائف ومحدد وكتلة وسائط. البطاقة أعلاه تحصل على فئتين.
  • أسماء الأصناف مشتقة من تجزئة المحتوى. تشترك التنسيقات المتطابقة في الصنف نفسه، وينتج الخادم والعميل الأسماء نفسها.
  • تعريف الأنماط مرة واحدة، على مستوى الوحدة. يقوم css() بالتخزين المؤقت حسب الكائن، لذلك تتم معالجة كائن جديد في كل استدعاء مرة أخرى في كل مرة.
  • تغيير كائن النمط بعد تمريره إلى css() لا يؤدي إلى أي شيء.
  • يظل كل نمط مسجلاً طوال عمر الصفحة أو عملية الخادم. بالنسبة للقيم التي تتغير لكل عنصر، مثل العرض من البيانات، استخدم السمة style.

تنسيقات مسماة

قم بتمرير الاسم أولاً للحصول على اسم فئة قابل للقراءة:

title.ts
const pageTitle = css('page-title', { text: 24, hover: { color: 'red' } })

pageTitle.className // "page-title page-title-<hash>"
  • الفئة الأساسية هي الاسم بالضبط. الكتل الأخرى تحصل على name-<hash>.
  • يجب أن يكون الاسم اسم فئة صالحًا. وإلا فإن Nuclo يحذر ويولد واحدًا.
  • عالمية. إذا كان هناك نمطان مختلفان يستخدمان نفس الاسم، يحذر Nuclo.

استخدام النتيجة

example.ts
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" للبكسل.
example.ts
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، plpadding، وكل منهما الجانب
px / pyالحشو اليسار واليمين / أعلى وأسفل
m، mt، mr، mb، ml، mx، myنفس الشيء بالنسبة للهامش
w، h، minW، maxW، minH، maxHالعرض والارتفاع والحد الأدنى والحد الأقصى
sizeالعرض والارتفاع
bgالخلفية
text / font / weightfont-size / font-family / font-weight
leading / trackingline-height / letter-spacing
align / items / justifytext-align / align-items / justify-content
z / rounded / shadow / selectz-index / border-radius / box-shadow / user-select

تأخذ هذه المفاتيح true:

المفتاحCSS
rowdisplay: flex; flex-direction: row
coldisplay: flex; flex-direction: column
centeralign-items: center; justify-content: center (لا يتم ضبط العرض)
truncateoverflow: 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.

menu.ts
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، لذا قم بتمرير السلاسل:

example.ts
css({ raw: { '--gap': '8px', '-webkit-line-clamp': '2' } })

يتم تحويل أي مفتاح مسطح آخر إلى صيغة kebab-case ويتم كتابته كما هو، لذا فإن الخطأ المطبعي مثل colr: 'red' يصبح إعلانًا يسقطه المتصفح. يتم تجاهل الكائن المتداخل ضمن مفتاح ليس حالة زائفة أو شاشة أو مفتاح & أو @. يُبلغ TypeScript عن كليهما.

fnواجهة API العامة
function cx(...inputs: ClassInput[]): StyleResult type ClassInput = StyleResult | string | false | null | undefined | ClassInput[]

يجمع بين الأنماط. عندما يقوم مدخلان بتعيين نفس الخاصية، يفوز الإدخال الأحدث.

example.ts
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(...) دائمًا:

toggle.ts
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" بدلاً من فئة.

createCss()

#
fnواجهة API العامة
function createCss<const T extends ThemeConfig>(theme?: T): CssInstance<T> // CssInstance<T> = { css, cx, variants, keyframes, globalStyle, theme }

تقوم بإرجاع css وcx وvariants وkeyframes وglobalStyle الخاصة بها والمرتبطة بسمة. يعطي السمة أسماء للقيم: باستخدام السمة أدناه، يكتب borderColor: 'border' #e5e7eb، ويقوم TypeScript بإكمال الأسماء تلقائيًا. يصبح كل إدخال في screens مفتاح نمط لاستعلام الوسائط، مثل md: {…}.

theme.ts
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 },
})
مفتاح السمةمستخدم بواسطة
colorsbg، backgroundColor، color، borderColor وكل جانب، outlineColor، caretColor، textDecorationColor
fontsfont, fontFamily
shadowsshadow, boxShadow
radiirounded, borderRadius
screens مفاتيح النمط الجديدة، مثل md: {…}
  • يتم حل اسم السمة فقط عندما يكون هو القيمة بأكملها. لم يتم حل border: "1px solid primary".
  • تصبح قيمة الشاشة بدون @ @media <value>. يتم استخدام القيمة التي تبدأ بـ @ كما هو مكتوب، على سبيل المثال "@container (min-width: 400px)".
  • مفاتيح الشاشات متاحة فقط في المثيلات التي تنشئها createCss(). لا تتوفر هذه المفاتيح في css() العامة، لكنها تقبل مفاتيح @media.
  • تتشارك كافة المثيلات والمساعدين العموميين في ورقة أنماط واحدة. يتم إخراج قواعد وسائط الشاشة بالترتيب الذي تم به الإعلان عن الشاشات، وليس الترتيب الذي تستخدمها به، لذا قم بإدراج شاشات min-width من الصغيرة إلى الكبيرة. استخدم مثيل createCss() واحد لكل تطبيق، بحيث يكون هذا الترتيب هو الموجود في قالبك.
  • theme على النتيجة هو كائن السمة الذي مررته.

variants()

#
fnواجهة API العامة
function variants(config: { base?: Style variants?: { [group: string]: { [value: string]: Style } } defaultVariants?: { [group: string]: value } compoundVariants?: ({ [group: string]: value } & { css: Style })[] }): (props?: { [group: string]: value }) => StyleResult

ينشئ دالة تقوم بإرجاع نمط لمجموعة من الخيارات، مثل وظيفة الزر وحجمه.

button.ts
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() وglobalStyle()

#
fnواجهة API العامة
function keyframes(frames: KeyframeFrames): string function globalStyle(selector: string, style: FlatStyle): void

keyframes() يسجل رسمًا متحركًا ويعيد اسمه الذي تم إنشاؤه. مفاتيح الإطار هي from وto والنسب المئوية، مثل "50%" أو "0%, 100%". تأخذ الإطارات خصائص مسطحة فقط. الإطارات المتطابقة ترجع نفس الاسم.

fade.ts
const fadeIn = keyframes({
  from: { opacity: 0 },
  to: { opacity: 1 },
})

const panel = css({ animation: `${fadeIn} 200ms ease-out` })

globalStyle() يضيف قاعدة لأي محدد:

reset.ts
globalStyle('*, *::before, *::after', { boxSizing: 'border-box' })
globalStyle('body', { m: 0, font: 'system-ui, sans-serif' })
  • يتم استخدام المحدد تمامًا كما هو مكتوب.
  • تعمل الخصائص المسطحة فقط. يتم تجاهل المفاتيح الزائفة والشاشات ومفاتيح @، لذا لا يمكن لـ globalStyle() كتابة قواعد الوسائط.
  • تتم إضافة نفس القاعدة مرة واحدة. يضيف النمط المختلف لنفس المحدد قاعدة ثانية، وتفوز القاعدة اللاحقة.

getCssText() وresetStyles()

#
fnواجهة API العامة
function getCssText(): string // from 'nuclo/ssr' or 'nuclo' function resetStyles(): void // from 'nuclo'

getCssText() تقوم بإرجاع كل قاعدة أنشأتها css() وcx() وvariants() وkeyframes() وglobalStyle() في هذه العملية: القواعد الأساسية أولاً، ثم @media و@container و كتل @supports. استخدمها للتصيير على الخادم.

  • استدعها بعد renderToString(). يتم تضمين القواعد التي تم إنشاؤها أثناء العرض.
  • يغطي العملية بأكملها، وليس صفحة واحدة أو طلبًا واحدًا، وينمو فقط. وهذا أمر آمن، لأن أسماء الفئات هي تجزئات للمحتوى.
  • ضعه في <style id="nuclo-styles">. يعيد العميل استخدام هذا العنصر ويتخطى القواعد الموجودة فيه بالفعل. باستخدام معرف آخر، أو لا شيء، يضيف العميل ورقة أنماط ثانية مع كل قاعدة مرة أخرى.
  • لا تُحوَّل المحارف الخاصة في النص إلى كيانات HTML، لذلك لا تقم أبدًا بإدخال مدخلات غير موثوقة في قيم النمط.

resetStyles() هو مساعد اختبار. فهو يزيل كل قاعدة وعنصر #nuclo-styles. تحتفظ الأنماط التي قمت بإنشائها سابقًا بأسماء فئاتها ولكنها تفقد CSS الخاصة بها. لا تستدعها في التطبيق أو على الخادم.

setup.test.ts
import { resetStyles } from 'nuclo'

beforeEach(() => resetStyles())

nuclo-router

#
حزمة منفصلة: nuclo-router ليست جزءًا من الحزمة الأساسية Nuclo. قم بتثبيته بجوار nuclo. إنه إصدار معاينة، لذلك قد يتغير API الخاص به.

موجّه لـ Nuclo يدعم التصيير على الخادم والترطيب وتقسيم كود المسارات والتحميل المسبق وقت الخمول، ومبني على list() وupdate(). حجمه نحو 4.9 كيلوبايت بعد ضغط gzip، ولا يعتمد إلا على Nuclo نفسه. وبفضل خلوّه من الآثار الجانبية، تستطيع أدوات البناء استبعاده عند عدم استخدامه.

terminal
$ npm install nuclo nuclo-router

أو استخدم pnpm add أو yarn add أو bun add بنفس الاسمين. يحتاج إلى nuclo 0.3 أو أحدث باعتباره تبعية نظير.

يحدد الموجّه ما يُحمَّل، ويحدد التطبيق موضع عرضه باستخدام router.outlet(): استدعاء list() واحد يعرض مكدس الطبقات، بصف لكل طبقة، فتكون الصفحة العادية صفًا واحدًا. يبني الخادم والعميل الشجرة نفسها، لذا يعيد hydrate() استخدام عقد الخادم. ويعتمد ما تبقّى على history ومستمع click مفوّض واحد وrequestIdleCallback.

استيراد واحد

main.ts
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().

تطبيق كامل

routes.ts
// 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'),
  )
pages/Post.ts
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' : '')))
}
main.ts
import 'nuclo'
import { router, App } from './routes.ts'

await router.start()
hydrate(App, document.getElementById('app')!)   // or render() without SSR
server.ts
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 المكتوب بـ ./ يتعلق بالمسار النشط. يصنعون معًا قسمًا قابلاً لإعادة الاستخدام ويعني نفس الشيء تحت كل والد. قيمة الجدول هي إما محمل أو جدول آخر، مفاتيحه مرتبطة به:

records/routes.ts
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'),
}
routes.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():

records/Record.ts
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()، فلن يظهر مسارها الفرعي.
  • لا توجد مسارات فهرس. '/' هو التخطيط نفسه، لذا فإن المسار الأصلي يعرض التخطيط بمنفذ فارغ بدلاً من صفحة فهرس منفصلة بداخله. ضع المحتوى المقصود في التخطيط، أو أعطه مسارًا فرعيًا حقيقيًا.

تعمل الاستدعاء نفسه من أي من العناصر الأبوية، ومن صفحة لا تعرف الصفحة الموجودة فيها:

example.ts
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() بجوار المحتوى الخاص بها. لا يتلقى التخطيط أبدًا محتوى الصفحة كخاصية، ولا يعيد التنقل بناءه أبدًا.

app.ts
export const App = () =>
  div(
    aside(
      input({ placeholder: 'Filter' }),   // keeps its text across navigations
      region({ id: 'sidebar', empty: p('Nothing to show.') }),
    ),
    main(router.outlet()),
  )
pages/Post.ts
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():

pages/Report.ts
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:

pages/Post.ts
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يتم تشغيل المُحمل مرة أخرى وإعادة بناء الصف. المسار الذي لا يحتوي على محمّل يحتفظ بصفه.

لإعادة التحقق، انتقل إلى المكان الذي تتواجد فيه بالفعل دون إضافة إدخال سجل:

example.ts
await router.go(router.url, { replace: true })   // runs load() again

بدء تحميل البيانات مع جزء الكود

يمكن تشغيل load الذي تم تصديره بواسطة وحدة الصفحة فقط بمجرد وصول هذه الوحدة، لذا فإن التنقل البارد يجلب القطعة، ثم البيانات. لبدء تشغيلهما معًا، قم بتعريف المُحمل في الجدول باستخدام withLoad():

routes.ts
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() لأحد العناصر الأبوية ليست موجودة عليه.

تقسيم الكود والتحميل المسبق

قيمة المسار هي دائمًا دالة تُرجع مكون الصفحة، لذلك يُحمَّل المكوّن عند الحاجة:

routes.ts
'/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 للموجّه

#
fnواجهة API العامة
// import { ... } from 'nuclo-router' function createRouter(table: RouteTable, options?: RouterOptions): Router function withLoad(loader: RouteLoader, load: DataLoader): RouteLoader interface RouterOptions { base?: string; preload?: boolean; idleTimeout?: number; onNavigate?: (ctx: RouteContext, kind: NavigationKind) => void }

createRouter(table, options?)

table يقوم بتعيين نمط إلى محمّل، أو إلى جدول متداخل. الخيارات:

الخيارالقيمة الافتراضية
base'/'URL البادئة التي يتم تقديم التطبيق ضمنها، مثل '/docs'. إنه يتطابق فقط عند حدود المقطع: /docs/intro موجود داخل '/docs'، /docsearch ليس كذلك. عناوين URL الموجودة خارجه لا تتطابق مع أي شيء، لذلك يرفض start() (404 من جانب الخادم الخاص بك) ويتم ترك الروابط إليها في المتصفح.
preloadtrue قم بتحميل وحدات المسارات المتبقية في الخلفية بمجرد أن تكون الصفحة خاملة. false يقوم بإيقاف تشغيله.
idleTimeout1000 ميلي ثانية بدون نقرات أو مفاتيح أو عجلة أو تمرير قبل بدء التحميل المسبق، أو استئنافه بعد أن كان المستخدم مشغول.
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 المطلقة فقط، نظرًا لأن حل ./ تحتاج إلى مسار نشط.

example.ts
// 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 }:

pages/Post.ts
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

حيث توجد الصفحة في المكدس، وطريقها للخروج.

عضو
depth0 للصفحة الأساسية، 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 وتركيزها ونموذجها نصف المملوء كلها لا تزال موجودة عند إغلاق الطبقة.

هذا يجعل تدفق "أحتاج إلى سجل غير موجود بعد" يعمل دون التخلص من النموذج الذي كان المستخدم يملؤه:

pages/Edit.ts
// the page underneath
const created = await layer.push<Option>('/options/new')
if (created) { options.push(created); selected = created.id; update() }
pages/NewOption.ts
// 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 (أو الوعد به) بالصفحة:

pages/Edit.ts
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) شيئًا آخر يعتمد على مكان تواجدك، مثل الشريط الجانبي أو مسارات التنقل أو شريط الأدوات، من جدوله الصغير، دون تحميل مسار أو إضافة صفحة:

app.ts
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() الخاص بها، فيبقى الإطار عند التنقل داخل القسم:

routes.ts
createRouter({ '/': …, '/docs': docsRoutes, '*': … })

أو امنح الميزة موجّه خاصًا بها، تم إنشاؤه ولكن لم يتم تشغيله مطلقًا. يمتلك الوالد مُدخلًا واحدًا شاملاً، ويقوم الطفل بتحليل الذيل مقابل الأنماط والمعاملات والمسار الاحتياطي الخاصة به:

routes.ts
// parent table
'/docs/*rest': () => import('./docs/Shell.ts'),
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 الخاص به:

routes.ts
'/shop': {
  '/':          () => import('./pages/Shop.ts'),    // layout, has a load()
  './:section': () => import('./pages/Section.ts'), // page, has a load()
},
pages/Shop.ts
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()),
  )
}
pages/Section.ts
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 الموجّه الرئيسي الذي بدأ تشغيله فقط، لذا يمكن تحميل المنطقتين بالتوازي، وقد تنتهي أي منهما أولًا:

sidebar.ts
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()
}
routes.ts
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 للمسار، ومؤشرات التحميل الخاصة بك، والعرض المؤجل الذي لم يتم الوصول إليه بعد.

القيم المحسوبة

#

لا يوفر Nuclo واجهة API خاصة بالقيم المحسوبة. استخدم دالة عادية واستدعها من دوال القيم الديناميكية:

example.ts
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 ومرة ​​واحدة بعد:

example.ts
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 (أسماء عامة وتصديرات مسماة)

nuclo (تصديرات مسماة فقط)

nuclo/ssr

  • renderToString(input)، renderManyToString(inputs)، renderToStringWithContainer(input, tag?, attrs?) وgetCssText().

nuclo/polyfill

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'.

مفاتيح خاصة

الأسئلة الشائعة

#

هل يؤدي تغيير الحالة إلى تحديث الواجهة؟

لا. يظل 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 يعملان مرة واحدة لكل عنصر، ويرتبطان بالإدراج والإزالة. لا شيء يعمل مرة أخرى لأن بعض الحالات تغيرت.