ВведениеОбзор
NUCLO / 12

اللغة

التوثيق

توثيق Nuclo

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

البدء السريعnpm create nuclo@latest
البدء السريع
v0.3.29
الإصدار الحالي
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. قم ببناء UI باستخدام منشئي العلامات.
  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. راجع عرض Server.
  • nuclo/polyfill: DOM صغير للعقدة والكعكة. انظر 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') بإرجاع builder: دالة تقوم بإنشاء العنصر لاحقًا. يستدعيه 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: يتم تقديمه كنص "صحيح" أو "false". استخدم when() للشروط.
  • A كائن عادي : السمات. راجع Attributes.
  • A css() أو cx() أو variants(): تضيف فئاتها.
  • on(...): مستمع الأحداث أو خطاف دورة الحياة.
  • منشئ آخر، when() أو list(): طفل.
  • A DOM Node: ملحق كما هو.
  • (el, index) => …: معدل مخصص. انظر أدناه.

محاذير شائعة

  • div(isAdmin && span('Admin')) يعرض النص "false". اكتب div(when(() => isAdmin, span('Admin'))).
  • ليست عناصر فرعيةًا. انشروها: ul(...names.map((n) => li(n))). بالنسبة للقوائم التي تتغير، استخدم list().
  • يخفي المتغير المحلي الذي يحمل اسم العلامة المنشئ. داخل function Row(label: string)، label(...) غير قابل للاستدعاء. إعادة تسمية السكان المحليين مثل label، title، data، p، a وi.
  • كل عنصر يحتوي على id كمتغير عام. إذا كانت الصفحة تحتوي على <main id="main"> قبل تحميل Nuclo، فإن Nuclo يترك ذلك العمومي في مكانه، لذلك يقوم main(...) بطرح TypeError. استخدم معرفات أخرى.

معدّلات مخصصة

يتم تشغيل الدالة التي تعلن عن معلمة واحدة على الأقل مرة واحدة، بترتيب الوسيطة، أثناء إنشاء العنصر. حجتها الأولى هي العنصر. والثاني هو فهرس داخلي فلا تعتمد عليه. إذا أعادت سلسلة أو رقمًا، فسيصبح ذلك نصًا. تم إلحاق Node. يتم تطبيق كائن كسمات.

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، ولا يقوم عرض الخادم بإخراجه. تجنب ذلك.

أصناف 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). الدالة التي تعلن عن معلمة هي معدل مخصص ويتم تشغيلها مرة واحدة فقط. يبلغ طول (x = 0) => … و(...args) => … 0، لذا فهما ديناميكيان.

النص الديناميكي

  • يتم عرض سلسلة أو رقم أو قيمة كبيرة أو منطقية على أنها String(value). يعرض () => ok && 'Saved' "false"، لذا اكتب () => (ok ? 'Saved' : '').
  • null وundefined كنص فارغ.
  • نتيجة css() أو cx() عند التشغيل الأول تجعل الدالة فئة ديناميكية، وليس نصًا. انظر cx().
  • العقدة أو المنشئ أو المصفوفة أو الكائن العادي لا يعرض شيئًا ولا يتم تحديثه أبدًا.

السمات الديناميكية

  • Nuclo يكتب قيمة فقط عندما تختلف عن القيمة الأخيرة التي كتبها Nuclo. ولا تتم مقارنتها بـ DOM المباشر، لذلك إذا قام المستخدم بالكتابة في أحد المدخلات وأرجعت وظيفتك نفس القيمة كما كانت من قبل، فسيظل النص الذي كتبه موجودًا.
  • نتيجة null أو undefined لا تكتب شيئًا، لذا تبقى القيمة القديمة. className هو الاستثناء: أي نتيجة خاطئة تزيل الفئات الديناميكية وتحتفظ بالفئات الثابتة.
  • تقوم الدالة style بتطبيق الكائن الذي يتم إرجاعه في كل تحديث.

الأخطاء

إذا ظهرت قيمة ديناميكية، فسيكتشف Nuclo الخطأ وسيستمر تشغيل بقية التحديث. يصبح النص فارغًا أو يحتفظ بقيمته القديمة، وتحتفظ السمة بقيمتها القديمة. كلاهما يتعافى في update() التالي الذي ينجح. الاستثناء: دالة النص التي يتم طرحها عند تشغيل first تظل فارغة إلى الأبد.

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().
  • Errors. يتم اكتشاف أخطاء النص والسمات، ويستمر المرور. إذا ظهرت حالة when()، فسيتم تسجيل الخطأ ويتوقف when() عن التحديث. في حالة ظهور دالة list() items أو renderItem، يفلت الخطأ من update() ولا يتم تشغيل الخطوات المتبقية.
  • Server. 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') لا يلمس الجزء الخارجي.
  • معرف عدم وجود أي عنصر في الوثيقة يحتوي على تحديثات nothing ولا يسبب أي خطأ، لذا قم بالتحقق من التهجئة.
  • 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() الذي لا يتطابق معرفه مع أي منطقة لا يعرض شيئًا ولا يسبب أي خطأ، لذا تحقق من التهجئة.
  • المعرفات فريدة في الشجرة. تقوم المنطقة الثانية التي تم إنشاؤها تحت معرف مباشر بتسجيل تحذير.
  • تعمل المناطق مع عرض server. في 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 مثل 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() لا يؤدي إلى أي شيء.
  • يظل كل نمط مسجلاً طوال عمر الصفحة أو عملية الخادم. بالنسبة للقيم التي تتغير لكل عنصر، مثل العرض من البيانات، استخدم السمة 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

المفتاح الذي يبدأ بـ & هو محدد، وكل & يصبح الفئة. المفتاح الذي يبدأ بـ @ هو القاعدة الأساسية: @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' } })

يتم تحويل أي مفتاح مسطح آخر إلى علبة كباب ويتم كتابته كما هو، لذا فإن الخطأ المطبعي مثل 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')
  • يتم تخطي المدخلات الزائفة، ويتم تسوية المصفوفات، ويتم تقسيم السلاسل وإلغاء تكرارها.
  • يتم دمج الفئات التي تم إنشاؤها لنفس الكتلة (القاعدة، نفس الزائفة، نفس استعلام الوسائط) في فئة واحدة جديدة. تمر أسماء الفئات الأخرى دون تغيير.
  • فقط 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">. يعيد العميل استخدام هذا العنصر ويتخطى القواعد الموجودة فيه بالفعل. باستخدام معرف آخر، أو لا شيء، يضيف العميل ورقة أنماط ثانية مع كل قاعدة مرة أخرى.
  • لم يتم هروب النص، لذلك لا تقم أبدًا بإدخال مدخلات غير موثوقة في قيم النمط.

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 KB gzipped، مع عدم وجود تبعيات تتجاوز Nuclo نفسها ولا توجد آثار جانبية، لذلك يتم هزها بعيدًا عند عدم استخدامها.

terminal
$ npm install nuclo nuclo-router

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

يقرر الموجّه تحميل what. يقول التطبيق where مع 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 شيئًا. ينتقل ../ إلى أعلى المقطع ويتوقف عند الجذر. يتم تنفيذ الاستعلام والتجزئة، وإعادة إضافة base، والمسار المطلق الذي تم حله هو ما يدخل في السجل. هذا هو نفس الانحراف عن دقة عنوان URL الذي يقوم به React Router لـ <Link to="./x">.

  • فقط ./ و../ مرتبطان بالمسار. يتم ترك preview العاري في href لقواعد المتصفح العادية.
  • أنشئ روابط نسبية باستخدام router.href('./preview') بدلاً من كتابة href="./preview" يدويًا. يتم حل href() في وقت العرض، وبالتالي فإن العلامات التي يتم شحنها تكون مطلقة ولا تزال صحيحة للزاحف، وللنقرة الوسطى مع تعطيل JavaScript.

ما يُحتفَظ به وما يُعاد بناؤه

العقدةعند التنقل
a الأصل الذي لم يتغير نمطه ومساره kept: نفس العقدة، وload() الخاص بها ليس كذلك إعادة تشغيل
الصفحة المطابقة kept عندما لا يتغير نمطها ومسارها ولا تحتوي على load()، لذلك يحتفظ بها الاستعلام أو تغيير التجزئة. يتم تحديث المسار المزود بالمحمل في كل زيارة.
الوالد الذي تغير مسارهأعيد بناؤه، بالإضافة إلى كل شيء تحته
a الطبقة المدفوعة تعرض الصفحة المطابقة وحدها. تم تثبيت أصولها بالفعل في الصف الموجود أسفلها.

تحافظ الصفحة المحفوظة على ctx التي تم إنشاؤها بها. هذا هو معنى ctx في كل مكان: اقرأ router.* للقيم الحية. الحالة المستندة إلى الاستعلام، مثل المرشحات وترقيم الصفحات وعلامات التبويب، تقرأ router.search داخل قيمة ديناميكية وتكتب باستخدام router.go('?page=2', { replace: true }). تبقى الصفحة وتركيزها ومدخلاتها نصف المكتوبة في مكانها.

الصفحات والمناطق

#

router.outlet() يضع الصفحة في مكان واحد. الصفحة التي تحتاج أيضًا إلى شريط جانبي أو شريط أدوات أو عنوان في مكان آخر في التخطيط، تملأها بـ into(). يحدد التخطيط المكان بـ region()، وتقوم الصفحة بإرجاع into() بجوار المحتوى الخاص بها. لا يتلقى التخطيط أبدًا محتوى الصفحة كدعم، ولا يعيد التنقل بناءه أبدًا.

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 لكلا الجزأين، حيث تم إنشاء كليهما بنفس الدالة.

ما يحدث عند التنقل

  • A الصفحة المحفوظة تحافظ على مناطقها. استعلام أو تغيير تجزئة يحافظ على الصفحة لا يمس محتوى المنطقة أيضًا. اقرأ router.search داخل قيمة ديناميكية إذا كان الشريط الجانبي يعتمد عليها.
  • تقوم الصفحة المعاد بناؤها A بإعادة ملئها. عندما يتغير المسار، تتم إزالة الصف القديم وتتولى طرق عرض into() للصفحة الجديدة. لم يبق شيء وراءهم.
  • A عرضًا. الطبقة عبارة عن صف لاحق من المنفذ، لذا فإن into() الخاص بها هو عرض أحدث لنفس المنطقة. باستخدام type: 'latest' الافتراضي، فإنه يغطي عرض الصفحة حتى تغلق الطبقة، ثم يظهر عرض الصفحة مرة أخرى. مع type: 'stack' يظهر كلاهما.
  • A يمكن للصفحة الرئيسية أن تملأ المناطق أيضًا. تظل عروضها بينما تتغير فروعها، لأنه لم تتم إعادة بناء الصفحة الرئيسية.
  • يعمل عرض Server كما هو. يقوم الخادم بإنشاء الصفحة داخل 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() - راجع الموجّهات المتعددة & Combined Loading.

محمّلات المسارات وتقسيم الكود

#

قد تقوم وحدة الصفحة بتصدير دالة 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 صحيح والصفحة التي كنت عليها تظل مرتفعة
it يرمي أو يرفض ويهبط على router.error. لا يزال go() يرفض أبدًا، ويمكن إعادة محاولة التنقل.
أثناء start()يخرج الرفض من start(). لا يوجد طريق بعد لوضعه.
a الطبقة المدفوعة تدير المُحمل الخاص بها، مع بياناتها الخاصة
وحدة التحميل المسبق الخاملة الوحدات النمطية الدافئة فقط. ليس له أي سياق لتمريره، وسيقوم بجلب البيانات لمسار قد لا تفتحه أبدًا.
إعادة التنقل إلى نفس 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حية.
pendinga التنقل في انتظار وحدة نمطية أو محمل
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() الذي فتحها. عدم وجود حجة يعني رفضها. عدم العمل على العمق 0.

DataLoader وwithLoad()

DataLoader هو (ctx: RouteContext) => TData | Promise<TData>، وهو تصدير اختياري لوحدة الصفحة load. يقوم withLoad(loader, load) بإنشاء مسار يبدأ load بجانب استيراد القطعة الخاصة به بدلاً من أن يبدأ بعده. راجع لوادر المسار .

الأنواع

كل نوع هو تصدير مسمى لـ nuclo-router، لذلك يمكن كتابة صفحة أو مُحمل أو حارس دون الاستيراد من المسار.

النوعما هو
Params<'/blog/:slug'> كائن المعلمات للنمط. RouteParams هو الشكل غير المكتوب، وMatch هي نتيجة المطابق.
PageComponenta الصفحة: (ctx, { layer, data, outlet, beforeLeave }) => …
RouteModulea وحدة الصفحة: المكون نفسه، أو { 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() الخاص بها على أنه مرفوض، تمامًا مثل الإلغاء. يعد Back وclose() مسارًا برمجيًا واحدًا: يسجل close() نتائجه ويعود بالتاريخ، والعمق في history.state هو المرجع. تحتفظ المتصفحات بحوالي 50 إدخالًا في علامة تبويب، لذلك بعد السير لفترة طويلة عبر الطبقات، قد يختفي الإدخال الموجود أسفلها: ثم يغلق close() الطبقات الموجودة في مكانها ويعيد كتابة الإدخال الحالي إلى الصفحة التي تركت تظهر.
  • التنقل العادي يمسح المكدس. النقر على الرابط أو go() ليس طبقة. فهو يستبدل كل شيء ويستبعد كل طبقة مفتوحة، لذلك لا يبقى أي متصل في انتظاره.
  • A التحميل البارد عبارة عن صفحة مستقلة. يقوم عنوان URL بتسمية الطبقة العليا ولكن ليس المجموعة الموجودة أسفلها، لذا فإن فتح عنوان URL الخاص بالطبقة يعرضها مباشرة على العمق 1 باستخدام layer.depth === 0 وclose() الخامل. يجب أن يكون للطريق المدفوع معنى في حد ذاته. فرع على layer.depth لتقديم ارتباط بدلاً من زر الإلغاء الميت.
  • لا يمكن للوعد أن ينجو من إعادة التحميل ، لأنه يعيش في حالة إغلاق. تعامل مع النتيجة باعتبارها طريقًا مختصرًا عبر التدفق الذي يعمل أيضًا على طول الطريق.
  • لم يبق شيء في الانتظار. 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.
  • A view هو (params, ctx) => content، حيث يتم كتابة params من النمط الخاص به وctx هو RouteContext مع pattern وparams للعرض. وقد يُرجع بدلاً من ذلك import() لوحدة الصفحة، والتي يتم استدعاؤها باستخدام ctx بمجرد وصولها.
  • لا يوجد شيء مطابق، لا شيء يتم عرضه. أضف طريقة عرض '*' للرجوع. إنه فارغ قبل start() وبعد stop()، مثل المنفذ.
  • يتم إعادة العرض فقط عندما يتغير المسار ، لذلك يحتفظ العرض بـ DOM عبر أي شيء يحتفظ بالصفحة.
  • A لا يعرض العرض البطيء أي شيء حتى وصول الوحدة النمطية الخاصة به. تم تسجيل 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() هو ما يعلق المستمعين، ويتعارض جهازا التوجيه. التجارة هي أن الطفل يمتلك الدقة، لذا فإن أدوات تحميل المسار، التي يقوم الموجّه بتشغيلها، هي ملكك للاتصال بها. يقوم هذا الشكل أيضًا بإعادة بناء الصدفة عندما يتغير الذيل، لأن الموجّه يرى مسارًا واحدًا ويقوم الصدفة بحل الباقي بنفسه. الجدول المتداخل هو ما يجعلك أحد الوالدين الذي يحافظ على الموجّه مثبتًا، لذا تفضله ما لم تكن الميزة تحتاج حقًا إلى الموجّه الخاص بها والقاعدة والاحتياطي.

موجّهات متعددة وتحميل مشترك

#

يمكن أن يحتوي التطبيق على عدد من الموجّهات بقدر ما يحتوي على مناطق. القاعدة التي تبقي هذا الأمر بسيطًا: ابدأ واحدًا بالضبط لكل مستند، واترك كل جهاز توجيه آخر يظل غير قيد التشغيل، وحل عناوين 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() الخاص بالعرض التوضيحي. إذا كنت تريد أدوات التحميل، فإن جدول المتداخل أو منطقة تملأ الصفحة تحافظ على هذه المهمة مع الموجّه الذي تم تشغيله.
  • Guard ردود قديمة. يجب ألا يصل جلب الشريط الجانبي البطيء إلى القسم الذي انتقلت بعيدًا عنه. قم بزيادة عداد الإنشاء لكل تحديث وإسقاط الردود التي لم تعد متطابقة، بالطريقة التي يتقاعد بها الموجّه نفسه من التنقلات المتجاوزة.
  • A عرض 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() لإجراء التحديثات الصغيرة والمتكررة، مثل الكتابة أو أجهزة ضبط الوقت.
  • Render في الوثيقة. العقد غير الموجودة في الوثيقة عند تشغيل 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): مسار يبدأ محمل البيانات الخاص به بجانب القطعة الخاصة به.
  • Types: RouteContext، PageProps، Params، DataLoader، RouteTable والمزيد.

أسماء عامة فقط

  • 175 منشئو العلامات : div، span، …، var_، وsvgSvg، pathSvg، ….
  • Types: 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 يعملان مرة واحدة لكل عنصر، ويرتبطان بالإدراج والإزالة. لا شيء يعمل مرة أخرى لأن بعض الحالات تغيرت.