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