EinführungÜberblick
NUCLO / 12

언어

문서

Nuclo 문서

Nuclo 설치, 명시적 업데이트를 통한 UI 구성, 타입이 지정된 CSS 스타일링, 서버 측 HTML 렌더링을 위한 실용적인 참조 문서입니다.

빠른 시작npm create nuclo@latest
빠른 시작
v0.3.30
현재 버전
175
태그 빌더
~18.4 KB
코어, gzip 압축
명시적
런타임 모델

개요

#

Nuclo는 TypeScript용 작은 DOM 라이브러리입니다. div() 및 button()와 같은 일반 함수를 사용하여 페이지를 만들고 일반 변수에 상태를 유지하며 페이지가 변경되어야 할 때 update()를 호출합니다.

Nuclo에는 가상 DOM, 컴파일러, 프록시 및 시그널이 없습니다.

main.ts
import 'nuclo'

let count = 0

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

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

동작 모델

  1. 태그 빌더를 사용하여 UI를 구축하세요.
  2. 상태에 따라 달라지는 모든 값에 대해 함수를 전달합니다.
  3. 일반 JavaScript로 상태를 변경합니다.
  4. update()를 호출하세요. Nuclo는 전달된 함수를 다시 실행하고 DOM을 패치합니다.

변수를 변경해도 DOM에는 영향을 미치지 않습니다. 페이지는 update()를 호출할 때만 변경됩니다.

빠른 시작

#

create-nuclo는 Nuclo 사용에 필요한 설정을 갖춘 Vite 및 TypeScript 프로젝트를 생성합니다.

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

프로젝트 이름을 묻습니다. 프롬프트를 건너뛰려면 이름과 플래그를 전달하세요. npm을 사용하면 플래그 앞에 --를 넣습니다.

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

그런 다음 의존성을 설치하고 개발 서버를 시작합니다.

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

옵션

  • [project-name]: 대상 폴더. 기본값은 nuclo-app입니다.
  • -t, --template <name>: 복사할 템플릿입니다. 현재는 basic만 존재합니다.
  • -y, --yes: 메시지를 표시하지 않고 기본값을 사용합니다.
  • -f, --force: 비어 있지 않은 폴더에 씁니다.
  • -h, --help: 도움말을 출력합니다.

설치 및 가져오기

#

기존 프로젝트에 Nuclo를 추가하려면:

terminal
$ npm install nuclo

또는 pnpm add nuclo, yarn add nuclo 또는 bun add nuclo를 사용하세요. Nuclo에는 의존성이 없습니다.

라우팅은 핵심 패키지의 일부가 아닙니다. nuclo-router로 별도로 배포되며 필요할 때 nuclo 옆에 설치할 수 있습니다.

전역 등록

import 'nuclo'는 다음을 globalThis에 넣습니다.

  • 태그 빌더 175개: HTML 빌더 112개(div, span, button, …)와 Svg 접미사가 붙는 SVG 빌더 63개(svgSvg, pathSvg, …)입니다. 태그 빌더를 참조하세요.
  • 도우미 16개: update, scope, when, list, region, into, on, render, hydrate, forceUpdate, css, cx, createCss, variants, keyframes 및 globalStyle.

Nuclo를 가져오는 코드가 빌더를 호출하는 코드보다 먼저 실행되어야 합니다. ES 모듈은 가져오기 순서대로 실행되므로 다음 코드는 실패합니다.

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

진입 파일에 import 'nuclo'를 먼저 넣거나 전역을 사용하는 모든 파일에서 가져옵니다.

이름으로 가져오기

도우미는 이름으로 가져올 수도 있습니다. 전역에 등록된 함수와 동일한 함수입니다. getCssText() 및 resetStyles()는 명명된 내보내기 전용입니다. 태그 빌더는 전역 전용이므로 import { div } from 'nuclo'는 작동하지 않습니다.

main.ts
import { render, update, getCssText } from 'nuclo'

진입점

  • nuclo: 전역을 등록하고 16개의 도우미와 getCssText 및 resetStyles를 내보냅니다.
  • nuclo/ssr: renderToString, renderManyToString, renderToStringWithContainer 및 getCssText. 서버 렌더링을 참조하세요.
  • nuclo/polyfill: Node.js 및 Bun용 경량 DOM 구현. nuclo/polyfill를 참조하세요.
  • nuclo/types: 타입만 제공합니다. TypeScript를 참조하세요.

TypeScript

#

Nuclo는 자체 타입을 제공합니다. 두 가지가 필요합니다.

  • "moduleResolution": "bundler". node16 및 nodenext는 아직 지원되지 않습니다.
  • DOM 타입 라이브러리입니다. lib를 설정하지 않으면 TypeScript가 기본으로 포함합니다.

프로젝트의 파일 중 하나라도 'nuclo'를 가져오면 모든 파일에 전역 타입이 표시됩니다. 다른 것은 필요하지 않습니다. 예를 들어 테스트용 별도의 tsconfig에서 가져오는 파일이 없으면 직접 타입을 추가하세요.

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["nuclo/types"]
  }
}

또는 /// <reference types="nuclo/types" />를 .d.ts 파일에 추가하세요.

유용한 타입

이러한 타입은 전역이므로 가져오기 없이 사용합니다.

  • NodeModFn<Tag>: 태그 빌더가 반환하는 것. 컴포넌트의 반환 타입으로 사용합니다.
  • NodeModLike<Tag>: 태그 빌더가 인수로 허용하는 모든 것.
  • ExpandedElement<Tag>: render() 및 hydrate()가 반환하는 내용입니다.
  • ElementTagName: 모든 HTML 태그 이름.
  • ExpandedElementAttributes<Tag>: on* 핸들러, onMount 및 onDestroy를 포함하여 태그 빌더가 허용하는 속성 객체입니다. 속성을 전달하는 컴포넌트에 사용하세요.
  • MountCallback 및 DestroyCallback: onMount 및 onDestroy 핸들러.
  • WhenBuilder, ListModifier: when() 및 list()가 반환하는 내용입니다.
  • RegionOptions, RegionType: region() 옵션에 사용하는 타입입니다.
  • StyleResult, Style, ThemeConfig, CssInstance 및 기타 스타일링 타입. import type { StyleResult } from 'nuclo'를 가져올 수도 있습니다.
badge.ts
function Badge(text: string): NodeModFn<'span'> {
  return span(text)
}

ExpandedElement<Tag>는 모든 DOM 멤버를 선택 사항으로 표시합니다. 전체 요소 타입(render(App, root) as HTMLDivElement)이 필요할 때 캐스팅하세요.

모든 HTML 태그는 동일한 이름을 가진 전역 함수입니다: div, span, input, … 한 가지 예외는 <var>입니다. var는 예약어이므로 var_입니다. 모든 SVG 태그에는 Svg 접미사(svgSvg, pathSvg, circleSvg, …)가 있습니다.

태그 빌더를 호출하는 시점에는 요소가 생성되지 않습니다. div('Hi')는 나중에 요소를 생성하는 함수인 빌더를 반환합니다. Nuclo는 render()로 마운트하거나 다른 빌더, when(), list() 안에 배치할 때 이 함수를 호출합니다. 사용할 때마다 새 요소가 생성됩니다.

card.ts
let name = 'Ada'

const Card = () =>
  div(
    { id: 'card', className: 'card' },
    h2('Profile'),
    p(() => `Hello, ${name}`),
    button('Rename', { onClick: () => { name = 'Grace'; update() } }),
  )

render(Card, document.body)
라이브빌더는 속성, 정적 자식, 동적 텍스트를 받습니다. Rename은 상태를 변경한 다음 update()를 호출합니다.열기 ↗

Profile

Hello, Ada

인수

빌더는 원하는 수의 인수를 가져와 왼쪽에서 오른쪽으로 적용합니다.

  • string, number, bigint: 텍스트 노드.
  • () => value: update()에서 변경되는 텍스트입니다. 동적 값을 참조하세요.
  • null, undefined: 무시됩니다.
  • true, false: "true" 또는 "false" 텍스트로 렌더링됩니다. 조건은 when()를 사용하세요.
  • 일반 객체: 속성. 속성을 참조하세요.
  • css(), cx() 또는 variants()의 반환값: 클래스를 추가합니다.
  • on(...): 이벤트 리스너 또는 생명주기 훅입니다.
  • 다른 빌더, when() 또는 list(): 자식 요소로 추가됩니다.
  • DOM Node: 그대로 첨부합니다.
  • (el, index) => …: 사용자 정의 수정자. 아래를 참조하세요.

주의할 점

  • div(isAdmin && span('Admin'))는 "false" 텍스트를 렌더링합니다. div(when(() => isAdmin, span('Admin')))를 작성하세요.
  • 배열 자체를 자식 요소로 전달할 수는 없습니다. 펼쳐서 전달하세요: ul(...names.map((n) => li(n))). 변경되는 목록의 경우 list()를 사용하세요.
  • 태그와 이름이 같은 지역 변수는 빌더를 가립니다. function Row(label: string) 내부에서는 label(...)를 호출할 수 없습니다. label, title, data, p, a 및 i와 같은 로컬 이름을 바꿉니다.
  • 브라우저는 id가 있는 모든 요소를 전역 변수로 노출합니다. Nuclo가 로드되기 전에 페이지에 <main id="main">가 포함되어 있으면 Nuclo는 해당 전역을 그대로 유지하므로 main(...)는 TypeError를 발생시킵니다. 다른 ID를 사용하세요.

사용자 정의 수정자

매개변수를 하나 이상 선언한 함수는 요소를 생성할 때 인수 순서대로 한 번 실행됩니다. 첫 번째 인수는 요소이며 두 번째 인수는 내부용 인덱스이므로 사용하지 마세요. 문자열이나 숫자를 반환하면 텍스트로 추가되고, Node를 반환하면 해당 노드가 추가됩니다. 객체를 반환하면 속성으로 적용됩니다.

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

요소 가져오기

render()는 루트 요소를 반환합니다. 트리 내부에서는 on("mount", (el) => …) 또는 사용자 정의 수정자를 사용합니다. 분리된 요소를 생성하기 위해 빌더 함수를 직접 호출할 수도 있습니다.

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

다음 update() 전에 연결하세요. update()가 실행될 때 문서에 없는 노드는 업데이트를 영구적으로 중지합니다.

SVG

SVG 빌더는 동일한 인수를 사용합니다. Nuclo는 모든 SVG 속성을 setAttribute로 설정하므로 "stroke-width"와 같은 이름은 작성된 대로 작동합니다.

icon.ts
const Icon = () =>
  svgSvg(
    { viewBox: '0 0 24 24', width: 24, height: 24, className: 'icon' },
    circleSvg({ cx: 12, cy: 12, r: 10, fill: 'currentColor' }),
  )

모든 빌더

HTML: a abbr address area article aside audio b base bdi bdo blockquote body br button canvas caption cite code col colgroup data datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins kbd label legend li link main map mark menu meta meter nav noscript object ol optgroup option output p picture pre progress q rp rt ruby s samp script search section select slot small source span strong style sub summary sup table tbody td template textarea tfoot th thead time title tr track u ul var_ video wbr

SVG: aSvg animateSvg animateMotionSvg animateTransformSvg circleSvg clipPathSvg defsSvg descSvg ellipseSvg feBlendSvg feColorMatrixSvg feComponentTransferSvg feCompositeSvg feConvolveMatrixSvg feDiffuseLightingSvg feDisplacementMapSvg feDistantLightSvg feDropShadowSvg feFloodSvg feFuncASvg feFuncBSvg feFuncGSvg feFuncRSvg feGaussianBlurSvg feImageSvg feMergeSvg feMergeNodeSvg feMorphologySvg feOffsetSvg fePointLightSvg feSpecularLightingSvg feSpotLightSvg feTileSvg feTurbulenceSvg filterSvg foreignObjectSvg gSvg imageSvg lineSvg linearGradientSvg markerSvg maskSvg metadataSvg mpathSvg pathSvg patternSvg polygonSvg polylineSvg radialGradientSvg rectSvg scriptSvg setSvg stopSvg styleSvg svgSvg switchSvg symbolSvg textSvg textPathSvg titleSvg tspanSvg useSvg viewSvg

컴포넌트는 button(...)와 같은 빌더를 반환하는 일반 함수입니다. 컴포넌트 API가 없습니다. 렌더링하거나 트리에 배치할 때까지는 아무 것도 생성되지 않습니다.

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

render(() => div(Counter('A'), Counter('B')))
라이브컴포넌트는 일반 함수입니다. Counter()를 호출할 때마다 자체 카운트를 유지합니다.열기 ↗

함수 내부에 선언된 상태는 해당 인스턴스에 속합니다. 외부에 선언된 상태는 모든 인스턴스에서 공유됩니다. forceUpdate()는 컴포넌트를 다시 호출하여 내부에 선언된 상태를 재설정합니다.

자식 요소도 일반 인수로 전달합니다:

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

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

속성

#

속성을 설정하려면 일반 객체를 전달하세요. 여러 객체를 전달할 수 있습니다. 동일한 키의 경우 병합되는 className 및 style를 제외하고 나중 값이 우선합니다. 값은 정적이거나 모든 update()에서 다시 실행되는 매개변수가 없는 함수일 수 있습니다.

example.ts
let busy = false

const save = button(
  { type: 'submit', disabled: () => busy, 'aria-busy': () => String(busy), 'data-id': 42 },
  'Save',
)
라이브속성 값은 함수일 수 있습니다. 두 번째 버튼은 busy를 전환하고 update()를 호출합니다. Save 버튼의 disabled와 aria-busy가 이에 따라 바뀝니다.열기 ↗

값 설정 방법

  • HTML 요소에 해당 이름(value, checked, disabled, htmlFor, tabIndex, …)의 속성이 있는 경우 Nuclo가 속성을 설정합니다. 그렇지 않으면 "aria-*" 및 "data-*"와 같이 setAttribute(key, String(value))를 호출합니다.
  • SVG 요소에서 Nuclo는 항상 setAttribute를 호출합니다.
  • null 및 undefined는 건너뜁니다. null 또는 undefined를 반환하는 동적 값은 이전 값을 유지합니다. 값을 지우려면 ""를 반환하고, 불리언 속성이라면 false를 반환하세요.
  • 불리언은 버튼의 disabled, checked, hidden 및 required와 같은 DOM 불리언 속성에 대해 작동합니다. 다른 곳에서는 true 및 false가 "true" 및 "false" 문자열로 작성됩니다.
  • "data-*" 키를 사용하세요. dataset는 지원되지 않습니다.
  • innerHTML는 이스케이프되지 않으며 서버 렌더링에서는 이를 출력하지 않습니다. 사용을 피하세요.

클래스

className를 사용하세요. 정적 클래스 문자열, css() 결과 및 동적 className가 모두 병합됩니다. 동적 className는 정적 클래스를 유지하고 ""를 반환하면 정적 클래스만 남습니다.

example.ts
let active = false
const base = css({ p: 8 })

const item = li(base, { className: 'item' }, { className: () => (active ? 'active' : '') }, 'Home')
// class="<base> item", then "<base> item active" after active = true; update()

HTML 요소에는 class 키를 사용하지 마세요. 원시 속성을 설정하고 css() 클래스를 포함하여 이전에 적용된 모든 클래스를 대체합니다.

인라인 스타일

style는 camelCase 키가 있는 객체 또는 이를 반환하는 함수를 사용합니다.

example.ts
let progress = 30

const bar = div({ style: () => ({ width: `${progress}%`, backgroundColor: 'teal' }) })
  • Nuclo는 단위를 자동으로 추가하지 않습니다. 12가 아닌 "12px"를 작성하세요. opacity 및 zIndex와 같은 단위 없는 속성은 숫자를 허용합니다.
  • "", null 및 undefined 속성을 지웁니다.
  • 동적 스타일은 반환하는 키만 설정합니다. 반환을 중지한 키는 이전 값을 유지하므로 ""를 반환하여 삭제하세요.
  • 스타일 문자열("color: red") 및 사용자 정의 속성("--gap")은 지원되지 않습니다. 대신 css()를 사용하세요. raw를 통해 사용자 정의 속성을 사용합니다.
fn공개 API
// DOM events such as "click". El and Ev are inferred from the tag and the event name. function on(type: DOMEventName, listener: (this: El, e: Ev) => unknown, options?: boolean | AddEventListenerOptions): NodeModFn // Custom events function on<K extends string, E extends Event = Event>(type: K, listener: (this: El, e: E) => unknown, options?: boolean | AddEventListenerOptions): NodeModFn // Lifecycle, see Lifecycle function on(type: "mount", listener: MountCallback): NodeModFn function on(type: "destroy", listener: DestroyCallback): NodeModFn

이벤트를 수신하는 방법에는 on* 속성과 on() 수정자의 두 가지 방법이 있습니다.

on* 속성

속성 객체에서 onClick(on 다음에 대문자가 옴)와 같은 키 아래의 함수는 동적 값이 아닌 이벤트 핸들러입니다.

search.ts
let query = ''

const search = input({
  placeholder: 'Search',
  value: () => query,
  onInput: (e) => { query = e.currentTarget.value; update() },
  onKeyDown: (e) => { if (e.key === 'Escape') { query = ''; update() } },
})
  • TypeScript는 키와 요소에서 이벤트 타입을 추론합니다. e.currentTarget 및 this가 요소입니다.
  • Nuclo는 기본 속성을 설정하므로 onClick는 el.onclick가 됩니다. 이벤트당 하나의 핸들러가 있습니다. 최신 onClick가 이전 핸들러를 대체합니다.
  • onDoubleClick 및 onDblClick는 모두 dblclick를 의미합니다. onclick와 같은 소문자 키는 타입 오류입니다.
  • 알려진 이벤트가 아닌 이름은 소문자 형식을 수신합니다. onMyEvent는 "myevent"를 수신합니다.
  • 핸들러에서 발생하는 오류는 포착되지 않습니다.
라이브onInput과 onKeyDown은 일반 속성입니다. 입력한 다음 Escape를 눌러 지웁니다.열기 ↗

Type something, then press Escape.

on()

on()는 요소에서 addEventListener를 호출하는 수정자를 반환합니다.

panel.ts
const panel = div(
  on('scroll', onScroll, { passive: true }),
  on('click', logClick),
  on('click', closeMenu),
)
  • 모든 on()는 리스너를 추가합니다. 모두 순서대로 실행됩니다.
  • options는 addEventListener에 그대로 전달됩니다: capture, once, passive, signal.
  • on()를 빌더에 직접 전달하면 요소 타입이 추론됩니다: input(on('input', (e) => e.currentTarget.value)).
  • 리스너가 오류를 발생시키면 Nuclo는 console.error로 오류를 기록하고 다른 리스너는 계속 실행됩니다.
  • Nuclo는 요소를 제거할 때 리스너를 제거합니다. 서버에서 on()는 아무 작업도 수행하지 않습니다.
  • 하나의 on() 값을 여러 요소에서 재사용할 수 있습니다.

사용자 정의 이벤트의 경우 이벤트 타입을 제네릭 인수로 전달합니다.

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

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

어느 것을 사용할 것인가

일반적인 경우에는 on* 속성을 사용하세요. 한 이벤트에 여러 리스너를 등록하거나, 리스너 옵션 또는 "my-event"와 같은 사용자 정의 이벤트 이름이 필요하면 on()를 사용하세요.

fn공개 API
// As attributes { onMount?: (el: El) => void | (() => void), onDestroy?: (el: El) => void } // Or with on() function on(type: "mount", listener: (el: El) => void | (() => void)): NodeModFn function on(type: "destroy", listener: (el: El) => void): NodeModFn

onMount는 요소가 삽입된 후 한 번 실행됩니다. onDestroy는 Nuclo가 요소를 제거할 때 한 번 실행됩니다. 속성으로 작성하거나 on()를 사용하여 작성하십시오. 두 형식은 동일하게 동작하며 요소 타입은 태그에서 추론됩니다.

timer.ts
let seconds = 0

const Timer = () =>
  span(
    {
      onMount: () => {
        const id = setInterval(() => { seconds++; update() }, 1000)
        return () => clearInterval(id) // runs when the span is removed
      },
    },
    () => `${seconds}s`,
  )

const search = input(on('mount', (el) => el.focus()))
라이브onMount가 인터벌을 시작하고 정리 함수를 반환합니다. 마운트 해제 시 정리 함수가 실행되므로 다시 마운트할 때까지 카운트가 멈춥니다.열기 ↗

실행될 때

  • onMount는 요소를 삽입한 render(), hydrate(), update() 또는 forceUpdate() 호출이 끝날 때 실행됩니다. 트리가 만들어지는 동안에는 절대로 실행되지 않습니다.
  • onMount가 함수를 반환하는 경우 해당 함수는 요소의 onDestroy 콜백 이후 소멸 시 실행됩니다.
  • onDestroy는 Nuclo가 요소를 제거할 때 실행됩니다. 즉, list() 행이 제거되고, when() 분기가 전환되고, into()가 해당 영역을 떠나거나, forceUpdate()가 요소를 삭제합니다. 제거 직전에 실행되므로 해당 요소는 여전히 문서에 있습니다.

DOM API로 직접 제거하기

node.remove() 또는 innerHTML = ''를 사용하여 요소를 직접 제거하면 해당 onDestroy 및 정리가 실행되지 않으며 타이머는 계속 실행됩니다. Nuclo에는 unmount()가 없습니다. 앱을 마운트 해제할 때 정리 함수도 실행하려면 when()로 래핑하세요.

main.ts
let mounted = true
render(() => div(when(() => mounted, App())), root)

mounted = false
update() // App's onDestroy hooks and cleanups run

실행 순서와 오류

  • 어느 형식으로든 원하는 만큼 훅을 등록할 수 있습니다. 등록한 순서대로 실행됩니다.
  • onDestroy는 항상 자식 요소를 먼저 실행한 다음 상위 항목을 실행합니다.
  • 요소는 인수 순서를 따르는 첫 번째 onMount 훅 순서로 마운트되며 각 요소는 모든 훅을 함께 실행합니다. 자식이 부모의 훅에 의존한다면 부모의 훅을 자식 요소보다 먼저 전달하세요.
  • 훅에서 발생한 오류는 console.error로 기록됩니다. 다른 훅은 여전히 실행됩니다.
  • 콜백은 아무것도 반환하지 않거나 정리 함수를 반환해야 하므로 async로 만들지 마세요. 반환된 Promise는 정리 용도로 사용되지 않습니다. 내부에서 비동기 작업을 시작하세요: { onMount: (el) => { void load(el) } }.
  • 훅은 서버에서 실행되지 않습니다. 클라이언트에서는 hydrate() 이후에 실행됩니다.
참고: 훅은 이펙트가 아닙니다. 상태가 변경되면 다시 실행되지 않습니다. 각각은 요소당 최대 한 번 실행됩니다.

Nuclo는 자체적으로 페이지를 업데이트하지 않습니다. 상태를 변경한 다음 update()를 호출하세요.

counter.ts
let count = 0

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

render(Counter)
라이브count++는 변수만 변경합니다. 이 버튼이나 페이지의 다른 동작이 다음 update()를 호출하면 텍스트가 반영됩니다.열기 ↗

Clicked 0 times

원하는 만큼 상태를 변경한 다음 update()를 한 번 호출합니다.

example.ts
let first = 'Ada'
let last = 'Lovelace'

render(() => p(() => `${first} ${last}`))

first = 'Grace'
last = 'Hopper'
update() // one pass, both changes

이벤트 핸들러, 타이머, 페치 콜백 또는 onMount에서 update()를 호출합니다. 동적 값 내부에서 호출하지 마세요.

동적 값

#

매개변수가 없는 함수는 동적 값입니다. Nuclo는 요소를 빌드할 때 한 번 실행하고 모든 update()에서 다시 실행합니다. 텍스트 자식 요소, 속성 값, style 및 className에 대해 동적 값을 사용합니다. 나타나거나 사라지거나 반복되는 자식 노드의 경우 when() 및 list()를 사용하세요.

example.ts
let name = 'Alice'
let busy = false

const Profile = () =>
  div(
    h1('Welcome'),              // static
    p(() => `Hello, ${name}`),  // dynamic text
    button({ disabled: () => busy }, 'Save'),
  )
라이브이름을 수정해 보세요. 정적 텍스트는 요소를 만들 때 고정되며, 동적 텍스트는 update()마다 다시 계산됩니다. forceUpdate()는 트리를 다시 구성하므로 정적 텍스트도 반영됩니다.열기 ↗

static: Hello, Alice

dynamic: Hello, Alice

Nuclo는 매개변수 개수(fn.length)를 확인합니다. 매개변수를 선언하는 함수는 사용자 정의 수정자이며 한 번만 실행됩니다. (x = 0) => … 및 (...args) => …의 길이는 0이므로 동적입니다.

동적 텍스트

  • 문자열, 숫자, bigint 또는 불리언은 String(value)로 렌더링됩니다. () => ok && 'Saved'에는 "false"가 표시되므로 () => (ok ? 'Saved' : '')를 작성합니다.
  • null 및 undefined는 빈 텍스트로 렌더링됩니다.
  • 첫 번째 실행의 css() 또는 cx() 결과는 함수를 텍스트가 아닌 동적 클래스로 만듭니다. cx()를 참조하세요.
  • 노드, 빌더, 배열 또는 일반 객체는 아무것도 렌더링하지 않으며 업데이트하지 않습니다.

동적 속성

  • Nuclo는 Nuclo가 쓴 마지막 값과 다른 경우에만 값을 씁니다. 실제 DOM과 비교하지 않으므로 사용자가 입력 필드에 텍스트를 입력한 뒤 함수가 이전과 동일한 값을 반환하면 입력한 텍스트가 유지됩니다.
  • null 또는 undefined의 결과는 아무것도 쓰지 않으므로 이전 값이 유지됩니다. className는 예외입니다. 거짓으로 평가되는 값(falsy)을 반환하면 동적 클래스를 제거하고 정적 클래스는 유지합니다.
  • style 함수는 모든 업데이트에서 반환된 객체를 적용합니다.

오류

동적 값 함수에서 오류가 발생하면 Nuclo가 오류를 포착하고 나머지 업데이트를 계속합니다. 텍스트는 비어 있거나 이전 값을 유지하고 속성도 이전 값을 유지합니다. 이후 update()에서 함수가 정상 실행되면 복구됩니다. 단, 첫 번째 실행에서 오류가 발생한 텍스트 함수는 이후에도 빈 텍스트로 남습니다.

fn공개 API
function update(...scopeIds: string[]): void

페이지의 모든 동적 값을 동기적으로 한 번씩 평가합니다. ID를 사용하면 해당 scope() ID로 표시된 요소 내부의 내용만 업데이트됩니다. 각 업데이트는 다음 단계를 순서대로 실행합니다.

  1. list(): 항목을 다시 읽고 행을 추가, 이동 또는 제거합니다.
  2. when(): 조건을 다시 확인합니다. 다른 분기가 활성화될 때만 분기가 다시 생성됩니다.
  3. style 및 className를 포함한 동적 속성.
  4. 동적 텍스트.
  5. 1단계와 2단계에서 삽입한 요소의 onMount를 실행합니다.
  • 모든 동적 값을 실행합니다. 모든 동적 값은 데이터가 변경되지 않은 경우에도 모든 호출에서 실행됩니다. DOM 쓰기만 건너뜁니다. 동적 값 함수는 빠르게 실행되고 부수 효과가 없도록 작성하세요.
  • 문서의 노드만. update()가 실행될 때 문서에 노드가 없으면 Nuclo는 노드 추적을 중지합니다. 연결한 후에도 다시 업데이트되지 않습니다. 이는 list() 및 when()에도 적용됩니다. 문서에 있는 컨테이너로 렌더링합니다.
  • 정적 값은 절대 변경되지 않습니다. 일반 문자열 및 속성 값은 요소가 빌드될 때 고정됩니다. 다시 빌드하려면 forceUpdate()를 사용하세요.
  • 오류. 텍스트와 속성 함수의 오류는 포착되며 나머지 업데이트는 계속됩니다. when() 조건에서 오류가 발생하면 오류가 기록되고 해당 when()의 업데이트가 중지됩니다. list()의 items 또는 renderItem 함수에서 오류가 발생하면 오류가 update() 호출자에게 전파되고 나머지 단계는 실행되지 않습니다.
  • 서버. update()는 서버에서 아무 작업도 수행하지 않습니다.
라이브update()는 데이터가 바뀌지 않았더라도 모든 동적 값을 실행합니다. 이 페이지의 다른 데모를 사용해도 카운트가 증가합니다.열기 ↗

This dynamic value has run 1 times.

fn공개 API
function scope(...ids: string[]): NodeModFn

전달받은 요소에 하나 이상의 스코프 ID를 지정합니다. 그런 다음 update('id')는 요소 자체 속성을 포함하여 해당 요소 내부에 있는 내용만 업데이트합니다.

example.ts
let cartItems: string[] = []
let user = 'Ada'

const App = () =>
  div(
    header(() => `Hi, ${user}`),
    aside(scope('cart'), span(() => `Items: ${cartItems.length}`)),
  )

render(App)

cartItems.push('apple')
user = 'Grace'
update('cart') // "Items: 1", the header still says "Hi, Ada"
update()       // everything, the header now says "Hi, Grace"
라이브두 버튼 모두 항목을 추가하고 사용자 이름을 바꿉니다. update('cart')는 해당 스코프의 패널만 업데이트합니다.열기 ↗
Hi, Ada
Items: 0
  • 요소에는 scope('cart', 'sidebar')라는 여러 ID가 있을 수 있습니다.
  • update('a', 'b')는 a 또는 b 내부의 모든 것을 업데이트합니다. 여러 요소가 동일한 ID를 사용하면 모두 업데이트됩니다.
  • 중첩된 범위를 사용하는 update('outer')에는 내부 범위가 포함됩니다. update('inner')는 외부에 닿지 않습니다.
  • 문서에 일치하는 요소가 없는 ID를 지정하면 아무것도 업데이트되지 않고 오류도 발생하지 않으므로 철자를 확인하세요.
  • scope()는 서버에서 아무 작업도 수행하지 않습니다.
fn공개 API
function when(condition: boolean | (() => boolean), ...content: NodeModLike[]): WhenBuilder interface WhenBuilder { when(condition: boolean | (() => boolean), ...content: NodeModLike[]): WhenBuilder else(...content: NodeModLike[]): WhenBuilder }

조건에 따라 콘텐츠를 렌더링합니다. Nuclo는 update()를 호출할 때마다 조건을 다시 확인합니다. 가장 먼저 참인 조건의 분기를 선택하며, 참인 조건이 없으면 .else()의 콘텐츠를 렌더링합니다. .else()도 없으면 아무것도 렌더링하지 않습니다.

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

const Status = () =>
  div(
    when(() => status === 'loading', p('Loading…'))
      .when(() => status === 'error', p('Something went wrong'))
      .else(p('Ready')),
  )
라이브가장 먼저 참이 되는 조건을 사용합니다. 참인 조건이 없으면 .else()를 렌더링합니다. 새 분기는 처음부터 만듭니다.열기 ↗

Loading…

조건

  • 상태의 경우 함수를 전달합니다. 일반 불리언은 한 번 읽혀집니다. when(open, …)는 절대 변경되지 않습니다.
  • TypeScript에서 함수는 불리언을 반환해야 합니다. () => items.length > 0 또는 () => user != null를 작성하세요.

업데이트되는 내용

  • 동일한 분기가 활성 상태로 유지되는 동안 해당 요소는 유지됩니다. 내부의 동적 값, 목록 및 when() 블록은 계속 업데이트됩니다.
  • 다른 분기가 활성화되면 이전 콘텐츠가 제거되고 해당 onDestroy 훅이 실행됩니다. 새 분기는 처음부터 생성되며 onMount 훅이 실행됩니다. 입력 텍스트나 포커스와 같은 이전 요소의 상태는 손실됩니다.

분기 내용

분기에는 텍스트, 동적 텍스트, 빌더, list(), 중첩된 when()를 전달할 수 있습니다. DOM 노드를 직접 전달하지 말고 p('Hi')와 같은 빌더를 전달하세요. 직접 전달한 DOM 노드는 분기가 한 번 숨겨지면 업데이트가 중지됩니다.

분기의 속성 객체 및 on()는 상위 요소에 적용되며 분기가 숨겨진 후에도 유지됩니다. 조건부 속성의 경우 동적 속성({ className: () => (active ? 'active' : '') })을 사용합니다.

체인 연결

.when() 및 .else()는 새 빌더를 반환하고 원본을 변경하지 않습니다. .else()를 두 번 호출하면 마지막 호출이 적용됩니다.

fn공개 API
function list<T>( items: () => readonly T[] | Iterable<T>, renderItem: (item: T, index: number) => ListRenderResult, ): ListModifier

항목당 하나의 행을 렌더링합니다. 모든 update()에서 Nuclo는 items()를 다시 호출하고 동일성(===)을 기준으로 항목을 기존 행과 연결합니다.

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

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

render(Fruits)

fruits.push('Cherry')
update()

행 일치 방법

  • 여전히 존재하는 항목이 해당 행을 유지합니다. Nuclo는 필요한 경우 행을 이동하고 renderItem를 다시 호출하지 않습니다.
  • 새 항목은 새 행을 가져옵니다. 제거된 항목은 해당 행을 잃고 해당 onDestroy 훅이 실행됩니다.
  • 중복 항목은 각각 고유한 행을 갖습니다.
  • 현재 배열(push, splice, sort)을 변경하거나 새 배열을 반환할 수 있습니다.

유지된 행은 다시 렌더링되지 않으므로 변경되는 필드에 동적 값을 사용하세요.

todos.ts
const todos = [{ text: 'Write docs', done: false }]

const Todos = () =>
  ul(
    list(() => todos, (todo) =>
      li({ className: () => (todo.done ? 'done' : '') }, () => todo.text),
    ),
  )

render(Todos)

todos[0].done = true
update() // same <li>, its class is now "done"

복사본은 새 항목이므로 배열을 todos.map((t) => ({ ...t }))로 교체하면 모든 행이 다시 빌드됩니다.

라이브행은 객체의 참조 동일성으로 연결됩니다. 행의 체크박스를 선택한 다음 Reverse를 눌러 보세요. 체크박스를 포함한 같은 <li>가 이동합니다.열기 ↗

renderItem가 반환하는 것

  • li(...), tr(...) 또는 SVG 빌더와 같은 하나의 요소를 반환합니다. 항목을 건너뛰려면 null 또는 undefined를 반환하세요. 문자열, 텍스트 노드 및 문서 조각은 아무것도 렌더링하지 않습니다.
  • when() 또는 list()를 직접 반환하지 마세요. 행 요소 li(todo.text, when(() => todo.done, ' ✓')) 안에 넣으세요.
  • 항목을 숨기려면 renderItem가 아닌 items()로 필터링하세요.
  • 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()

#
fn공개 API
function region(options: RegionOptions): NodeModFn function into(id: string, ...content: NodeModLike[]): NodeModFn function into(regions: Record<string, NodeModLike | NodeModLike[]>): NodeModFn interface RegionOptions { id: string type?: 'latest' | 'stack' // default: 'latest' empty?: NodeModLike }

region()은 트리에서 이름이 있는 위치를 지정하고 into()는 트리의 어느 깊이에서든 그곳을 채웁니다. 레이아웃은 콘텐츠를 props로 받지 않으며 콘텐츠를 만드는 컴포넌트는 콘텐츠가 배치될 위치를 몰라도 됩니다.

app.ts
const Layout = () =>
  div(
    aside(nav(a({ href: '/' }, 'Home'))),
    main(region({ id: 'main', empty: p('Nothing open.') })),
  )

// Anywhere in the tree, before or after the region
const Article = () => into('main', article(h1('Hello')))

render(() => div(Layout(), Article()))

작동 방식

  • into()는 작성한 곳에 코멘트만 남깁니다. 해당 콘텐츠는 영역의 마커 사이에서 렌더링됩니다.
  • 선언 순서는 중요하지 않습니다. 영역보다 먼저 생성된 into()는 기다렸다가 영역이 생성되면 표시됩니다. 뷰는 생성된 순서대로 표시됩니다.
  • 콘텐츠의 생명주기는 뷰와 같습니다. into()를 보유하는 when()가 닫히거나 해당 list() 행이 제거되면 콘텐츠가 영역을 떠납니다. 다시 열면 두 개가 아닌 한 개의 사본이 추가됩니다.
  • when()가 닫히는 등 영역이 사라지면 해당 뷰는 다시 기다립니다. 나중에 동일한 ID로 구축된 영역이 이를 보여줍니다.
  • ID가 영역과 일치하지 않는 into()는 아무것도 렌더링하지 않으며 오류도 발생하지 않으므로 철자를 확인하세요.
  • ID는 트리에서 고유합니다. 이미 존재하는 영역과 같은 ID로 다른 영역을 만들면 경고를 기록합니다.
  • 영역은 서버 렌더링과 함께 작동합니다. hydrate()에서 각 뷰는 서버가 렌더링한 HTML을 다시 만들지 않고 재사용합니다.

옵션

  • id: into() 호출이 영역을 찾는 데 사용하는 이름입니다.
  • type: 'latest'(기본값)는 최신 뷰만 표시합니다. 'stack'는 도착 순서대로 모든 뷰를 표시합니다.
  • empty: 영역에 뷰가 없는 동안 렌더링됩니다.

누적 영역

type: 'stack'를 사용하면 다른 뷰 위에 열리는 뷰가 아래 뷰를 다시 생성하지 않습니다. DOM, 포커스, 폼 입력값이 유지됩니다. 페이지 위의 대화 상자와 같은 레이어에 사용하세요.

layers.ts
let open = false

const App = () =>
  div(
    region({ id: 'main', type: 'stack' }),
    into('main', form(input({ placeholder: 'Keeps its text' }))),
    when(() => open, into('main', div('On top'))),
  )

여러 영역을 동시에

한 번의 into()로 여러 영역을 채우려면 객체를 전달하세요. 각 값은 자식 하나 또는 자식 배열일 수 있습니다. 두 형식 모두 중첩해서 사용할 수 있으며, 다른 뷰의 콘텐츠 안에 있는 into()도 자신이 지정한 영역을 채웁니다.

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

트리 깊은 곳에서 열린 대화 상자

오버레이 영역은 루트에 위치합니다. 모든 깊이의 모든 컴포넌트는 into()를 when()로 래핑하여 대화 상자를 엽니다. when()를 닫으면 해당 영역에서 대화 상자가 제거됩니다.

dialog.ts
let confirming = false

const App = () =>
  div(
    Page(),
    div({ className: 'overlay' }, region({ id: 'overlay' })),
  )

// Deep inside Page()
const DeleteButton = () =>
  div(
    button('Delete', { onClick: () => { confirming = true; update() } }),
    when(() => confirming,
      into('overlay',
        dialog({ open: true },
          p('Delete this item?'),
          button('Cancel', { onClick: () => { confirming = false; update() } }),
        ),
      ),
    ),
  )

목록 행의 토스트

list()의 각 행은 뷰입니다. 누적된 영역에는 해당 항목이 순서대로 표시되며 배열에서 항목을 제거하면 알림이 제거됩니다.

toasts.ts
const toasts: { text: string }[] = []

const notify = (text: string) => {
  toasts.push({ text })
  update()
}

const dismiss = (toast: { text: string }) => {
  toasts.splice(toasts.indexOf(toast), 1)
  update()
}

const App = () =>
  div(
    button('Save', { onClick: () => notify('Saved.') }),
    div({ className: 'toasts' }, region({ id: 'toasts', type: 'stack' })),
    list(() => toasts, (toast) =>
      into('toasts', div(toast.text, button('×', { onClick: () => dismiss(toast) }))),
    ),
  )

페이지에서 설정한 페이지 제목

기본값인 type: 'latest'에서는 가장 최근 뷰를 표시합니다. 레이아웃에 제목 영역을 두고 열린 페이지가 제목을 지정합니다. 페이지에 into('title', …)가 없으면 empty 텍스트가 표시됩니다.

title.ts
let page: 'home' | 'settings' = 'home'

const Layout = () =>
  div(
    h1(region({ id: 'title', empty: 'My app' })),
    when(() => page === 'home', Home()).else(Settings()),
  )

const Home = () => p('Welcome.')

const Settings = () =>
  div(
    into('title', 'Settings'),
    form(/* … */),
  )

뷰 내부의 동적 값

into()의 내용은 일반 Nuclo 내용입니다. 그 안의 함수는 상태에 따라 달라지는 값이며 영역이 어디에 있든 update()에서 새로 고쳐집니다.

status.ts
const selected = new Set<string>()

const Layout = () =>
  div(
    header(region({ id: 'status' })),
    Table(),
  )

const Table = () =>
  div(
    into('status', () => `${selected.size} selected`),
    list(() => rows, (row) =>
      label(
        input({ type: 'checkbox', onChange: (e) => {
          e.currentTarget.checked ? selected.add(row.id) : selected.delete(row.id)
          update()
        } }),
        row.name,
      ),
    ),
  )

동적으로 나타나고 사라지는 영역

대시보드의 into()가 한 번 작성됩니다. 사용자가 로그아웃한 동안에는 main 영역이 없으므로 뷰가 기다립니다. 로그인하면 영역이 구축되고 뷰가 표시됩니다. 로그아웃하면 영역이 제거되고 뷰가 다시 기다립니다.

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

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

자체 영역이 포함된 뷰

뷰는 region()를 포함할 수 있으며 다른 뷰는 이를 대상으로 할 수 있습니다. 여기서 편집기 페이지는 tabs 영역을 가져오고 각 탭이 이를 채웁니다. 편집기가 닫히면 해당 영역도 함께 제거됩니다.

editor.ts
let tab: 'code' | 'preview' = 'code'

const Editor = () =>
  into('main',
    div(
      nav(
        button('Code', { onClick: () => { tab = 'code'; update() } }),
        button('Preview', { onClick: () => { tab = 'preview'; update() } }),
      ),
      section(region({ id: 'tabs' })),
    ),
    when(() => tab === 'code', into('tabs', textarea()))
      .else(into('tabs', iframe({ src: '/preview' }))),
  )

라이브 예시

region-app.ts
import 'nuclo'

let page: 'home' | 'article' | null = 'home'
let dialog = false

const go = (next: typeof page) => () => {
  page = next
  dialog = false
  update()
}

const Layout = () =>
  div(
    nav(
      button('Home', { onClick: go('home') }),
      button('Article', { onClick: go('article') }),
      button('Close page', { onClick: go(null) }),
    ),
    main(
      region({ id: 'main', type: 'stack', empty: p('Nothing open.') }),
    ),
    aside(region({ id: 'side', empty: p('No related links.') })),
  )

// Pages fill the layout's regions from outside it.
const Home = () =>
  into('main',
    input({ placeholder: 'Type here, then open the dialog' }),
    button('Open dialog', {
      onClick: () => { dialog = true; update() },
    }),
  )

const Article = () =>
  into({
    main: article(h2('Article'), p('One into() fills both regions.')),
    side: [h2('Related'), ul(li('One'), li('Two'))],
  })

// Stacks over Home, so Home keeps its DOM and the text you typed.
const Dialog = () =>
  into('main',
    div(
      p('On top of Home.'),
      button('Close', { onClick: () => { dialog = false; update() } }),
    ),
  )

export const RegionApp = () =>
  div(
    Layout(),
    when(() => page === 'home', Home()),
    when(() => page === 'article', Article()),
    when(() => dialog, Dialog()),
  )
라이브메인 영역을 스택 방식으로 구성하고 보조 영역도 둔 레이아웃을 외부에서 채웁니다. Home에 입력한 뒤 대화 상자를 열었다 닫아도 텍스트는 유지됩니다. Article은 into() 한 번으로 두 영역을 채웁니다.열기 ↗

No related links.

fn공개 API
function forceUpdate(): void function forceUpdate(nodeModFn: NodeModFn | (() => NodeModFn), parent?: Element): ExpandedElement

update()는 동적 값만 실행합니다. h1(labels[lang].title)의 텍스트와 같은 정적 값은 트리가 작성될 때 고정됩니다. forceUpdate()는 컴포넌트를 다시 호출하고 정적 값까지 새 결과와 일치하도록 실제 DOM을 업데이트합니다. 언어 전환과 같이 앱 전반에 걸쳐 드물게 변경하는 경우에 사용하세요. 다른 모든 것에는 update()를 사용하세요.

i18n.ts
const labels = { en: { title: 'Hello' }, pt: { title: 'Olá' } }
let lang: keyof typeof labels = 'en'

const App = () => div(h1(labels[lang].title))
render(App, root)

lang = 'pt'
forceUpdate() // same <h1> element, now "Olá"
라이브update()는 동적인 lang 행만 업데이트합니다. forceUpdate()는 App을 다시 호출해 같은 <h1>을 패치합니다. 명시적인 형식은 페이지 전체가 아닌 이 트리만 다시 구성합니다.열기 ↗

lang = 'en'

Hello

Static text, fixed on build.

인수 없이 호출한 forceUpdate()는 render(App) 또는 hydrate(App)로 등록한 모든 루트를 다시 생성합니다. render(App())로 렌더링한 루트는 건너뜁니다. 컴포넌트에서 오류가 발생하면 오류를 기록하고 해당 루트의 기존 DOM을 유지합니다. 새 결과를 적용하는 도중에 발생한 오류도 기록하지만 이미 반영한 변경 사항은 유지됩니다.

남는 것과 변화하는 것

  • 자식은 태그별로 순서대로 일치됩니다. 일치하는 요소가 재사용되므로 포커스와 입력값이 유지되고 해당 onMount 훅이 다시 실행되지 않습니다. 자식 요소 하나의 태그가 다르면 해당 요소와 그 뒤의 모든 형제 요소를 새로 만듭니다.
  • 텍스트, 속성, 클래스, 스타일, 리스너, when() 분기 및 list() 행은 새 빌드를 따릅니다.
  • 새 요소가 생성되고 해당 onMount 훅이 실행됩니다. 새 빌드에서 더 이상 만들지 않는 요소는 제거되고 해당 onDestroy 훅이 실행됩니다.
  • list() 행도 항목이 아닌 위치를 기준으로 연결하므로 재사용된 행 요소가 결국 다른 항목을 표시할 수 있습니다.
  • 새 빌드에서 더 이상 설정되지 않는 속성은 제거되지 않습니다.
주의 사항: 컴포넌트 함수 내부에 선언된 상태는 함수가 다시 실행되기 때문에 재설정됩니다. 유지해야 하는 상태는 컴포넌트 외부에 두세요.

명시적 호출 형식. forceUpdate(App(), parent)는 parent에서 하나의 트리를 다시 생성하고 해당 루트를 반환합니다. 이후 호출을 위해 루트를 등록하지 않습니다. hydrate()와 마찬가지로 루트가 parent의 첫 번째 노드가 될 것으로 예상합니다. 첫 번째 노드에 다른 태그가 있으면 교체됩니다.

서버에서 forceUpdate()는 아무 작업도 수행하지 않습니다.

render()

#
fn공개 API
function render(nodeModFn: NodeModFn | (() => NodeModFn), parent?: Element, index?: number): ExpandedElement

트리를 생성하고 루트 요소를 parent에 추가합니다. parent의 기본값은 document.body입니다. 루트 요소를 반환합니다.

main.ts
import 'nuclo'
import { App } from './app.ts'

render(App, document.getElementById('app')!)
  • 컴포넌트(render(App)) 또는 컴포넌트가 반환하는 빌더(render(App()))를 전달할 수 있으며 생성되는 DOM은 같습니다. 나중에 forceUpdate()로 루트를 다시 생성할 수 있도록 render(App)를 권장합니다.
  • 기존 내용 뒤에 추가합니다. parent를 비우지 않으므로 두 번 호출하면 두 개의 복사본이 추가됩니다.
  • render()가 반환되면 트리의 모든 onMount가 실행되었습니다.
  • 문서에 있는 요소로 렌더링합니다. update()를 참조하세요.
  • 루트는 when() 또는 list()가 아닌 div(...)와 같은 요소여야 합니다.
  • index는 내부용 매개변수이며 요소 삽입 위치를 지정하는 값이 아닙니다. 기본값을 그대로 사용하세요.

render(App)로 전달된 컴포넌트는 매개 변수를 사용하지 않아야 합니다. 필수 매개변수는 타입 오류입니다. 선택적 매개변수는 타입 검사를 통과하지만 런타임에 render()에서 오류가 발생합니다. props를 사용하는 컴포넌트를 래핑합니다.

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

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

hydrate()

#
fn공개 API
function hydrate(nodeModFn: NodeModFn | (() => NodeModFn), parent?: Element): ExpandedElement

renderToString()로 생성한 HTML에 클라이언트 동작을 연결합니다. 기존 요소를 재사용하고, 이벤트 리스너를 연결하고, 동적 값, when() 및 list()를 연결하므로 나중에 update()가 작동합니다. 그런 다음 onMount 훅을 실행합니다. parent의 기본값은 document.body입니다. 루트 요소를 반환합니다.

client.ts
import 'nuclo'
import { App } from './app.ts'

hydrate(App, document.getElementById('root')!)
  • 서버와 동일한 컴포넌트, 동일한 데이터를 사용하세요.
  • 전용 컨테이너에서 하이드레이션하세요. 공백은 괜찮지만 앱의 루트는 첫 번째 노드여야 합니다. 다른 노드가 먼저 오면 Nuclo는 해당 노드를 교체하고 서버 복사본을 남겨 둡니다.
  • render()와 마찬가지로 App()가 아닌 App를 전달하므로 forceUpdate()는 나중에 루트를 다시 빌드할 수 있습니다. App는 매개변수를 사용하지 않아야 합니다. 선택적 매개변수가 있는 경우에도 hydrate()는 아무것도 연결하지 않은 채 종료합니다. props를 사용하는 컴포넌트를 래핑합니다: hydrate(() => Page(props), root).
  • 서버에서 렌더링한 HTML이 없으면 render()를 사용하세요.

서버와 클라이언트가 다른 경우

클라이언트의 결과를 기준으로 맞춥니다.

  • 다른 루트 태그: 해당 노드는 새로운 DOM로 대체됩니다. 빈 컨테이너: 새로운 DOM이 추가됩니다.
  • 다른 텍스트는 수정됩니다. 서로 다른 list() 항목과 when() 분기가 클라이언트를 따릅니다.
  • 서버 HTML에만 있는 자식 요소는 제거되고 누락된 자식 요소는 생성됩니다.
  • 서버에서 작성했지만 클라이언트에서 설정하지 않은 속성은 그대로 유지됩니다.

renderToString()는 HTML 주석을 각 텍스트 노드 앞에 <!-- text-0 -->와 같은 마커로 씁니다. hydrate()에는 주석이 필요하므로 서버 HTML에서 주석을 제거하지 마십시오. 마커가 없으면 하이드레이션은 list() 행을 삭제하고 list() 및 when() 업데이트를 중지합니다.

서버 렌더링

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

서버에서 컴포넌트를 HTML 문자열로 렌더링합니다. 브라우저에서 동일한 컴포넌트로 hydrate()를 호출하여 HTML을 대화형으로 만듭니다. 서버 런타임에는 document가 없으므로 먼저 nuclo/polyfill를 가져옵니다.

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

export const App = () =>
  main(
    h1(heading, () => `Count: ${count}`),
    button({ onClick: () => { count++; update() } }, '+1'),
  )
server.ts
import 'nuclo/polyfill'
import 'nuclo'
import { renderToString, getCssText } from 'nuclo/ssr'
import { App } from './app.ts'

const html = renderToString(App)
const page = `<!doctype html>
<html>
  <head><style id="nuclo-styles">${getCssText()}</style></head>
  <body>
    <div id="root">${html}</div>
    <script type="module" src="/client.js"></script>
  </body>
</html>`
client.ts
import 'nuclo'
import { App } from './app.ts'

hydrate(App, document.getElementById('root')!)

스타일 태그에 id="nuclo-styles"를 유지합니다. 클라이언트는 해당 요소를 재사용하고 새 규칙만 추가합니다. getCssText()를 참조하세요.

서버에서 실행되는 것

  • 각 renderToString() 호출은 트리를 구축하고 각 동적 값을 한 번 실행합니다.
  • 이벤트 핸들러, onMount 및 onDestroy는 실행되지 않습니다. update(), scope() 및 forceUpdate()는 아무것도 수행하지 않습니다.
  • 렌더링은 동기식입니다. 먼저 데이터를 로드한 다음 렌더링하세요.
  • 렌더링 후에는 등록된 상태가 유지되지 않으므로 하나의 프로세스가 여러 요청을 처리할 수 있습니다. 요청 데이터를 renderToString(Page(user))와 같은 인수로 전달합니다. 모듈 최상위 변수는 모든 요청에서 공유됩니다.
  • 서버에서 render()를 호출하지 마십시오. 공유 document.body에 추가됩니다.

renderToString()

빌더(App()), 컴포넌트(App) 또는 DOM 노드를 사용합니다. null 및 undefined는 ""를 반환합니다. 루트는 요소여야 합니다. renderToString(Page)로 전달된 컴포넌트는 매개 변수를 사용하지 않아야 하므로 데이터를 사용하는 컴포넌트에는 Page(user)를 전달합니다.

example.ts
renderToString(p('Hello'))
// '<p><!-- text-0 -->Hello</p>'
  • 텍스트 및 속성 값은 HTML로 이스케이프됩니다. script() 및 style() 내부의 텍스트는 닫는 태그만 분리된 상태로 있는 그대로 작성되므로 사용자 입력을 거기에 넣지 마십시오.
  • 호출자에게 예외를 던지지 않습니다. 컴포넌트, list() 함수 또는 when() 조건에서 오류가 발생하거나 폴리필이 누락된 경우 오류를 기록하고 ""를 반환합니다. 렌더링 실패 시 요청도 실패로 처리해야 한다면 결과가 비어 있는지 확인하세요. 텍스트나 속성 함수에서 오류가 발생하면 해당 부분만 비어 있게 렌더링됩니다.

renderManyToString()

inputs.map(renderToString)와 동일합니다.

renderToStringWithContainer()

출력을 하나의 요소로 래핑합니다. containerTag의 기본값은 "div"이고, containerAttrs는 {}입니다. 속성 값이 이스케이프됩니다. 속성 이름은 주어진 대로 작성하므로 class와 같은 HTML 이름을 사용합니다. 사용자 입력을 태그로 전달하지 마십시오.

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

nuclo/polyfill

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

Nuclo가 Node.js 및 Bun에서 트리를 만들 수 있게 해 주는 경량 DOM 구현입니다. 첫 렌더링 전에 한 번 가져오세요. 대부분의 앱에서는 부수 효과 import만 사용하면 됩니다.

  • window가 없을 경우 globalThis.document, Node, Element, HTMLElement를 설정하되, 누락된 것만 설정합니다. window를 생성하지 않습니다.
  • 브라우저나 jsdom에서는 아무 작업도 수행하지 않습니다.
  • Event 및 CustomEvent는 런타임 자체 생성자입니다. Node, Element 및 HTMLElement는 폴리필 클래스의 별칭입니다.

브라우저가 아닌 트리 구축용 DOM입니다. innerHTML 및 getElementById가 없으며 querySelector()는 null를 반환하고 이벤트 리스너는 무시됩니다. renderToString()를 사용하여 HTML을 얻으세요.

css()

#
fn공개 API
function css(style: Style): StyleResult function css(name: string, style: Style): StyleResult type StyleResult = { className: string }

타입이 지정된 스타일 객체를 생성된 CSS 클래스로 변환합니다. 결과를 태그 빌더에 전달합니다. 브라우저에서 규칙은 하나의 <style id="nuclo-styles"> 요소로 들어갑니다. 서버에서는 getCssText()가 이를 반환합니다.

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

const Card = () => div(card, 'Simple card')
  • css()는 기본 선언에 대해 하나의 클래스를 만들고 각 가상 선택자 키, 선택자 및 미디어 블록에 대해 하나의 클래스를 만듭니다. 위의 카드는 두 가지 클래스를 얻습니다.
  • 클래스 이름은 콘텐츠의 해시입니다. 동일한 스타일은 클래스를 공유하며 서버와 클라이언트는 동일한 이름을 생성합니다.
  • 모듈 최상위에서 스타일을 한 번 정의합니다. css()는 객체별로 캐시하므로 호출할 때마다 새 객체가 매번 다시 처리됩니다.
  • 스타일 객체를 css()에 전달한 후 변경하면 아무 작업도 수행되지 않습니다.
  • 모든 스타일은 페이지 또는 서버 프로세스의 수명 동안 등록된 상태로 유지됩니다. 데이터의 너비와 같이 항목별로 변경되는 값의 경우 style 속성을 사용하세요.

명명된 스타일

읽을 수 있는 클래스 이름을 얻으려면 먼저 이름을 전달하세요.

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

pageTitle.className // "page-title page-title-<hash>"
  • 기본 클래스는 바로 이름입니다. 다른 블록은 name-<hash>를 얻습니다.
  • 이름은 유효한 클래스 이름이어야 합니다. 그렇지 않으면 Nuclo가 경고하고 생성합니다.
  • 이름은 전역적입니다. 두 가지 다른 스타일이 동일한 이름을 사용하는 경우 Nuclo가 경고합니다.

결과 사용

example.ts
const card = css({ p: 8 })

div(card)                           // best
div(card, { className: 'featured' }) // className merges
div(card, { class: 'featured' })     // replaces: only "featured" is left

String(card) 및 card.className는 모두 클래스 이름을 반환합니다.

스타일 객체

#

css()에 전달하는 객체는 camelCase의 일반적인 CSS 속성, 약식 집합 및 가상 상태, 선택자 및 미디어 쿼리에 대한 중첩 키를 사용합니다. TypeScript는 모든 항목을 자동 완성합니다. borderTopLeftRadius, fill 또는 paddingInline와 같이 타입 정의에 없는 속성은 raw에 들어갑니다(아래 참조).

값

  • 숫자에는 px가 붙습니다. 0과 단위 없는 속성(zIndex, opacity, fontWeight, lineHeight, flex, flexGrow, flexShrink, order, aspectRatio, gridColumn, gridRow)은 예외입니다. raw 안의 zoom, scale, column-count, orphans, widows, tab-size, animation-iteration-count에도 단위가 붙지 않습니다.
  • 문자열은 작성된 대로 사용됩니다. null, undefined 및 false는 건너뜁니다.
  • lineHeight(및 leading)는 단위가 없으므로 leading: 24는 글꼴 크기의 24배를 의미합니다. 픽셀 단위라면 "24px"를 작성하세요.
example.ts
css({ p: 16, m: 0, opacity: 0.5, leading: 1.5, z: 10, w: '50%' })
// padding:16px; margin:0; opacity:0.5; line-height:1.5; z-index:10; width:50%

축약형

키CSS
p, pt, pr, pb, pl패딩 및 각 사이드
px / py좌우 패딩 / 상하 패딩
m, mt, mr, mb, ml, mx, my여백도 동일
w, h, minW, maxW, minH, maxH폭, 높이 및 최소 및 최대
size폭 및 높이
bg배경
text / font / weightfont-size / font-family / font-weight
leading / trackingline-height / letter-spacing
align / items / justifytext-align / align-items / justify-content
z / rounded / shadow / selectz-index / border-radius / box-shadow / user-select

이 키에는 true가 사용됩니다.

키CSS
rowdisplay: flex; flex-direction: row
coldisplay: flex; flex-direction: column
centeralign-items: center; justify-content: center (display는 설정하지 않음)
truncateoverflow: hidden; text-overflow: ellipsis; white-space: nowrap

가상 상태

hover, focus, focusVisible, focusWithin, active, visited, disabled, enabled, checked, required, invalid, valid, readOnly, first(:first-child), last(:last-child), only(:only-child), odd, even, empty, placeholderShown, placeholder, before, after, selection, marker, firstLine 및 firstLetter. 중첩할 수도 있습니다: hover: { focus: {…} }는 :hover:focus를 제공합니다.

선택자 및 at-규칙

&로 시작하는 키는 선택자이며 각 &는 클래스가 됩니다. @로 시작하는 키는 at-rule(@media, @container 또는 @supports)입니다.

menu.ts
const menu = css({
  p: 8,
  '& > li': { py: 4 },
  '& > li:hover': { color: 'red' },
  '&:is(.dark *)': { color: 'white' },
  '@media (min-width: 768px)': { p: 16 },
})
  • 키는 &로 시작해야 합니다. ".dark &"와 같은 키는 무시되므로 "&:is(.dark *)"를 작성하세요.
  • 두 개의 중첩된 @media 블록이 and와 결합됩니다. 문법이 처음에만 타입을 허용하기 때문에 미디어 타입(print, only screen)이 이끄는 쿼리가 먼저 배치됩니다. 평면화할 수 없는 쌍(두 개의 미디어 타입 또는 선행 not)은 경고하고 내부 쿼리만 사용합니다. 다른 중첩된 at-규칙은 외부 규칙을 대체합니다.

raw

임의의 CSS 속성은 raw에 CSS 이름 그대로 작성할 수 있습니다. 숫자에는 여기서도 px가 붙으므로 단위를 직접 지정하려면 문자열을 전달하세요.

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

다른 일반 키는 케밥 케이스로 변환되어 그대로 작성되므로 colr: 'red'와 같은 오타는 브라우저가 삭제하는 선언이 됩니다. 가상 상태, screen 설정, & 또는 @ 키에 해당하지 않는 키 아래의 중첩 객체는 무시됩니다. TypeScript는 둘 다 보고합니다.

fn공개 API
function cx(...inputs: ClassInput[]): StyleResult type ClassInput = StyleResult | string | false | null | undefined | ClassInput[]

스타일을 결합합니다. 두 입력이 동일한 속성을 설정하면 나중에 입력이 우선합니다.

example.ts
const base = css({ p: 8, color: 'red' })
const blue = css({ color: 'blue' })

cx(base, blue)                  // padding: 8px; color: blue
cx(base, false, null, 'extra')  // base's classes plus "extra"
div(cx(base, blue), 'Blue text')
  • falsy 값을 건너뛰고, 배열을 평면화하고, 문자열을 분할하고 중복을 제거합니다.
  • 동일한 블록(기본, 동일한 가상 선택자, 동일한 미디어 쿼리)에 대해 생성된 클래스가 하나의 새로운 클래스로 병합됩니다. 다른 클래스 이름은 변경되지 않고 전달됩니다.
  • cx()만이 인수 순서를 알고 있습니다. div(base, blue)를 사용하면 두 클래스가 모두 추가되고 마지막에 생성된 규칙이 적용됩니다. 하나의 cx() 호출에 서로 재정의되는 스타일을 넣습니다.
  • 명명된 스타일의 블록이 다른 입력의 블록과 병합될 때 병합된 클래스는 기본 이름이 아닌 name-<hash>입니다. 병합되지 않는 블록은 클래스를 유지하므로 cx(pageTitle, 'extra')에는 여전히 page-title가 있습니다.

동적 클래스

항상 cx(...)를 반환하는 함수를 전달합니다.

toggle.ts
const baseButton = css({ px: 12, py: 8, border: '1px solid #ccc' })
const activeButton = css({ bg: '#ff3f00', color: 'white' })

let active = false

const Toggle = () =>
  button(
    () => cx(baseButton, active && activeButton),
    () => (active ? 'Active' : 'Inactive'),
    { onClick: () => { active = !active; update() } },
  )

함수는 처음 실행 시 스타일 결과를 반환해야 합니다. () => active && activeButton는 클래스 대신 "false" 텍스트를 렌더링합니다.

createCss()

#
fn공개 API
function createCss<const T extends ThemeConfig>(theme?: T): CssInstance<T> // CssInstance<T> = { css, cx, variants, keyframes, globalStyle, theme }

테마에 바인딩된 자체 css, cx, variants, keyframes 및 globalStyle를 반환합니다. 테마는 값에 이름을 부여합니다. 아래 테마를 사용하면 borderColor: 'border'가 #e5e7eb를 쓰고 TypeScript가 이름을 자동 완성합니다. screens의 각 항목은 md: {…}와 같은 미디어 쿼리의 스타일 키가 됩니다.

theme.ts
export const { css, cx, variants } = createCss({
  colors: { primary: '#ff3f00', border: '#e5e7eb' },
  radii: { card: '12px' },
  screens: { md: '(min-width: 768px)' },
})

const panel = css({
  p: 12,
  rounded: 'card',
  border: '1px solid',
  borderColor: 'border',
  md: { p: 24 },
})
테마 키사용 대상
colorsbg, backgroundColor, color, borderColor 및 각 측면, outlineColor, caretColor, textDecorationColor
fontsfont, fontFamily
shadowsshadow, boxShadow
radiirounded, borderRadius
screensmd: {…}와 같은 새로운 스타일 키
  • 테마 이름은 값 전체가 토큰 이름일 때만 치환됩니다. border: "1px solid primary"는 치환되지 않습니다.
  • @가 없는 screen 설정값은 @media <value>가 됩니다. @로 시작하는 값은 작성된 대로 사용됩니다(예: "@container (min-width: 400px)").
  • screen 설정은 createCss()로 만든 인스턴스에만 존재합니다. 전역 css()에는 screen 설정이 없지만 @media 키를 허용합니다.
  • 모든 인스턴스와 전역 도우미는 하나의 스타일시트를 공유합니다. 스크린 미디어 규칙은 스크린을 사용하는 순서가 아닌 선언된 순서대로 출력되므로 min-width 스크린을 작은 것부터 큰 것 순으로 나열합니다. 앱당 하나의 createCss() 인스턴스를 사용하여 해당 순서가 테마의 순서가 되도록 합니다.
  • 반환값의 theme는 전달한 테마 객체입니다.

variants()

#
fn공개 API
function variants(config: { base?: Style variants?: { [group: string]: { [value: string]: Style } } defaultVariants?: { [group: string]: value } compoundVariants?: ({ [group: string]: value } & { css: Style })[] }): (props?: { [group: string]: value }) => StyleResult

버튼의 의도 및 크기와 같은 옵션 세트에 대한 스타일을 반환하는 함수를 빌드합니다.

button.ts
const buttonClass = variants({
  base: { px: 12, py: 8, rounded: 6 },
  variants: {
    intent: { primary: { bg: '#2563eb' }, danger: { bg: '#dc2626' } },
    size: { sm: { text: 12 }, lg: { text: 18 } },
    block: { true: { w: '100%' } },
  },
  defaultVariants: { intent: 'primary', size: 'sm' },
  compoundVariants: [{ intent: 'danger', size: 'lg', css: { weight: 700 } }],
})

button(buttonClass(), 'Save')                                                // primary, sm
button(buttonClass({ intent: 'danger', size: 'lg', block: true }), 'Delete') // bold, full width
  • 스타일은 base, 각 그룹에서 선택한 값, 일치하는 각 복합 조건 순서로 병합됩니다. 속성마다 나중에 전달한 스타일이 우선합니다.
  • 생략하거나 undefined 또는 null로 전달한 옵션은 기본값을 사용합니다. 기본값이 없는 그룹은 건너뜁니다.
  • true 및 false 키만 있는 그룹은 불리언을 사용합니다.
  • 나열된 모든 그룹이 일치할 때 복합 조건의 스타일이 적용됩니다(기본값 포함).
  • 각 선택 항목은 한 번만 작성되고 캐시됩니다.

keyframes() 및 globalStyle()

#
fn공개 API
function keyframes(frames: KeyframeFrames): string function globalStyle(selector: string, style: FlatStyle): void

keyframes()는 애니메이션을 등록하고 생성된 이름을 반환합니다. 프레임 키는 from, to 및 백분율(예: "50%" 또는 "0%, 100%")입니다. 프레임은 평면 속성만 사용합니다. 동일한 프레임은 동일한 이름을 반환합니다.

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

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

globalStyle()는 모든 선택자에 대한 규칙을 추가합니다.

reset.ts
globalStyle('*, *::before, *::after', { boxSizing: 'border-box' })
globalStyle('body', { m: 0, font: 'system-ui, sans-serif' })
  • 선택자는 작성된 대로 정확하게 사용됩니다.
  • 평면 속성만 작동합니다. 가상 선택자 키, screen 설정 및 @ 키는 무시되므로 globalStyle()는 미디어 규칙을 작성할 수 없습니다.
  • 동일한 규칙은 한 번만 추가됩니다. 같은 선택자에 다른 스타일을 지정하면 규칙이 하나 더 추가되고 나중 규칙이 우선합니다.

getCssText() 및 resetStyles()

#
fn공개 API
function getCssText(): string // from 'nuclo/ssr' or 'nuclo' function resetStyles(): void // from 'nuclo'

getCssText()는 이 프로세스에서 css(), cx(), variants(), keyframes() 및 globalStyle()가 생성한 모든 규칙을 반환합니다. 기본 규칙을 먼저 반환한 다음 @media, @container 및 @supports 블록. 서버 렌더링에 사용합니다.

  • renderToString() 이후에 호출하세요. 렌더링 중에 생성된 규칙이 포함됩니다.
  • 하나의 페이지나 요청이 아닌 전체 프로세스의 규칙을 포함하며, 규칙은 계속 누적됩니다. 클래스 이름은 콘텐츠 해시이므로 안전합니다.
  • <style id="nuclo-styles">에 넣으세요. 클라이언트는 해당 요소를 재사용하고 이미 포함된 규칙을 건너뜁니다. 다른 ID를 사용하거나 ID를 생략하면 클라이언트는 모든 규칙을 담은 스타일시트를 하나 더 추가합니다.
  • 텍스트는 이스케이프되지 않으므로 신뢰할 수 없는 입력을 스타일 값에 넣지 마십시오.

resetStyles()는 테스트 도우미입니다. 모든 규칙과 #nuclo-styles 요소를 제거합니다. 이전에 생성한 스타일은 클래스 이름을 유지하지만 CSS를 잃습니다. 앱이나 서버에서 호출하지 마세요.

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

beforeEach(() => resetStyles())

nuclo-router

#
별도 패키지: nuclo-router는 Nuclo 코어 패키지의 일부가 아닙니다. nuclo 옆에 설치하세요. 미리보기 릴리스이므로 API가 여전히 변경될 수 있습니다.

Nuclo용 라우팅 라이브러리로, list()와 update()를 기반으로 서버 렌더링, 하이드레이션, 코드 분할 라우트, 유휴 시간 사전 로드를 제공합니다. gzip 압축 시 약 4.9 KB이며 Nuclo 외에는 의존성이 없습니다. 부수 효과가 없어 사용하지 않으면 트리 셰이킹으로 제거됩니다.

terminal
$ npm install nuclo nuclo-router

또는 동일한 두 이름으로 pnpm add, yarn add 또는 bun add를 사용합니다. 피어 의존성으로 nuclo 0.3 이상이 필요합니다.

라우터는 무엇을 로드할지 결정하고, 앱은 router.outlet()으로 어디에 표시할지 지정합니다. 레이어 스택을 하나의 list()로 렌더링하며 레이어마다 행이 하나씩 있으므로 일반 페이지는 행 하나입니다. 서버와 클라이언트가 동일한 트리를 만들기 때문에 hydrate()는 서버 노드를 교체하지 않고 재사용합니다. 나머지는 history, 위임된 click 리스너 하나, requestIdleCallback으로 구성됩니다.

한 번 정의하고 어디서든 가져오기

main.ts
export const router = createRouter(routes)   // define once, import anywhere
await router.start()                          // resolve the current URL
render(App, document.getElementById('app')!) // App places router.outlet() and reads router.path, router.go(), …

start()는 일치하는 라우트의 모듈을 모두 확인한 뒤 완료되므로 서버와 클라이언트 모두 첫 번째 트리를 완성된 상태로 만듭니다. 이후에는 라우터가 시작한 Route로 접근을 전달하므로 router.path, router.pending, router.go()로 해당 Route를 사용합니다.

전체 앱

routes.ts
// isomorphic: imported by the browser and the server
import { createRouter } from 'nuclo-router'

export const router = createRouter({
  '/':           () => import('./pages/Home.ts'),
  '/docs':       () => import('./pages/Docs.ts'),
  '/blog/:slug': () => import('./pages/Post.ts'),
  '*':           () => import('./pages/NotFound.ts'),
})

export const App = () =>
  div(
    header(a({ href: router.href('/') }, 'Home'), a({ href: router.href('/docs') }, 'Docs')),
    main(
      when(() => router.pending, Spinner()),
      when(() => router.error !== null, ErrorView()),
      router.outlet(),   // where the pages land
    ),
    footer('© nuclo'),
  )
pages/Post.ts
import type { RouteContext } from 'nuclo-router'
import { router } from '../routes.ts'

// A page is plain content
export default function Post(ctx: RouteContext) {
  // A query-only change keeps the page, so the query is read live.
  return article(h1(ctx.params.slug), p(() => (router.search.has('preview') ? 'draft' : '')))
}
main.ts
import 'nuclo'
import { router, App } from './routes.ts'

await router.start()
hydrate(App, document.getElementById('app')!)   // or render() without SSR
server.ts
import 'nuclo/polyfill'
import 'nuclo'
import { renderToString } from 'nuclo/ssr'
import { router, App } from './routes.ts'

// one Route per request
const route = await router.start(request.url)
const html = route.run(() => renderToString(App))

이것이 전체 통합입니다. <a href>는 단지 <a href>이기 때문에 링크가 작동합니다. document의 위임된 리스너 중 하나는 라우터가 소유한 링크를 가로채고 다른 모든 링크를 브라우저에 남겨둡니다.

서버에서 start()는 요청마다 Route를 반환하고 라우터 자체는 변경하지 않으므로 동시 요청이 서로의 라우트를 참조하지 않습니다. route.run()은 렌더링하는 동안 해당 Route를 라우터의 활성 라우트로 설정하며, 아웃렛도 이 Route를 읽습니다. 브라우저에서는 페이지 로드당 Route가 하나이고 start()가 이를 라우터의 활성 Route로 설정하므로 run()이 필요하지 않습니다.

페이지는 RouteContext 및 PageProps를 (ctx, { layer, data, outlet, beforeLeave })로 수신하며 더 적은 인수를 사용할 수 있습니다. list()가 허용하는 모든 것을 반환합니다. 레이아웃의 다른 부분도 채우려면 페이지 내부에서 div(article(…), into('sidebar', nav(…)))와 같은 into()를 반환하세요. 해당 콘텐츠는 페이지와 함께 사라집니다. 페이지 & 영역를 참조하세요.

패턴 및 중첩 라우트

#
패턴일치 대상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 처리){ '*': '…' }

정적 패턴이 우선하고, 다음은 선언 순서에 따른 동적 패턴, 마지막은 접두사가 긴 순서의 catch-all 패턴입니다. 따라서 '*'는 다른 패턴과 일치하지 않는 경로만 처리하며 테이블 어디에 선언해도 됩니다. 후행 슬래시, 중복 슬래시, 퍼센트 인코딩은 정규화되고 매개변수 값은 디코딩되어 전달됩니다. 패턴별로 세그먼트를 비교하며 정규식을 생성하거나 실행하지 않습니다.

상대 경로

./로 시작하는 패턴은 선언된 상위 패턴을 기준으로, ./로 시작하는 href는 활성 라우트를 기준으로 해석됩니다. 이를 함께 사용하면 어떤 상위 경로에 배치해도 동일하게 동작하는 섹션을 만들 수 있습니다. 테이블의 값은 로더 또는 상대 경로를 키로 사용하는 중첩 테이블입니다.

records/routes.ts
import type { RouteTable } from 'nuclo-router'

// a section, with no idea where it lives
export const recordSection: RouteTable = {
  '/':             () => import('./Record.ts'),   // the parent path itself
  './preview':     () => import('./Preview.ts'),
  './preview/raw': () => import('./Raw.ts'),
}
routes.ts
// declare it wherever it belongs
createRouter({
  '/': () => import('./pages/Home.ts'),
  '/invoices/:id':  recordSection,
  '/customers/:id': recordSection,
  '*': () => import('./pages/NotFound.ts'),
})

이 두 줄은 경로를 두 번 쓰지 않고 6개 패턴을 모두 생성합니다. 두 상위 경로 모두 동일한 로더 객체를 참조하므로 하나의 모듈 캐시와 하나의 import()를 공유합니다.

  • 하위 테이블 내부에서 './x', '/x' 및 'x'는 모두 같은 의미이고, '/'는 상위 경로 자체를 의미하며, '*rest'는 섹션에 자체 catch-all을 제공합니다. 중첩은 원하는 만큼 깊어집니다.
  • 최상위 키는 작성된 대로 사용되므로 '*'는 '*'를 유지합니다.
  • *catch-all 부모 아래의 자식은 createRouter() 오류입니다. catch-all은 패턴의 마지막에 와야 하기 때문입니다.

상위 페이지 및 outlet()

부모와 자식 페이지는 실제 계층 구조를 이룹니다. 부모 페이지가 렌더링되고, 자식 페이지는 부모가 outlet()을 호출한 위치에 렌더링됩니다.

records/Record.ts
import type { PageProps, RouteContext } from 'nuclo-router'

// a parent page takes its outlet from (ctx, { outlet })
export default function Record(ctx: RouteContext, { outlet }: PageProps) {
  return div(Header(ctx), input({ id: 'notes' }), main(outlet()))
}

하위 페이지 사이를 이동해도 상위 페이지는 다시 생성되지 않습니다. 같은 DOM 노드를 유지하므로 포커스, 스크롤 위치, 입력 중인 텍스트, 클로저의 모든 상태가 그대로 유지됩니다.

  • 하위 라우트가 없는 페이지의 아웃렛은 비어 있으므로 레이아웃에서 조건 없이 호출할 수 있습니다. 하위 라우트가 있는데 상위 페이지가 outlet()을 호출하지 않으면 하위 페이지는 표시되지 않습니다.
  • 인덱스 경로가 없습니다. '/'는 레이아웃 자체이므로 상위 경로는 내부에 별도의 인덱스 페이지가 아닌 빈 아웃렛이 있는 레이아웃을 렌더링합니다. 랜딩 콘텐츠를 레이아웃에 넣거나 실제 하위 경로를 제공하세요.

따라서 같은 호출이 어느 부모 아래에서든 동작하며, 페이지가 자신의 부모를 몰라도 사용할 수 있습니다.

example.ts
router.href('./preview')      // '/invoices/42/preview' or '/customers/7/preview'
await router.go('./preview')
await layer.push('./preview') // the usual shape: a child layer

./x, ../x, ., ..는 URL의 디렉터리가 아니라 현재 라우트의 경로를 기준으로 해석됩니다. /invoices/42에서 ./preview는 /invoices/42/preview입니다. 브라우저의 기본 규칙으로는 /invoices/preview가 되며, URL 끝의 슬래시는 라우터의 결과에 영향을 주지 않습니다. ../는 세그먼트 단위로 상위 경로로 이동하되 루트에서 멈춥니다. 쿼리와 해시는 유지되고 base가 다시 붙으며, 해석된 절대 경로가 히스토리에 기록됩니다. React Router의 <Link to="./x">도 같은 방식으로 URL 표준과 다른 경로 해석을 적용합니다.

  • ./ 및 ../만 경로 기준입니다. href의 기본 preview는 일반 브라우저 규칙에 따릅니다.
  • href="./preview"를 직접 작성하는 대신 router.href('./preview')로 상대 링크를 구축하세요. href()는 렌더링 시 확인되므로 제공되는 마크업은 절대적이며 크롤러, 중간 클릭 및 JavaScript가 비활성화된 경우에도 여전히 정확합니다.

유지되는 것과 재구축되는 것

노드탐색 중
패턴과 경로가 바뀌지 않은 상위 페이지유지: 동일한 노드를 재사용하며 load()도 다시 실행하지 않습니다.
일치한 페이지패턴과 경로가 같고 load()가 없으면 유지됩니다. 쿼리나 해시만 바뀌어도 유지됩니다. 로더가 있는 라우트는 방문할 때마다 새로 생성됩니다.
경로가 바뀐 상위 페이지해당 페이지와 그 아래 모든 페이지를 다시 생성합니다.
추가한 레이어일치한 페이지만 렌더링합니다. 상위 페이지는 이미 아래쪽 행에 마운트되어 있습니다.

유지된 페이지의 ctx는 생성 당시의 컨텍스트입니다. 문서 전체에서 ctx는 이 의미로 사용합니다. 현재 값은 router.*에서 읽으세요. 필터, 페이지 번호, 탭처럼 쿼리로 관리하는 상태는 동적 값 안에서 router.search를 읽고 router.go('?page=2', { replace: true })로 변경합니다. 페이지와 포커스, 입력 중인 텍스트는 유지됩니다.

페이지 및 영역

#

router.outlet()은 페이지를 한 위치에 표시합니다. 페이지가 레이아웃의 다른 위치에도 사이드바, 도구 모음 또는 제목을 표시해야 한다면 into()로 해당 영역을 채웁니다. 레이아웃은 region()로 장소를 표시하고 페이지는 자체 콘텐츠 옆에 into()를 반환합니다. 레이아웃은 페이지 콘텐츠를 props로 받지 않으며 탐색 시에도 다시 생성되지 않습니다.

app.ts
export const App = () =>
  div(
    aside(
      input({ placeholder: 'Filter' }),   // keeps its text across navigations
      region({ id: 'sidebar', empty: p('Nothing to show.') }),
    ),
    main(router.outlet()),
  )
pages/Post.ts
import type { PageProps, RouteContext } from 'nuclo-router'

export default function Post(_ctx: RouteContext, { data: post }: PageProps<Post>) {
  return div(
    article(h1(post.title), p(post.body)),
    into('sidebar', nav(h2('In this post'), ul(list(() => post.headings, (h) => li(h))))),
  )
}

페이지의 콘텐츠는 아웃렛의 list() 행이며 into()는 이를 보유하는 행만큼 오래 유지됩니다. 따라서 사이드바 콘텐츠는 페이지와 함께 도착하고 페이지와 함께 떠납니다. into('sidebar', …) 없이 페이지로 이동하면 해당 영역의 empty 콘텐츠가 다시 표시됩니다. 두 부분을 같은 함수에서 생성하므로 페이지에서는 두 부분 모두에 대해 ctx 및 data를 사용할 수 있습니다.

내비게이션에서 발생하는 현상

  • 페이지가 유지되면 영역도 유지됩니다. 페이지를 유지하는 쿼리·해시 변경은 영역 콘텐츠도 변경하지 않습니다. 사이드바가 쿼리에 의존한다면 동적 값 안에서 router.search를 읽으세요.
  • 재구성된 페이지는 이를 다시 채웁니다. 경로가 변경되면 이전 행이 제거되고 새 페이지의 into() 보기가 이어받습니다. 아무것도 남지 않았습니다.
  • 푸시 레이어는 뷰를 추가합니다. 레이어는 아웃렛의 나중 행이므로 into()는 동일한 영역의 새로운 뷰입니다. 기본 type: 'latest'를 사용하면 레이어가 닫힐 때까지 페이지 보기를 덮고 페이지 보기가 다시 표시됩니다. type: 'stack'를 사용하면 둘 다 표시됩니다.
  • 상위 페이지도 영역을 채울 수 있습니다. 상위 페이지가 다시 생성되지 않기 때문에 해당 하위 페이지가 변경되는 동안 해당 뷰는 그대로 유지됩니다.
  • 서버 렌더링에서도 그대로 사용할 수 있습니다. 서버는 route.run() 안에서 페이지를 생성하므로 into()의 뷰도 HTML에 포함됩니다. hydrate()는 이 노드를 재사용합니다.

한 페이지의 여러 영역

한 번에 두 개 이상의 영역을 채우려면 객체를 전달하세요. 페이지의 주요 콘텐츠는 여전히 아웃렛을 통과하므로 into() 옆에 반환합니다.

pages/Report.ts
export default function Report(ctx: RouteContext) {
  return div(
    table(…),
    into({
      toolbar: [button('Export'), button('Print')],
      sidebar: nav(a({ href: router.href('./summary') }, 'Summary')),
    }),
  )
}

어떤 작업에 어떤 도구가 필요합니까?

  • outlet()는 하위 경로용입니다. URL이 결정하는 콘텐츠는 상위 페이지 내부에 렌더링됩니다. 중첩 경로를 참조하세요.
  • into()는 레이아웃 전체에 퍼지는 일치하는 페이지용입니다. 콘텐츠는 페이지에 속하며 해당 데이터를 사용합니다.
  • matches()는 페이지가 포함되지 않고 자체 작은 테이블을 사용하여 URL만으로 레이아웃을 결정하는 데 사용됩니다. matches()를 참조하세요.

이들 모두는 일치하는 하나의 경로에 속합니다. 라우터는 하나의 URL에서 두 개의 독립적인 영역을 구동하지 않으므로 자체 경로가 있는 사이드바는 두 번째 아웃렛이 아니라 페이지가 채우는 영역입니다. 자체 라우터, 로딩 및 폴백을 원하는 사이드바는 하나를 시작하지 않은 상태로 유지하고 match()로 해결합니다. 다중 라우터 & 결합 로딩를 참조하세요.

라우트 로더 및 코드 분할

#

페이지 모듈은 load 함수를 내보낼 수 있습니다. 해당 라우트로 이동할 때마다 페이지를 만들기 전에 실행되며 반환값은 data로 페이지에 전달됩니다.

pages/Post.ts
import type { DataLoader, PageProps, RouteContext } from 'nuclo-router'

export const load: DataLoader<Post> = (ctx) => fetchPost(ctx.params.slug)

export default function Post(_ctx: RouteContext, { data: post }: PageProps<Post>) {
  return article(h1(post.title), p(post.body))
}

경로 테이블은 변경되지 않았습니다. 로더는 페이지와 함께 이동합니다. 일치하는 RouteContext를 가져오므로 params, search 및 path를 모두 사용할 수 있습니다. 동기식 또는 비동기식일 수 있으며 서버에서도 실행되므로 서버 렌더링 페이지는 하이드레이션 후 가져오는 대신 이미 HTML에 있는 데이터와 함께 제공됩니다.

모듈은 캐시되지만 데이터는 캐시되지 않습니다. 경로의 청크는 한 번 가져와 재사용되며 해당 로더는 탐색할 때마다 다시 실행됩니다. 이것이 바로 /blog/a와 /blog/b를 혼동할 수 없는 이유이며, 실패한 로더를 간단히 재시도할 수 있는 이유입니다.

상황행동
실행되는 동안router.pending는 true이고 현재 있던 페이지는 그대로 유지됩니다.
예외가 발생하거나 Promise가 거부될 때router.error에 오류가 설정됩니다. go()는 거부되지 않으며 탐색을 다시 시도할 수 있습니다.
start() 실행 중start()가 반환한 Promise가 거부됩니다. 아직 오류를 저장할 Route가 없습니다.
추가한 레이어자체 데이터로 자체 로더를 실행합니다
유휴 시간 사전 로드모듈만 미리 로드합니다. 전달할 컨텍스트가 없으며 사용자가 열지 않을 수도 있는 라우트의 데이터를 미리 가져오지 않습니다.
동일한 URL로 다시 탐색로더가 다시 실행되고 행이 다시 생성됩니다. 로더가 없는 경로는 해당 행을 유지합니다.

재검증하려면 기록 항목을 추가하지 않고 이미 있는 위치로 이동하세요.

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

청크와 데이터를 동시에 로드하기

페이지 모듈에서 내보낸 load는 해당 모듈이 도착한 후에만 실행될 수 있으므로 콜드 탐색은 청크를 가져온 다음 데이터를 가져옵니다. 두 가지를 동시에 시작하려면 withLoad()를 사용하여 테이블에서 로더를 선언하세요.

routes.ts
import { createRouter, withLoad } from 'nuclo-router'
import { loadPost } from './data.ts'   // small, in the main bundle

createRouter({
  '/blog/:slug': withLoad(() => import('./pages/Post.ts'), loadPost),
})

모듈의 자체 load를 대체하고 방문할 때마다 실행됩니다. 청크가 여전히 로드되는 동안 거부는 다른 것과 마찬가지로 router.error에 도달합니다.

설계상 HTML에 서버 데이터가 직렬화되지 않습니다. 클라이언트의 load가 하이드레이션 시 다시 실행됩니다. 이것이 중요한 경우 서버가 route.data(예: window.__DATA__)를 내보내고 로더가 이를 먼저 가져오도록 합니다. route.data는 일치하는 페이지의 데이터만입니다. 부모의 load() 결과가 여기에 없습니다.

코드 분할 및 사전 로드

라우트의 값은 항상 페이지 컴포넌트를 반환하는 함수이므로 컴포넌트는 필요할 때 불러옵니다.

routes.ts
'/a': () => import('./A.ts'),                      // default export (+ optional load)
'/b': () => import('./B.ts').then(m => m.BPage),   // named export
'/c': () => CPage,                                 // eager, not split
'/d': () => ({ default: DPage, load: loadD }),     // eager, module-shaped

모듈 객체 형식으로 반환할 때만 load 내보내기를 함께 전달할 수 있습니다. .then(m => m.BPage)로 이름을 지정한 컴포넌트를 선택하면 라우터에는 그 함수만 전달됩니다.

각 모듈은 한 번 가져온 뒤 캐시됩니다. 같은 라우트로 동시에 이동하면 하나의 import()를 공유합니다. idleTimeout 동안 사용자 조작이 없으면(기본 1초: 터치, 키 입력, 휠, 스크롤 없음) 유휴 시간 구간(requestIdleCallback)마다 하나씩 나머지 라우트를 백그라운드에서 로드합니다. 입력이 발생하거나 사용자가 기다리는 탐색이 있으면 페이지가 다시 유휴 상태가 될 때까지 사전 로드를 멈춥니다.

사전 로드되었거나 즉시 사용 가능한 라우트는 router.pending를 변경하지 않고 다음 화면 갱신 전에 표시됩니다. 스피너가 잠깐 나타나거나 레이아웃이 밀리지 않습니다. router.pending는 모듈이나 로더를 기다려야 할 때만 true이며, 그동안 기존 페이지는 화면에 유지됩니다. 청크 로드에 실패하면 처리되지 않은 Promise 거부 대신 router.error에 오류가 설정되며 다시 시도할 수 있습니다.

라우터 API

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

createRouter(table, options?)

table는 패턴을 로더 또는 중첩 테이블에 매핑합니다. 옵션:

옵션기본값
base'/'URL 접두사(예: '/docs')에서 앱이 제공됩니다. 세그먼트 경계에서만 일치합니다. /docs/intro는 '/docs' 내부에 있고 /docsearch는 그렇지 않습니다. 외부의 URL은 일치하지 않으므로 start()는 거부하고(서버 측 404) 이에 대한 링크는 브라우저에 남아 있습니다.
preloadtrue페이지가 유휴 상태가 되면 백그라운드에서 나머지 경로의 모듈을 로드합니다. false로 비활성화합니다.
idleTimeout1000터치, 키 입력, 휠, 스크롤 없이 지나야 하는 시간(밀리초)입니다. 이 시간이 지나면 사전 로드를 시작하거나 사용자 조작으로 중단된 사전 로드를 재개합니다.
onNavigate(ctx, kind) => void. 브라우저에서 최초 탐색을 포함해 각 탐색이 완료되고 DOM에 반영된 뒤 호출됩니다. kind는 'start', 'push', 'replace', 'pop'(뒤로, 앞으로, close()) 또는 'layer'입니다. document.title, 메타 태그, 포커스, 스크롤, 분석 처리를 위한 훅이며 SSR 중에는 호출되지 않습니다.

반환된 라우터는 start(), match(), outlet(), matches()와 run()을 제외한 모든 Route 멤버를 제공합니다. Route 멤버는 활성 Route에서 읽습니다. 브라우저에서는 마지막 start()가 반환한 Route이고, 서버에서는 현재 run()을 실행 중인 Route입니다. 어느 쪽도 없을 때 멤버를 읽으면 오류가 발생합니다. 라우트 테이블, 컴파일된 매처, 모듈 캐시는 라우터에 저장되므로 서버의 여러 요청이 공유합니다.

router.start(url?)

url(브라우저 기본값: location.href)에 해당하는 페이지 모듈을 확인하고 Promise<Route>를 반환합니다. 일치하는 패턴도 테이블의 '*' 라우트도 없으면 Promise가 거부됩니다. 서버에서는 이를 404로 처리합니다. 브라우저에서는 해당 Route를 라우터의 활성 Route로 설정하고 popstate와 위임된 click 리스너를 연결하며 사전 로드를 예약합니다. start()를 다시 호출하면 이전 Route를 정리합니다. 서버에서는 공유 상태를 변경하지 않으므로 route.run() 안에서 렌더링하세요.

router.match(url?)

아무것도 로드하지 않고 URL을 테이블과 비교합니다. import, 히스토리 변경, 리스너 연결, Route 생성이 모두 없습니다. 일치하면 RouteContext를 반환합니다. URL이 base 밖에 있거나, 일치하는 패턴이 없거나, 브라우저에서 출처가 다르면 null을 반환합니다. ./를 해석하려면 활성 라우트가 필요하므로 절대 URL만 허용합니다.

example.ts
// 404 before importing a single chunk
if (!router.match(request.url)) return new Response(null, { status: 404 })

// "is this link ours?" for a custom link component or a prefetch rule
const owned = router.match(href) !== null

router.outlet()

활성 라우트의 페이지들을 list()로 반환합니다. 페이지를 표시할 위치에 한 번 배치하세요. 추가한 레이어는 목록의 뒤쪽 행이 됩니다. start()가 완료되기 전과 stop() 이후에는 비어 있습니다. 서버에서는 현재 run()을 실행 중인 Route를 렌더링합니다.

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 })필요한 모듈을 로드하고 이동합니다. Promise는 거부되지 않습니다. 모듈에서 예외가 발생하거나 Promise가 거부되면 error에 기록됩니다.
push(href)새 라우트를 최상위 레이어로 열고, 닫을 때 전달된 값으로 Promise를 이행합니다. ./를 허용하며 일치하는 라우트가 없거나 모듈 로드에 실패하면 Promise가 거부됩니다.
href(path)a({ href })의 경우 경로 앞에 base를 붙입니다. 활성 경로에 대해 ./ 및 ../를 확인합니다.
stop()모든 리스너를 분리하고, 열린 레이어를 닫고, 아웃렛을 비웁니다. 멱등성. 중지된 Route에서도 마지막 일치 결과를 읽을 수 있으며 go()는 아무 작업도 수행하지 않습니다.
run(fn)이 Route를 라우터의 활성 Route로 설정하고 fn을 호출한 뒤 그 결과를 반환합니다. 서버에서는 route.run(() => renderToString(App))처럼 사용합니다. 동기 호출만 지원하며 fn이 반환되면 이 Route는 더 이상 활성 Route가 아닙니다.

RouteContext

페이지의 첫 번째 인수: path, pattern, params, search, hash, url. 페이지가 작성된 컨텍스트입니다. 실시간 값을 보려면 router.*를 읽어보세요. 다시 생성하지 않고 쿼리나 해시가 변경된 경우에만 다릅니다.

params는 기본적으로 { [name]: string }입니다. 페이지는 Params를 사용하여 자체 패턴에서 매개변수 타입을 추론할 수 있으며, 이는 '/blog/:slug'를 { slug: string }로, '/files/*rest'를 { rest: string }로 바꿉니다.

pages/Post.ts
import type { Params, RouteContext } from 'nuclo-router'

export default function Post(ctx: RouteContext<Params<'/blog/:slug'>>) {
  return article(h1(ctx.params.slug))   // slug: string, nothing else exists
}

PageProps

페이지의 두 번째 인수입니다. 필요한 속성을 구조 분해해서 사용하세요.

속성
layer페이지가 스택에 있는 위치입니다. 아래 Layer를 참조하세요.
data라우트의 load() 반환값이며 로더가 없으면 undefined입니다. PageProps<TData>로 타입을 지정합니다.
outlet상위 페이지 안에 하위 라우트를 렌더링하는 위치입니다. 중첩 라우트를 참조하세요.
beforeLeave페이지를 제거하기 전에 호출할 가드를 등록합니다. 페이지 이탈 가드를 참조하세요.

Layer

페이지의 스택 내 위치와 레이어를 닫는 방법을 제공합니다.

멤버
depth기본 페이지는 0, 추가한 레이어는 1 이상입니다.
push(href)페이지 안에서 다른 레이어를 엽니다. Route.push()와 같습니다.
close(result?)이 레이어와 그 위의 모든 레이어를 닫고, 이 레이어를 연 push()의 Promise를 이행합니다. 인수가 없으면 취소한 것으로 처리합니다. 깊이 0에서는 아무 작업도 하지 않습니다.

DataLoader 및 withLoad()

DataLoader는 페이지 모듈의 선택적 load 내보내기인 (ctx: RouteContext) => TData | Promise<TData>입니다. withLoad(loader, load)는 load와 청크 가져오기가 동시에 시작되는 라우트를 만듭니다. 경로 로더를 참조하세요.

타입

모든 타입은 nuclo-router에서 이름으로 내보내므로 라우트 정의 파일을 가져오지 않고도 페이지, 로더, 가드에 타입을 지정할 수 있습니다.

타입설명
Params<'/blog/:slug'> 패턴에 대한 params 객체입니다. RouteParams는 구체적인 패턴을 반영하지 않은 기본 타입이고 Match는 매처의 반환값입니다.
PageComponent페이지 함수: (ctx, { layer, data, outlet, beforeLeave }) => …
RouteModule페이지 모듈입니다. 컴포넌트 자체이거나 import()가 반환하는 { default, load? } 형식입니다.
RouteLoader() => RouteModule | Promise<RouteModule>. 선택적으로 withLoad()의 load를 포함합니다.
RouteTablecreateRouter()에 전달하는 테이블로, 패턴을 로더 또는 중첩 테이블에 매핑합니다.
RouterOptions{ base?, preload?, idleTimeout?, onNavigate? }
NavigateOptions{ replace? }. go()의 두 번째 인수입니다.
NavigationKind'start' | 'push' | 'replace' | 'pop' | 'layer'. onNavigate의 두 번째 인수입니다.
OutletPageProps의 outlet() 타입입니다.
LeaveGuard(to) => boolean | void | Promise<…>, beforeLeave()가 취하는 것
MatchViewmatches()의 뷰입니다. 매개변수를 받아 콘텐츠 또는 페이지 모듈의 import()를 반환합니다.

레이어 및 이탈 가드

#

push()는 현재 페이지를 교체하는 대신 그 위에 새 라우트를 열며, 레이어가 닫힐 때 전달한 값으로 Promise를 이행합니다. 페이지 스택은 하나의 list()이므로 레이어를 열면 행 하나가 추가됩니다. 아래 페이지는 다시 렌더링되지 않아 레이어를 닫은 뒤에도 DOM, 포커스, 입력 중이던 폼이 그대로 유지됩니다.

이렇게 하면 사용자가 작성하고 있던 양식을 버리지 않고도 "아직 존재하지 않는 레코드가 필요합니다" 흐름이 작동합니다.

pages/Edit.ts
// the page underneath
const created = await layer.push<Option>('/options/new')
if (created) { options.push(created); selected = created.id; update() }
pages/NewOption.ts
// the pushed page: (ctx, { layer })
layer.close(created)   // resolves the push() above with the new option
layer.close()          // dismissed: resolves with undefined

레이어 중첩. 자체 대화 상자가 필요한 대화 상자는 layer.push()를 호출하고 Route를 직접 참조하지 않고 결과를 기다립니다.

  • URL은 최상위 레이어를 나타냅니다. push()는 히스토리 항목을 추가하므로 레이어에 직접 링크할 수 있습니다. 뒤로 가면 레이어가 닫히고 취소와 마찬가지로 해당 push()가 이행됩니다. 뒤로 가기와 close()는 같은 코드 경로를 사용합니다. close()는 결과를 저장하고 히스토리를 뒤로 이동하며, 깊이는 history.state의 값을 기준으로 판단합니다. 브라우저는 탭당 약 50개 항목만 유지하므로 레이어를 오래 탐색하면 아래 항목이 사라질 수 있습니다. 이때 close()는 레이어를 닫고 남아 있는 페이지에 맞춰 현재 히스토리 항목을 교체합니다.
  • 일반 탐색은 스택을 지웁니다. 링크 클릭 또는 go()는 레이어가 아닙니다. 모든 것을 대체하고 열려 있는 모든 레이어를 닫으므로 대기 중인 호출자가 없습니다.
  • URL에 직접 접속하면 독립 페이지로 표시됩니다. URL은 최상위 레이어만 나타내고 아래 스택은 나타내지 않습니다. 따라서 레이어 URL에 직접 접속하면 깊이가 1인 스택에 layer.depth === 0인 기본 페이지로 렌더링되며 close()는 동작하지 않습니다. 레이어로 여는 라우트도 단독으로 사용할 수 있어야 합니다. layer.depth에 따라 분기하여 동작하지 않는 취소 버튼 대신 나가기 링크를 제공하세요.
  • 다시 로드하면 Promise는 유지되지 않습니다. Promise가 클로저에 저장되기 때문입니다. 결과 전달은 편의 수단으로 사용하되, 페이지에 직접 접속하는 방식으로도 같은 작업을 완료할 수 있게 하세요.
  • 대기 중인 호출이 남지 않습니다. stop()는 열린 레이어를 모두 닫으며 탐색으로 제거된 레이어의 Promise는 undefined로 이행됩니다.

페이지 이탈 가드

입력 중인 폼처럼 보존할 내용이 있는 페이지는 가드를 등록합니다. 페이지를 제거하기 전에 가드를 호출하며, false 또는 false로 이행되는 Promise를 반환하면 페이지를 유지합니다.

pages/Edit.ts
import type { PageProps, RouteContext } from 'nuclo-router'

export default function Edit(_ctx: RouteContext, { beforeLeave }: PageProps) {
  let dirty = false
  beforeLeave(() => !dirty || confirm('Discard your changes?'))
  return form(input({ onInput: () => (dirty = true) }), …)
}
  • 페이지를 떠날 때만 호출합니다. 레이아웃 내부의 하위 탐색이나 현재 페이지를 유지하는 쿼리·해시 변경에서는 호출하지 않습니다. 가드는 페이지의 생명주기에 묶여 있으므로 직접 등록을 해제할 필요가 없습니다.
  • 모든 이탈 경로에 적용됩니다. 링크, go(), 뒤로·앞으로 가기, 레이어의 close(), 라우터가 처리하지 않는 URL로의 go()에도 적용됩니다(앱 밖으로 나갈 때 to는 null). 뒤로·앞으로 가기를 거부하면 히스토리 이동을 되돌려 주소 표시줄과 현재 페이지를 일치시킵니다. 탭 닫기와 새로 고침은 beforeunload로 처리해야 합니다.
  • to는 이동할 대상의 RouteContext입니다. 가드는 가장 안쪽부터 실행되며 처음 거부한 지점에서 멈춥니다. 가드에서 오류가 발생해도 현재 페이지를 유지합니다. close(result)도 가드를 거치므로 결과를 반환하며 닫기 전에 변경 여부 플래그를 해제하세요.
  • 라우터는 자신이 만든 히스토리 항목 사이의 이동만 되돌릴 수 있습니다. 기본 #fragment 링크나 다른 스크립트의 pushState처럼 라우터가 만들지 않은 항목으로 뒤로·앞으로 이동할 때는 가드를 호출하지 않습니다.

matches() 및 하위 라우터

#

router.outlet()가 페이지를 렌더링합니다. router.matches(table)는 경로를 로드하거나 페이지를 추가하지 않고도 자체 작은 테이블에서 사이드바, 이동 경로 또는 도구 모음과 같이 사용자의 위치에 따라 달라지는 다른 항목을 렌더링합니다.

app.ts
div(
  aside(
    router.matches({
      '/':           () => nav('Home links'),
      '/blog/new':   () => nav('Drafting tips'),
      '/blog/:slug': ({ slug }) => nav(`More like ${slug}`),
      '*':           () => import('./Sidebar.ts'),   // code-split, rendered once loaded
    }),
  ),
  main(router.outlet()),
)
  • 라우트 테이블과 같은 규칙을 사용합니다. 패턴, 우선순위, 원래 경로와의 매칭, base 제거는 라우터와 동일하게 처리하므로 뷰와 페이지가 같은 URL을 기준으로 동작합니다. router.path와 마찬가지로 최상위 레이어의 경로를 읽습니다.
  • 뷰는 (params, ctx) => content 형식입니다. params 타입은 패턴에서 추론되며 ctx는 뷰의 pattern과 params가 담긴 RouteContext입니다. 페이지 모듈의 import()를 반환할 수도 있습니다. 모듈이 로드되면 ctx를 전달해 호출합니다.
  • 일치하는 항목이 없으면 아무것도 렌더링되지 않습니다. 대체를 위해 '*' 보기를 추가합니다. 아웃렛처럼 start() 이전과 stop() 이후는 비어 있습니다.
  • 경로가 바뀔 때만 다시 렌더링됩니다. 따라서 페이지가 유지되는 탐색에서는 뷰의 DOM도 유지됩니다.
  • 지연 보기는 해당 모듈이 도착할 때까지 아무것도 렌더링하지 않습니다. 실패한 import()는 console.error로 기록되고 router.error를 설정하지 않으며 다음 탐색에서 다시 시도됩니다. 일반 update()는 재시도하지 않습니다.
  • 서버에서는 지연 로드 뷰를 렌더링하지 않습니다. 모듈을 클라이언트에서 불러오기 때문입니다. 최초 HTML에 포함되어야 하는 뷰는 동기 함수로 작성하세요.

서브 라우터

라우트가 중첩을 지원하므로 하위 라우터는 보통 하나의 라우터 안에 중첩 테이블로 구성하면 됩니다. 섹션의 '/' 페이지가 공통 UI를 제공하고 그 안의 outlet()에 하위 페이지를 렌더링하므로 섹션 내부를 이동해도 공통 UI가 유지됩니다.

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

또는 해당 기능에 라우터를 따로 만들되 시작하지 않고 사용할 수 있습니다. 상위 라우터는 catch-all 하나로 섹션을 처리하고 하위 라우터는 남은 경로를 자체 패턴, 매개변수, 폴백으로 해석합니다.

routes.ts
// parent table
'/docs/*rest': () => import('./docs/Shell.ts'),
docs/Shell.ts
const docs = createRouter(docsRoutes, { base: '/docs' })
const hit = docs.match()                       // { pattern: '/:topic', … }
const page = await docsRoutes[hit.pattern]()   // the app's own lookup

이 라우터는 start()를 호출하지 않는 것이 핵심입니다. start()는 리스너를 연결하므로 라우터 두 개를 시작하면 충돌합니다. 하위 라우터가 경로 해석을 담당하는 대신, 시작된 라우터가 자동 실행해 주는 데이터 로더는 직접 호출해야 합니다. 또한 바깥 라우터는 하나의 라우트만 보고 나머지는 셸이 해석하므로 하위 경로가 바뀔 때 셸도 다시 생성됩니다. 중첩 테이블을 사용하면 상위 페이지가 계속 마운트된 상태로 유지됩니다. 해당 기능에 독립된 라우터, base, 폴백이 꼭 필요한 경우가 아니라면 중첩 테이블을 사용하세요.

여러 라우터 및 통합 로딩

#

앱에는 영역마다 라우터를 둘 수 있습니다. 규칙은 간단합니다. 문서당 하나만 시작하고 나머지는 시작하지 않은 채 match()로 URL을 해석합니다. 시작된 라우터만 URL, 히스토리, 클릭 리스너를 관리하며 나머지는 직접 조회하는 테이블입니다. 각 영역은 자체 스피너와 독립적인 로딩 상태를 가질 수 있습니다.

결합 로딩: 로더 체인

일치하는 라우트 체인의 각 페이지는 load()를 내보낼 수 있습니다. 하위 라우트에 처음 진입하면 레이아웃과 페이지의 로더가 병렬로 실행되며 하나의 router.pending가 전체 체인의 로딩 상태를 나타냅니다. 두 로더가 모두 완료될 때까지 기존 페이지가 유지되므로 일부만 로드된 트리는 표시되지 않습니다. 각 컴포넌트는 자신의 data를 받습니다.

routes.ts
'/shop': {
  '/':          () => import('./pages/Shop.ts'),    // layout, has a load()
  './:section': () => import('./pages/Section.ts'), // page, has a load()
},
pages/Shop.ts
import type { DataLoader, PageProps, RouteContext } from 'nuclo-router'

export const load: DataLoader<ShopData> = async () => {
  await wait(500)                              // a slow API
  return { sections: ['toys', 'books', 'music'] }
}

// the layout gets its own data, and its outlet for the child page
export default function Shop(_ctx: RouteContext, { data, outlet }: PageProps<ShopData>) {
  return div(
    h1('Shop'),
    nav(...data.sections.map((s) => a({ href: router.href(`/shop/${s}`) }, s))),
    main(outlet()),
  )
}
pages/Section.ts
export const load: DataLoader<SectionData> = async (ctx) => {
  await wait(900)                              // slower than the layout's
  return { section: ctx.params.section, items: catalog[ctx.params.section] }
}

// the page gets its own data: the two loaders ran beside each other
export default function Section(_ctx: RouteContext, { data }: PageProps<SectionData>) {
  return article(h2(data.section), ul(...data.items.map((i) => li(i))))
}

두 로더가 차례로 시작되지 않고 함께 시작됩니다. router.pending는 둘 중 하나가 대기하는 동안 true이고 나머지 시간에는 false입니다. 사전 로드되거나 즉시 사용 가능한 라우트는 이 값을 true로 바꾸지 않습니다. 섹션 사이를 이동하면 레이아웃이 유지되므로 섹션의 로더만 다시 실행됩니다. 레이아웃의 데이터, DOM 및 입력 중인 텍스트는 모두 유지됩니다. 동일한 URL로 다시 탐색하면 단일 페이지와 마찬가지로 리프의 로더가 다시 실행되고 해당 행이 다시 생성됩니다.

다중 로딩: 자체 스피너가 있는 두 번째 라우터

독립 영역(사이드바, 트레이, 라이브 영역)은 생성되었지만 시작되지 않은 자체 라우터와 자체 로딩 플래그를 갖습니다. router.pending는 시작된 라우터에 속하며 이 영역에 대해 아무 말도 하지 않으므로 두 라우터가 나란히 로드될 수 있으며 둘 중 하나가 먼저 완료될 수 있습니다.

sidebar.ts
import { createRouter } from 'nuclo-router'

// Created but never started: start() is what attaches the history and click
// listeners, and only the main router is started. match() attaches nothing.
const standIn = () => ({ default: () => div() })   // never rendered

const sidebarRouter = createRouter({
  '/shop':          standIn,
  '/shop/:section': standIn,
  '*':              standIn,
}, { base: '/docs' })              // the same base as the main router

export const sidebar = { loading: false, info: null as SidebarInfo | null }

export async function refreshSidebar(at: string) {
  const hit = sidebarRouter.match(at)          // the second router resolves
  if (!hit || (hit.pattern !== '/shop' && hit.pattern !== '/shop/:section')) {
    sidebar.info = null; update(); return
  }
  sidebar.loading = true                       // its own flag, its own spinner
  sidebar.info = null; update()
  const data = await fetchSidebar(hit.params.section)
  sidebar.loading = false; sidebar.info = data
  update()
}
routes.ts
createRouter(routes, {
  onNavigate: (ctx, kind) => {
    announce(kind)
    refreshSidebar(ctx.url)   // every navigation re-resolves the sidebar
  },
})

두 번째 라우터는 직접 조회할 때만 사용됩니다. match()는 URL을 읽고 컨텍스트를 반환하며, 기본 라우트 테이블에는 사이드바 관련 항목이 들어가지 않습니다. onNavigate는 최초 탐색을 포함한 모든 탐색이 완료된 뒤 실행되므로 독립 영역을 URL과 동기화하는 코드를 한곳에 둘 수 있습니다.

  • pending는 시작된 라우터에 속합니다. 시작되지 않은 라우터에는 Route, pending 및 error가 없습니다. 해당 영역은 자체 플래그를 추적하고 두 번째 스피너는 첫 번째 스피너 옆에 배치될 수 있습니다. 데모에서 사이드바는 800ms 동안 로드되는 반면 경로의 로더는 900ms 동안 실행되므로 페이지는 사이드바보다 먼저 완료되거나 그 반대 방향으로 완료될 수 있습니다.
  • match()는 아무것도 첨부하지 않습니다. 시작되지 않은 라우터 수에 관계없이 문서를 공유할 수 있습니다. 각 라우터에는 자체 테이블, base 및 모듈 캐시가 있으며 어느 라우터도 히스토리를 변경하지 않습니다. 필요한 자원은 테이블과 매처뿐입니다.
  • 해당 라우터의 로더는 실행되지 않습니다. 시작된 라우터는 일치하는 페이지의 load()를 실행하지만 시작되지 않은 라우터는 경로만 해석합니다. 뷰에 데이터가 필요하면 직접 가져오세요(데모의 refreshSidebar()). 자동 로더 실행이 필요하면 시작된 라우터 안에서 중첩 테이블이나 페이지가 채우는 영역으로 구성하세요.
  • 오래된 응답을 무시하세요. 느린 사이드바 요청의 응답이 이미 다른 섹션으로 이동한 화면에 적용되어서는 안 됩니다. 요청마다 세대 카운터를 증가시키고 현재 값과 일치하지 않는 응답은 버리세요. 라우터도 새 탐색이 앞선 탐색을 추월하면 같은 방식으로 이전 결과를 버립니다.
  • 지연 로드 matches() 뷰에도 pending가 없습니다. 모듈이 로드되기 전에는 아무것도 렌더링하지 않습니다. 라우트의 pending, 독립 영역의 자체 플래그, 아직 표시되지 않은 지연 로드 뷰는 각각 독립적으로 로딩됩니다.

계산된 값

#

Nuclo에는 계산된 값을 위한 별도 API가 없습니다. 일반 함수를 사용하고 동적 값에서 호출하세요.

example.ts
let items = ['apple', 'banana', 'cherry']
let filter = ''

const visible = () => items.filter((item) => item.includes(filter))

const Fruits = () =>
  div(
    p(() => `Showing ${visible().length} of ${items.length}`),
    ul(list(visible, (item) => li(item))),
  )

filter = 'an'
update()

비동기 처리 및 로딩

#

일반 async/await를 사용하고 상태가 변경될 때마다 update()를 호출하세요. 데이터를 가져올 때는 await 이전에 한 번, 이후에 한 번을 의미합니다.

example.ts
let status: 'idle' | 'loading' | 'done' | 'error' = 'idle'
let result: unknown = null

async function loadData() {
  status = 'loading'
  update()
  try {
    result = await fetch('/api/data').then((r) => r.json())
    status = 'done'
  } catch {
    status = 'error'
  }
  update()
}

권장 사항

#
  • 일괄 변경. 필요한 모든 상태를 변경한 다음 update()를 한 번 호출합니다.
  • 동적 값을 빠르고 순수하게 유지하세요. 이들 모두는 모든 update()에서 실행됩니다.
  • 입력이나 타이머 등 작고 빈번한 업데이트에는 scope()를 사용하세요.
  • 문서에 연결된 요소에 렌더링하세요. update()가 실행될 때 문서에 없는 노드는 업데이트를 중지합니다.
  • 노드를 반환하는 함수가 아니라 변경되는 자식 요소에 대해 when() 및 list()를 사용합니다.
  • class 대신 className을 사용하세요, 동적 클래스에는 () => cx(...) 또는 { className: () => … }를 사용하세요.
  • 모듈 최상위에서 스타일을 한 번 정의합니다.
  • 서버 렌더링: 먼저 렌더링한 다음 getCssText()를 <style id="nuclo-styles">에 넣습니다. 요청 데이터를 인수로 전달하고 출력에 HTML 주석을 유지합니다.

API 색인

#

모든 공개 함수와 제공 모듈입니다.

nuclo(전역 및 명명된 내보내기)

nuclo(이름을 지정한 내보내기 전용)

nuclo/ssr

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

nuclo/polyfill

  • 부수 효과 import, document, Event, CustomEvent, Node, Element, HTMLElement 및 Nuclo* 클래스.

nuclo-router(별도 패키지)

  • createRouter(table, options?): 라우터, start(), match(), outlet(), matches(), go(), push(), href() 및 stop().
  • withLoad(loader, load): 데이터 로더와 청크 로드가 동시에 시작되는 라우트입니다.
  • 타입: RouteContext, PageProps, Params, DataLoader, RouteTable 등.

전역 전용

  • 태그 빌더 175개: div, span, …, var_ 및 svgSvg, pathSvg, ….
  • 타입: NodeModFn, NodeModLike, ExpandedElement 등. StyleResult와 같은 스타일 타입을 'nuclo'에서 가져올 수도 있습니다.

특수 키

자주 묻는 질문

#

상태 변경이 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()가 실행될 때 해당 노드가 문서에 없었기 때문입니다. 한 번 추적이 중지된 노드는 나중에 연결해도 다시 업데이트되지 않으므로 문서에 연결된 컨테이너에 렌더링하세요. 그 밖에 when() 조건에서 오류가 발생하면 해당 블록의 업데이트가 중지되고, 텍스트 함수가 첫 실행에서 오류를 던지면 계속 빈 텍스트로 남습니다. update()를 참조하세요.

언제 forceUpdate()를 사용해야 합니까?

언어 전환과 같이 정적 값이 거의 변경되지 않는 경우에만 해당됩니다. 그 밖의 모든 것에는 update()를 사용하세요. forceUpdate()를 참조하세요.

props으로 전달하지 않고 페이지를 레이아웃에 어떻게 넣나요?

region({ id: 'main' })로 장소를 표시하고 페이지가 into('main', …)를 반환하도록 합니다. region() & into()를 참조하세요.

앱을 어떻게 마운트 해제하나요?

when()로 감싸고 조건을 끕니다. DOM을 직접 제거하면 onDestroy가 실행되지 않습니다. 생명주기를 참조하세요.

Nuclo에는 라우터가 포함되어 있습니까?

코어 패키지에는 없습니다. 라우팅은 서버 렌더링, 하이드레이션, 코드 분할 경로 및 유휴 사전 로드 기능을 갖춘 nuclo-router로 별도로 배포됩니다. npm install nuclo nuclo-router로 설치하세요. nuclo-router를 참조하세요.

Nuclo에 이펙트가 있나요?

아니요. onMount 및 onDestroy는 요소당 한 번 실행되며 삽입 및 제거와 연결됩니다. 일부 상태가 변경되었기 때문에 아무 것도 다시 실행되지 않습니다.