Документация 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, …). См. Функции построения элементов. - 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()для условий.- Обычный объект: атрибуты. См. Атрибуты.
- Результат
css(),cx()илиvariants(): добавляет свои классы. on(...): обработчик событий или хук жизненного цикла.- Другая функция построения элемента,
when()илиlist(): дочерний узел. - Узел DOM
Node: добавляется как есть. (el, index) => …: пользовательский модификатор. См. ниже.
Типичные ошибки
div(isAdmin && span('Admin'))отображает текст «false». Напишитеdiv(when(() => isAdmin, span('Admin'))).- Массив нельзя передать как дочерний узел. Используйте spread:
ul(...names.map((n) => li(n))). Для списков, которые изменяются, используйтеlist(). - Локальная переменная с именем тега скрывает построитель. Внутри
function Row(label: string)label(...)не может быть вызван. Переименуйте локальные объекты, такие какlabel,title,data,p,aиi. - Браузеры предоставляют каждый элемент, имеющий
id, как глобальную переменную. Если страница содержит<main id="main">до загрузки Nuclo, Nuclo оставляет этот глобальный объект на месте, поэтомуmain(...)выдаетTypeError. Используйте другие идентификаторы.
Пользовательские модификаторы
Функция, объявляющая хотя бы один параметр, выполняется один раз в порядке аргументов во время построения элемента. Ее первый аргумент — элемент. Второй — внутренний индекс, поэтому не полагайтесь на него. Если она возвращает строку или число, оно становится текстом. Добавляется Node. Объект применяется как атрибуты.
const field = input((el) => { el.autocomplete = 'off' })
Получение элемента
render() возвращает корневой элемент. Внутри дерева используйте on("mount", (el) => …) или собственный модификатор. Вы также можете самостоятельно вызвать функцию построения элемента, чтобы создать отдельный элемент:
const el = div('Hello')() as HTMLDivElement document.body.append(el)
Прикрепите его перед следующим update(). Узел, которого нет в документе при запуске update(), перестает обновляться навсегда.
SVG
Функции SVG принимают те же аргументы. Nuclo задает каждый атрибут SVG через setAttribute, поэтому такие имена, как "stroke-width", работают так, как написано.
const Icon = () => svgSvg( { viewBox: '0 0 24 24', width: 24, height: 24, className: 'icon' }, circleSvg({ cx: 12, cy: 12, r: 10, fill: 'currentColor' }), )
Все функции построения элементов
HTML: a abbr address area article aside audio b base bdi bdo blockquote body br button canvas caption cite code col colgroup data datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins kbd label legend li link main map mark menu meta meter nav noscript object ol optgroup option output p picture pre progress q rp rt ruby s samp script search section select slot small source span strong style sub summary sup table tbody td template textarea tfoot th thead time title tr track u ul var_ video wbr
SVG: aSvg animateSvg animateMotionSvg animateTransformSvg circleSvg clipPathSvg defsSvg descSvg ellipseSvg feBlendSvg feColorMatrixSvg feComponentTransferSvg feCompositeSvg feConvolveMatrixSvg feDiffuseLightingSvg feDisplacementMapSvg feDistantLightSvg feDropShadowSvg feFloodSvg feFuncASvg feFuncBSvg feFuncGSvg feFuncRSvg feGaussianBlurSvg feImageSvg feMergeSvg feMergeNodeSvg feMorphologySvg feOffsetSvg fePointLightSvg feSpecularLightingSvg feSpotLightSvg feTileSvg feTurbulenceSvg filterSvg foreignObjectSvg gSvg imageSvg lineSvg linearGradientSvg markerSvg maskSvg metadataSvg mpathSvg pathSvg patternSvg polygonSvg polylineSvg radialGradientSvg rectSvg scriptSvg setSvg stopSvg styleSvg svgSvg switchSvg symbolSvg textSvg textPathSvg titleSvg tspanSvg useSvg viewSvg
Компоненты
#Компонент — это простая функция, возвращающая построитель, например button(...). Отдельного API компонентов нет. Ничто не создается до тех пор, пока вы не отрендерите его или не поместите в дерево.
function Counter(label: string) { let count = 0 // one per call return button(() => `${label}: ${count}`, { onClick: () => { count++; update() }, }) } render(() => div(Counter('A'), Counter('B')))
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()непосредственно функции построения элемента, чтобы TypeScript вывел тип элемента: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является исключением: любой результат, приводимый к false удаляет динамические классы и сохраняет статические. - Функция
styleприменяет возвращаемый объект при каждом обновлении.
Ошибки
Если функция динамического значения выбрасывает исключение, Nuclo улавливает ошибку, и остальная часть обновления продолжает выполняться. Текст становится пустым или сохраняет прежнее значение, а атрибут сохраняет прежнее значение. Оба восстанавливаются при следующем успешном update(). Исключение: текстовая функция, которая выбрасывает исключение при первом запуске, навсегда остается пустой.
update()
#Выполняет один синхронный проход для каждого динамического значения на странице. При использовании идентификаторов обновляется только то, что находится внутри элементов, отмеченных этими идентификаторами scope(). Каждый проход выполняет эти шаги по порядку:
list(): снова считывает элементы и добавляет, перемещает или удаляет строки.when(): снова проверяет условия. Ветка перестраивается только тогда, когда другая ветка становится активной.- Динамические атрибуты, включая
styleиclassName. - Динамический текст.
onMountдля элементов, вставленных на шагах 1 и 2.
- Он запускает все. Каждое динамическое значение выполняется при каждом вызове, даже если его данные не изменились. Пропускается только запись DOM. Функции динамических значений должны работать быстро и не иметь побочных эффектов.
- Только узлы в документе. Если во время запуска
update()узла нет в документе, Nuclo прекращает его отслеживание. Он больше никогда не обновляется, даже после того, как вы его прикрепите. Это относится и кlist()иwhen(). Выполните рендеринг в контейнер, который находится в документе. - Статические значения никогда не меняются. Простые строки и значения атрибутов фиксируются при построении элемента. Чтобы их пересобрать, используйте
forceUpdate(). - Ошибки. Ошибки текста и атрибутов перехватываются, и проход продолжается. Если условие
when()выбрасывает исключение, ошибка регистрируется иwhen()прекращает обновление. Если функцияlist()itemsилиrenderItemвыдает ошибку, ошибка переходит вupdate(), а остальные шаги не выполняются. - На сервере.
update()ничего не делает на сервере.
update() вычисляет все динамические значения, даже если их данные не изменились. Другие демонстрации на странице тоже увеличивают счётчик.Открыть ↗This dynamic value has run 1 times.
scope()
#Предоставляет элементу, которому он передается, один или несколько идентификаторов области. update('id') затем обновляет только то, что находится внутри этого элемента, включая собственные атрибуты элемента.
let cartItems: string[] = [] let user = 'Ada' const App = () => div( header(() => `Hi, ${user}`), aside(scope('cart'), span(() => `Items: ${cartItems.length}`)), ) render(App) cartItems.push('apple') user = 'Grace' update('cart') // "Items: 1", the header still says "Hi, Ada" update() // everything, the header now says "Hi, Grace"
- Элемент может иметь несколько идентификаторов:
scope('cart', 'sidebar'). update('a', 'b')обновляет все внутриaилиb. Если несколько элементов используют один и тот же идентификатор, все они обновляются.- При использовании вложенных областей
update('outer')включает внутреннюю область.update('inner')не касается внешнего. - Идентификатор, которого нет ни у одного элемента в документе, ничего не обновляет и не вызывает ошибок, поэтому проверьте написание.
scope()ничего не делает на сервере.
Отрисовывает содержимое на основе условия. Nuclo снова проверяет условия на каждом update(). Первое истинное условие побеждает. Если ни одно из них не соответствует действительности, отображается содержимое .else(). Без .else() ничего не рендерится.
let status: 'loading' | 'error' | 'ready' = 'loading' const Status = () => div( when(() => status === 'loading', p('Loading…')) .when(() => status === 'error', p('Something went wrong')) .else(p('Ready')), )
else(). Новая ветвь создаётся с нуля.Открыть ↗Loading…
Условия
- Для состояния передайте функцию. Простое логическое значение считывается один раз:
when(open, …)никогда не изменяется. - В TypeScript функция должна возвращать логическое значение. Напишите
() => items.length > 0или() => user != null.
Что обновляется
- Пока одна и та же ветка остается активной, ее элементы сохраняются. Динамические значения, списки и блоки
when()внутри него постоянно обновляются. - Когда другая ветка становится активной, старое содержимое удаляется и запускаются ее хуки
onDestroy. Новая ветка создается с нуля, и ее хукиonMountработают. Состояние старых элементов, таких как вводимый текст или фокус, теряется.
Содержимое ветки
Поместите дочерние элементы в ветку: текст, динамический текст, функции построения элементов, list() и вложенный when(). Передайте функции построения элементов, такие как p('Hi'), а не узлы DOM: узел DOM перестает обновляться после того, как его ветвь скрывается один раз.
Объекты атрибутов и on() в ветке применяются к родительскому элементу и остаются после скрытия ветки. В качестве условного атрибута используйте динамический атрибут: { className: () => (active ? 'active' : '') }.
Цепочка
.when() и .else() возвращают новую сборку и не меняют оригинал. Если вы вызовете .else() дважды, победит последний вызов.
list()
#Отрисовывает одну строку для каждого элемента. В каждом update() Nuclo снова вызывает items() и сопоставляет новые элементы с существующими строками по идентичности (===).
let fruits = ['Apple', 'Banana'] const Fruits = () => ul(list(() => fruits, (fruit) => li(fruit))) render(Fruits) fruits.push('Cherry') update()
Как сопоставляются строки
- Элемент, который все еще существует, сохраняет свою строку. Nuclo перемещает строку, если это необходимо, и больше не вызывает
renderItem. - Новый элемент получает новую строку. Удаленный элемент теряет свою строку, и его хуки
onDestroyвыполняются. - Каждому повторяющемуся элементу присваивается отдельная строка.
- Вы можете изменить массив на месте (
push,splice,sort) или вернуть новый массив.
Сохраненная строка никогда не строится заново, поэтому используйте динамические значения для изменяющихся полей:
const todos = [{ text: 'Write docs', done: false }] const Todos = () => ul( list(() => todos, (todo) => li({ className: () => (todo.done ? 'done' : '') }, () => todo.text), ), ) render(Todos) todos[0].done = true update() // same <li>, its class is now "done"
Копии — это новые элементы, поэтому замена массива на todos.map((t) => ({ ...t })) перестраивает каждую строку.
Что возвращает renderItem
- Возвращает один элемент, например
li(...),tr(...)или построитель SVG. Вернитеnullилиundefined, чтобы пропустить элемент. Строки, текстовые узлы и фрагменты ничего не отображают. - Не возвращайте
when()илиlist()напрямую. Поместите их внутри элемента строки:li(todo.text, when(() => todo.done, ' ✓')). - Чтобы скрыть элементы, фильтруйте по
items(), а не поrenderItem. - Берегите
renderItemот побочных эффектов. Nuclo может вызывать его несколько раз для одного и того же элемента.
Индекс
index — это позиция элемента на момент создания его строки. Он не обновляется при перемещении строк. Для текущей позиции используйте динамическое значение: li(() => `${names.indexOf(name) + 1}. ${name}`).
Итерируемые объекты
Подойдет любой итерируемый объект: массивы, Set, генераторы и map.values(). Возврат одного и того же массива или Set каждый раз — это нормально. Итератор, такой как генератор или map.values(), используется за один проход, поэтому создавайте его внутри items() при каждом вызове. Итерация Map каждый раз напрямую создает новые массивы [key, value], поэтому каждая строка перестраивается. Вместо этого используйте map.values().
Размещение
Строки появляются там, где list() находится среди своих одноуровневых элементов, между двумя маркерами комментариев. Другие дочерние узлы родителя не затрагиваются. list() также работает внутри функций построения SVG-элементов.
region() отмечает именованное место в дереве. into() заполняет его откуда угодно, на любой глубине. Макет не получает контент в качестве параметра, а компонент, создающий контент, не знает, где он находится.
const Layout = () => div( aside(nav(a({ href: '/' }, 'Home'))), main(region({ id: 'main', empty: p('Nothing open.') })), ) // Anywhere in the tree, before or after the region const Article = () => into('main', article(h1('Hello'))) render(() => div(Layout(), Article()))
Как это работает
into()оставляет только комментарий там, где это написано. Его содержимое отображается между маркерами региона.- Порядок не имеет значения.
into(), созданный до того, как его регион, ждет и отображается сразу после создания региона. Представления отображаются в том порядке, в котором они были написаны. - Содержимое существует столько же, сколько его представление. Когда
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, поэтому его двойной вызов добавляет две копии. - К моменту возврата из
render()все хукиonMountв дереве уже выполнены. - Выполняйте рендеринг в элемент, находящийся в документе. См.
update(). - Корнем должен быть элемент, например
div(...), а неwhen()илиlist(). indexявляется внутренним. Он не выбирает, куда вставлять элемент. Не передавайте его.
Компонент, передаваемый как render(App), не должен принимать никаких параметров. Обязательный параметр является ошибкой типа. Необязательный параметр проходит проверку типов, но render() выдает ошибку во время выполнения. Оберните компонент, который принимает параметр:
const Page = (props: { name: string }) => div(h1('Hi ', props.name)) render(() => Page({ name: 'Ana' }), root)
hydrate()
#Подключает интерактивность к 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 | внутренние отступы со всех сторон или с отдельной стороны |
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 (свойство display не задается) |
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. - Все экземпляры и глобальные вспомогательные функции используют одну таблицу стилей. Медиаправила из screens выводятся в том порядке, в котором экраны объявлены, а не в том порядке, в котором вы их используете, поэтому перечисляйте экраны
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 КБ после gzip-сжатия, без каких-либо зависимостей, кроме самого Nuclo, и без побочных эффектов, поэтому, когда он не используется, сборщик удаляет его из неиспользуемого кода (tree shaking).
$ npm install nuclo nuclo-router
Или используйте pnpm add, yarn add или bun add с теми же двумя именами. В качестве peer-зависимости требуется nuclo 0.3 или новее.
Маршрутизатор решает, что загрузить. Приложение указывает, где отобразить результат, через router.outlet(): один list() для стека слоев, одна строка на слой. Обычная страница занимает одну строку. Сервер и клиент строят одинаковое дерево, поэтому hydrate() использует серверные узлы, а не заменяет их. Все остальное — history, один делегированный обработчик click и requestIdleCallback.
Один импорт
export const router = createRouter(routes) // define once, import anywhere await router.start() // resolve the current URL render(App, document.getElementById('app')!) // App places router.outlet() and reads router.path, router.go(), …
start() разрешает модули соответствующего маршрута перед возвратом, поэтому первое дерево является полным как на сервере, так и на клиенте. После этого маршрутизатор считывает запущенный им маршрут: router.path, router.pending, router.go().
Целое приложение
// isomorphic: imported by the browser and the server import { createRouter } from 'nuclo-router' export const router = createRouter({ '/': () => import('./pages/Home.ts'), '/docs': () => import('./pages/Docs.ts'), '/blog/:slug': () => import('./pages/Post.ts'), '*': () => import('./pages/NotFound.ts'), }) export const App = () => div( header(a({ href: router.href('/') }, 'Home'), a({ href: router.href('/docs') }, 'Docs')), main( when(() => router.pending, Spinner()), when(() => router.error !== null, ErrorView()), router.outlet(), // where the pages land ), footer('© nuclo'), )
import type { RouteContext } from 'nuclo-router' import { router } from '../routes.ts' // A page is plain content export default function Post(ctx: RouteContext) { // A query-only change keeps the page, so the query is read live. return article(h1(ctx.params.slug), p(() => (router.search.has('preview') ? 'draft' : ''))) }
import 'nuclo' import { router, App } from './routes.ts' await router.start() hydrate(App, document.getElementById('app')!) // or render() without SSR
import 'nuclo/polyfill' import 'nuclo' import { renderToString } from 'nuclo/ssr' import { router, App } from './routes.ts' // one Route per request const route = await router.start(request.url) const html = route.run(() => renderToString(App))
Вот и вся интеграция. Ссылки работают, потому что <a href> — это просто <a href>. Один делегированный обработчик на document перехватывает ссылки, принадлежащие маршрутизатору, и оставляет все остальные ссылки браузеру.
На сервере start() возвращает Route для каждого запроса и оставляет маршрутизатор в покое, поэтому одновременные запросы не могут видеть маршрут друг друга. route.run() делает этот маршрут маршрутом, который маршрутизатор и, следовательно, outlet считывают на протяжении рендеринга. В браузере существует один маршрут для каждой загрузки страницы, и 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 пуст, поэтому макет может вызывать его без проверки. Если родитель не вызывает
outlet(), его дочерний маршрут не отображается. - Индексных маршрутов нет.
'/'— это сам макет, поэтому родительский путь отображает макет с пустым 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. Завершающий слеш ничего не меняет. ../ поднимается на сегмент выше и останавливается в корне. Строка запроса и хеш сохраняются, base добавляется обратно, а в историю попадает абсолютный путь. Такое же отступление от правил разрешения URL использует React Router для <Link to="./x">.
- Только
./и../являются относительными по маршруту. Путьpreviewбез префикса в href оставлен обычным правилам браузера. - Создавайте относительные ссылки с помощью
router.href('./preview'), а не записывайтеhref="./preview"вручную.href()разрешается во время рендеринга, поэтому поставляемая разметка является абсолютной и по-прежнему корректной для сканера, для щелчка средней кнопкой мыши и с отключенным JavaScript.
Что сохраняется, а что перестраивается
| Узел | Навигация |
|---|---|
| родительская страница, шаблон и путь которого не изменились. | сохраняется: тот же узел, а его load() не запускается повторно |
| соответствующая страница | сохраняется, когда ее шаблон и путь не изменились и у нее нет load(), поэтому запрос или изменение хэша сохраняют ее. Маршрут с загрузчиком обновляется при каждом посещении. |
| родительский элемент, путь которого изменился | пересобран вместе со всем, что находится под ним |
| добавленный слой | отображает только соответствующую страницу. Его родители уже установлены в ряду под ним. |
Сохраненная страница хранит ctx, с которым была создана. Значение ctx всегда остается исходным контекстом: текущие значения читайте из router.*. Фильтры, пагинация и вкладки, зависящие от строки запроса, читают router.search внутри динамического значения и меняют URL через 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() в outlet, а into() существует до тех пор, пока строка, в которой она содержится. Таким образом, содержимое боковой панели поступает вместе со страницей и удаляется вместе с ней: при переходе на страницу без into('sidebar', …) снова отображается содержимое региона empty. Страница может использовать свои ctx и data для обеих частей, поскольку обе они созданы с помощью одной и той же функции.
Что происходит при навигации
- Сохраненная страница сохраняет содержимое регионов. Запрос или изменение хеша, которые сохраняет страницу , также не затрагивают содержимое региона. Прочитайте
router.searchвнутри динамического значения, если от него зависит боковая панель. - Перестроенная страница заполняет регионы заново. При смене маршрута старая строка удаляется, а представления
into()новой страницы занимают ее место. Старое содержимое не остается. - Добавленный слой добавляет представление. Он занимает более позднюю строку outlet, поэтому его
into()становится новым представлением того же региона. При стандартномtype: 'latest'оно скрывает содержимое страницы до закрытия слоя. Приtype: 'stack'оба представления видны одновременно. - Родительская страница также может заполнять регионы. Ее представления остаются, пока изменяются ее дочерние элементы, поскольку родительский элемент не перестраивается.
- Серверный рендерингработает как есть. Сервер создает страницу внутри
route.run(), поэтому его представленияinto()находятся в HTML, аhydrate()использует существующие узлы.
Несколько регионов на одной странице
Передайте объект для одновременного заполнения нескольких регионов. Основной контент страницы по-прежнему проходит через outlet, поэтому верните его рядом с 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 не управляет двумя независимыми маршрутами: боковую панель заполняет страница, а не второй outlet. Если боковой панели нужны собственные маршрутизатор, загрузка и резервный маршрут, создайте маршрутизатор, но не запускайте его: используйте 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() отклоняется. Объекта Route для записи ошибки пока нет. |
| добавленный слой | запускает собственный загрузчик со своими собственными данными |
| предварительная загрузка в режиме ожидания | загружает только модули: контекста для загрузчика пока нет, а данные могли бы запрашиваться для маршрута, который пользователь никогда не откроет. |
| повторный переход на тот же 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 имеет значение true только во время ожидания модуля или загрузчика; предыдущая страница остается на экране до завершения загрузки. Ошибка загрузки чанка попадает в 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(). Обращения к членам Route перенаправляются активному маршруту: результату последнего start() в браузере или маршруту, чей run() сейчас выполняется на сервере. Без активного маршрута чтение выбрасывает исключение. Таблица маршрутов, скомпилированный механизм сопоставления и кэш модулей хранятся в маршрутизаторе и используются всеми серверными запросами.
router.start(url?)
Загружает модули страниц для url (в браузере по умолчанию location.href) и возвращает Promise<Route>. Промис отклоняется, если совпадения нет и в таблице отсутствует '*'; на сервере это повод вернуть 404. В браузере метод делает Route активным, подключает обработчики popstate и делегированный click, планирует предварительную загрузку. Повторный start() останавливает предыдущий Route этого маршрутизатора. На сервере общее состояние не меняется: выполняйте рендеринг внутри route.run().
router.match(url?)
Сопоставляет URL с таблицей без загрузки модулей, изменения истории, подключения обработчиков и создания Route. Возвращает RouteContext или null, если URL находится вне base, не соответствует шаблону или в браузере указывает на другой origin. Принимает только абсолютные 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() | удаляет обработчики, закрывает открытые слои без результата и очищает outlet. Повторный вызов ничего не меняет. Остановленный Route сохраняет последнее совпадение, а go() ничего не делает. |
run(fn) | вызывает fn, временно делая этот Route активным для маршрутизатора, и возвращает результат. На сервере: route.run(() => renderToString(App)). Работает только синхронно; после возврата из fn маршрут перестает быть активным. |
RouteContext
Первый аргумент страницы: path, pattern, params, search, hash, url. Это контекст, в котором была создана страница. Прочтите router.* для получения текущих значений. Они различаются только в том случае, если запрос или хэш изменились без перестроения.
По умолчанию params имеет тип { [name]: string }. Страница может уточнить его по своему шаблону через Params: '/blog/:slug' превращается в { slug: string }, а '/files/*rest' — в { rest: string }:
import type { Params, RouteContext } from 'nuclo-router' export default function Post(ctx: RouteContext<Params<'/blog/:slug'>>) { return article(h1(ctx.params.slug)) // slug: string, nothing else exists }
PageProps
Второй аргумент страницы. Деструктурируйте то, что нужно странице.
| Свойство | |
|---|---|
layer | место, где страница находится в стеке. См. Layer ниже. |
data | результат, который вернул load() маршрута, или undefined без него. Тип задается через PageProps<TData>. |
outlet | место, где родительская страница отображает свой дочерний маршрут. См. Вложенные маршруты. |
beforeLeave | регистрирует проверку перед удалением страницы. См. Проверки перед уходом со страницы. |
Layer
Положение страницы в стеке и способ закрыть ее слой.
| Член | |
|---|---|
depth | 0 для базовой страницы, 1 и выше для добавленных слоев |
push(href) | открыть еще один слой изнутри страницы, так же, как Route.push() |
close(result?) | закройте этот слой и все, что находится над ним, разрешив push(), который его открыл. Без аргумента слой закрывается без результата. На глубине 0 ничего не делает. |
DataLoader и withLoad()
DataLoader — это (ctx: RouteContext) => TData | Promise<TData>, дополнительный экспорт load страничного модуля. withLoad(loader, load) создает маршрут, load которого запускается параллельно с импортом чанка, а не после него. См. Загрузчики данных маршрутов .
Типы
Все типы доступны как именованные экспорты nuclo-router: типизируйте страницы, загрузчики и проверки без импорта из внутренних путей.
| Тип | Что это такое |
|---|---|
Params<'/blog/:slug'> | — объект параметров для шаблона. RouteParams — это общая форма без конкретных имен параметров, а Match — результат сопоставления. |
PageComponent | страница: (ctx, { layer, data, outlet, beforeLeave }) => … |
RouteModule | модуль страницы: сам компонент или { 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()добавляет запись в историю: на слой можно сослаться, а кнопка «Назад» закрывает его, завершаяpush()со значением undefined, как «Отмена». «Назад» иclose()используют один механизм:close()сохраняет результат и возвращает историю назад, опираясь на глубину изhistory.state. Браузеры хранят около 50 записей на вкладку. Если после долгой навигации запись под слоем потеряна,close()закрывает слои на месте и заменяет текущую запись адресом оставшейся страницы. - Обычная навигация очищает стек. Щелчок по ссылке или
go()не является слоем. Он заменяет все и закрывает каждый открытый слой, поэтому ни один вызывающий код не остается в ожидании. - Прямая загрузка открывает отдельную страницу. 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. - Представление —
(params, ctx) => content. Типparamsвыводится из шаблона, аctx—RouteContextсpatternиparamsпредставления. Вместо содержимого можно вернутьimport()модуля страницы. После загрузки его компонент вызывается сctx. - Ничего не соответствует, ничего не отображается. Добавьте представление
'*'в качестве запасного варианта. Доstart()и послеstop()пусто, как и outlet. - Представление перестраивается только при смене маршрута. Любой переход, сохраняющий страницу, также сохраняет DOM представления.
- Отложенное представление ничего не отображает, пока не прибудет его модуль. Неудачный
import()регистрируется с помощьюconsole.error, не устанавливаетrouter.errorи повторяется при следующей навигации. Обычныйupdate()не повторяет попытку. - На сервере отложенное представление ничего не выводит: его модуль загрузится на клиенте. Для содержимого, которое должно присутствовать в исходном HTML, используйте синхронное представление.
Вложенные маршрутизаторы
Маршруты поддерживают вложенность, поэтому вложенный маршрутизатор обычно заменяет вложенная таблица. Маршрутизатор остается один: страница '/' раздела задает его оболочку, а дочерние маршруты отображаются в ее outlet(). Оболочка сохраняется при переходах внутри раздела:
createRouter({ '/': …, '/docs': docsRoutes, '*': … })
Или дайте этой функции собственный маршрутизатор, созданный, но так и не запущенный. Родительский элемент владеет единственной всеобъемлющей записью, а дочерний элемент разрешает хвост по своим собственным шаблонам, параметрам и резервным вариантам:
// parent table '/docs/*rest': () => import('./docs/Shell.ts'),
const docs = createRouter(docsRoutes, { base: '/docs' }) const hit = docs.match() // { pattern: '/:topic', … } const page = await docsRoutes[hit.pattern]() // the app's own lookup
Главное — не вызывать start(): именно он подключает обработчики, а два запущенных маршрутизатора конфликтуют. В этом варианте дочерний маршрутизатор только сопоставляет URL, поэтому загрузчики данных нужно вызывать самостоятельно. При смене оставшейся части пути оболочка тоже перестраивается: основной маршрутизатор видит один маршрут, а остальное разрешает оболочка. Вложенная таблица сохраняет родительскую страницу смонтированной. Предпочитайте ее, если разделу не нужны отдельные маршрутизатор, base и резервный маршрут.
Несколько маршрутизаторов и общая загрузка
#В приложении может быть несколько маршрутизаторов, по одному на область. Правило простое: запускайте ровно один на документ. Остальные только сопоставляют 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 имеет значение true, пока выполняется хотя бы один из них, и false в остальное время. Предварительно загруженный или синхронно доступный маршрут не меняет этот флаг. При переходе между разделами макет сохраняется, поэтому повторно запускается только загрузчик раздела. Данные макета, его DOM и недописанный текст сохраняются. Повторный переход на тот же URL заново запускает загрузчик конечной страницы и перестраивает ее строку.
Множественная загрузка: второй маршрутизатор со своим индикатором загрузки
Независимая область — боковая панель, лоток, активная область — получает собственный маршрутизатор, созданный, но никогда не запускавшийся, и собственный флаг загрузки. router.pending принадлежит запущенному маршрутизатору и ничего не говорит об этой области, поэтому оба могут загружаться параллельно, и любой из них может завершить работу первым:
import { createRouter } from 'nuclo-router' // Created but never started: start() is what attaches the history and click // listeners, and only the main router is started. match() attaches nothing. const standIn = () => ({ default: () => div() }) // never rendered const sidebarRouter = createRouter({ '/shop': standIn, '/shop/:section': standIn, '*': standIn, }, { base: '/docs' }) // the same base as the main router export const sidebar = { loading: false, info: null as SidebarInfo | null } export async function refreshSidebar(at: string) { const hit = sidebarRouter.match(at) // the second router resolves if (!hit || (hit.pattern !== '/shop' && hit.pattern !== '/shop/:section')) { sidebar.info = null; update(); return } sidebar.loading = true // its own flag, its own spinner sidebar.info = null; update() const data = await fetchSidebar(hit.params.section) sidebar.loading = false; sidebar.info = data update() }
createRouter(routes, { onNavigate: (ctx, kind) => { announce(kind) refreshSidebar(ctx.url) // every navigation re-resolves the sidebar }, })
Второй маршрутизатор только сопоставляет URL: match() возвращает контекст, а боковая панель не добавляется в основную таблицу маршрутов. onNavigate вызывается после каждого завершенного перехода, включая начальный, и позволяет синхронизировать независимую область с URL.
pendingпринадлежит запущенному маршрутизатору. Никогда не запускавшийся маршрутизатор не имеет маршрута,pendingиerror. Область отслеживает свои собственные флаги, а второй спиннер может сидеть рядом с первым. В демо-версии боковая панель загружается 800 мс, а загрузчики маршрута работают 900 мс, поэтому страница может завершиться раньше боковой панели или наоборот.match()не подключает обработчики. Документ может использовать любое число незапущенных маршрутизаторов. У каждого своя таблица, base и кэш модулей, и ни один не меняет историю. Для них нужны только таблица и механизм сопоставления.- Его загрузчики не запускаются автоматически. Запущенный маршрутизатор вызывает
load()совпавших страниц. Незапущенный лишь сопоставляет URL. Если представлению нужны данные, загружайте их самостоятельно, как вrefreshSidebar(). Чтобы поручить загрузку основному маршрутизатору, используйте вложенную таблицу или регион, заполняемый страницей. - Игнорируйте устаревшие ответы. Медленный ответ для боковой панели не должен применяться после перехода в другой раздел. Увеличивайте счетчик поколения при каждом обновлении и отбрасывайте ответы с прежним значением — так же маршрутизатор отбрасывает результаты устаревших переходов.
- Отложенное представление
matches()также не имеетpending. Оно ничего не отображает, пока не загрузится его модуль. Три системы загрузки, три независимых часа:pendingмаршрута, свои флаги и ленивый вид, которого просто пока нет.
Обработка ссылок
#Делегированный обработчик перехватывает обычный клик левой кнопкой по <a href>, включая SVG <a> и вложенные элементы. Остальное обрабатывает браузер: клики с клавишами-модификаторами или средней кнопкой, target кроме _self, download, rel="external", ссылки на другой origin, ссылки только с хешем на текущей странице, пути вне base, пути без совпадения в таблице (чтобы работала серверная страница 404) и события, для которых приложение уже вызвало preventDefault(). Чтобы явно исключить ссылку из обработки, задайте data-nuclo-router="off".
Прокрутка
Навигация через push сохраняет текущую позицию прокрутки. Когда новый 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 не кэшируется. Route хранит функцию страницы и контекст. При уходе со страницы ее DOM удаляется.
stop()освобождает обработчики, отменяет текущую загрузку и сбрасываетpending, поэтому остановленный Route не оставляет индикатор загрузки включенным. Результат модуля, загрузившегося послеstop(), игнорируется. - Изоляция одновременных запросов на сервере.
start()возвращает состояние каждого запроса, аrun()ограничивает его одним синхронным рендерингом, поэтому чередующиеся запросы не могут видеть маршрут друг друга. Общим является только кэш модуля. Область действия заканчивается, когда функцияrun()возвращает значение.renderToString()синхронен, так что этого достаточно. Для асинхронного или потокового рендеринга потребуется, чтобы маршрут передавался черезawait, чегоrun()не делает. - Восстановление прокрутки на
popstateосуществляется браузером (scrollRestoration), а не маршрутизатором. - Запускайте один маршрутизатор на документ. Каждый
start()подключает свой делегированный обработчикclick. Вызовstart()второго маршрутизатора не останавливает первый; повторныйstart()заменяет Route только в том же маршрутизаторе. Чтобы сопоставить URL с другой таблицей, создайте отдельный маршрутизатор и вызывайте толькоmatch(). Он не подключает обработчики, поэтому независимая область может загружаться отдельно отpendingосновного маршрута. См. Несколько маршрутизаторов и общая загрузка. - Намеренно не поддерживаются: hash- и memory-history — маршрут задается путем URL, и сервер должен обслуживать каждый путь; две независимые области маршрутизации под одним URL — страница может заполнять несколько регионов через
into(), но все они принадлежат одному совпавшему маршруту. Независимая область может иметь свой незапущенный маршрутизатор и загрузку, как в разделе Несколько маршрутизаторов и общая загрузка. Предварительная загрузка по наведению также не встроена: вызывайте загрузчик маршрута изmouseenter.
Примеры
Два работоспособных примера в репозитории дополняют друг друга, поэтому каждый параметр где-то можно наблюдать:
examples/router: обзор всего API на стороне клиента. Каждая форма шаблона и правило приоритета, отказ от обработки ссылок, каждый член Route с живыми элементами управления, подмаршрутизатор, стек слоев в три уровня, относительная секция, смонтированная на двух родительских элементах, и загрузчики маршрутов сpendingиerror. Устанавливаетpreload: false, чтобы эти состояния оставались видимыми.examples/router-ssr:renderToString()на запрос, гидратация с проверкой повторного использования серверных узлов, настоящие коды состояния 404,baseотличный от корневого пути, предварительная загрузка в режиме ожидания, которую можно наблюдать на сетевой панели, и загрузчик, данные которого передаются с сервера на клиент, поэтому при гидратации данные не запрашиваются повторно.
В Nuclo нет специального API для вычисляемых значений. Используйте простую функцию и вызывайте ее из динамических значений:
let items = ['apple', 'banana', 'cherry'] let filter = '' const visible = () => items.filter((item) => item.includes(filter)) const Fruits = () => div( p(() => `Showing ${visible().length} of ${items.length}`), ul(list(visible, (item) => li(item))), ) filter = 'an' update()
Используйте простой async/await и вызывайте update() каждый раз при изменении состояния. При загрузке данных это означает один раз до await и один раз после:
let status: 'idle' | 'loading' | 'done' | 'error' = 'idle' let result: unknown = null async function loadData() { status = 'loading' update() try { result = await fetch('/api/data').then((r) => r.json()) status = 'done' } catch { status = 'error' } update() }
Рекомендации
#- Пакетные изменения. Измените все необходимые состояния, затем один раз вызовите
update(). - Функции динамических значений должны быть быстрыми и без побочных эффектов. Все они работают на каждом
update(). - Используйте
scope()для небольших, частых обновлений, таких как ввод текста или таймеры. - Рендеринг в документ. Узлы, которых нет в документе при запуске
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
- Импорт для побочных эффектов, а также классы
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 вызываются один раз для каждого элемента при вставке и удалении. Изменение состояния не запускает их повторно.