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