简介概述
NUCLO / 12

언어

문서

Nuclo 문서

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

빠른 시작npm create nuclo@latest
빠른 시작
v0.3.29
현재 버전
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(div, span, button, …)의 경우 112개, SVG의 경우 63개, Svg 포함 접미사(svgSvg, pathSvg, …). 태그 빌더를 참조하세요.
  • 16 도우미: update, scope, when, list, region, into, on, render, hydrate, forceUpdate, css, cx, createCss, variants, keyframes 및 globalStyle.

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

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

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

명명된 수입품

도우미는 내보내기라고도 합니다. 전역변수와 동일한 기능입니다. getCssText() 및 resetStyles()는 명명된 내보내기 전용입니다. 태그 빌더는 전역 전용이므로 import { div } from 'nuclo'는 작동하지 않습니다.

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

진입점

  • nuclo: 전역을 등록하고 16개의 도우미와 getCssText 및 resetStyles를 내보냅니다.
  • nuclo/ssr: renderToString, renderManyToString, renderToStringWithContainer 및 getCssText. 서버 렌더링를 참조하세요.
  • nuclo/polyfill: 노드 및 Bun용 소형 DOM. nuclo/polyfill를 참조하세요.
  • nuclo/types: 유형만 해당. TypeScript를 참조하세요.

TypeScript

#

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

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

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

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

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

유용한 유형

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

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

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

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

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

card.ts
let name = 'Ada'

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

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

Profile

Hello, Ada

인수

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

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

알았어

  • div(isAdmin && span('Admin'))는 "false" 텍스트를 렌더링합니다. div(when(() => isAdmin, span('Admin')))를 작성하세요.
  • 배열은 하위 배열이 아닙니다. 전파하세요: ul(...names.map((n) => li(n))). 변경되는 목록의 경우 list()를 사용하세요.
  • 태그 이름이 있는 로컬 변수는 빌더를 숨깁니다. function Row(label: string) 내부에서는 label(...)를 호출할 수 없습니다. label, title, data, p, a 및 i와 같은 로컬 이름을 바꿉니다.
  • 브라우저는 id가 있는 모든 요소를 ​​전역 변수로 노출합니다. 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 제거

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)를 확인합니다. 매개변수를 선언하는 함수는 custom 수정자이며 한 번만 실행됩니다. (x = 0) => … 및 (...args) => …의 길이는 0이므로 동적입니다.

동적 텍스트

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

동적 속성

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

오류

동적 값이 발생하면 Nuclo가 오류를 포착하고 나머지 업데이트는 계속 실행됩니다. 텍스트는 비어 있거나 이전 값을 유지하며 속성은 이전 값을 유지합니다. 둘 다 성공한 다음 update()에서 복구됩니다. 예외: first 실행을 던지는 텍스트 함수는 영원히 비어 있습니다.

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

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

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

This dynamic value has run 1 times.

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

하나 이상의 범위 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는 nothing를 업데이트하고 오류가 발생하지 않으므로 철자를 확인하세요.
  • scope()는 서버에서 아무 작업도 수행하지 않습니다.
fn공개 API
function when(condition: boolean | (() => boolean), ...content: NodeModLike[]): WhenBuilder interface WhenBuilder { when(condition: boolean | (() => boolean), ...content: NodeModLike[]): WhenBuilder else(...content: NodeModLike[]): WhenBuilder }

조건에 따라 콘텐츠를 렌더링합니다. Nuclo는 모든 update()에서 조건을 다시 확인합니다. 첫 번째 참 조건이 승리합니다. true가 아닌 경우 .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()를 다시 호출하고 ID(===)를 기준으로 새 항목을 기존 행과 일치시킵니다.

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()는 어느 곳에서나 어떤 깊이에서도 채울 수 있습니다. 레이아웃은 콘텐츠를 소품으로 받지 않으며 콘텐츠를 구성하는 컴포넌트는 콘텐츠가 어디에 배치되는지 알지 못합니다.

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() 행은 항목이 아닌 위치로도 일치하므로 재사용된 행 요소가 결국 다른 항목을 표시할 수 있습니다.
  • 새 빌드에서 더 이상 설정되지 않는 속성은 제거되지 않습니다.
주의 사항: 컴포넌트 함수 내부에 선언된 상태는 함수가 다시 실행되기 때문에 재설정됩니다. 컴포넌트 외부에서 유지되어야 하는 상태를 유지합니다.

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

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

render()

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

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

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

render(App, document.getElementById('app')!)
  • 컴포넌트(render(App)) 또는 컴포넌트가 반환하는 빌더(render(App()))를 전달할 수 있습니다. DOM도 동일합니다. render(App)만 forceUpdate()가 나중에 루트를 다시 빌드할 수 있으므로 선호합니다.
  • 첨부합니다. parent를 지우지 않으므로 두 번 호출하면 두 개의 복사본이 추가됩니다.
  • render()가 반환되면 트리의 모든 onMount가 실행되었습니다.
  • 문서에 있는 요소로 렌더링합니다. update()를 참조하세요.
  • 루트는 when() 또는 list()가 아닌 div(...)와 같은 요소여야 합니다.
  • index는 내부입니다. 요소가 삽입되는 위치는 선택하지 않습니다. 그것을 그대로 두십시오.

render(App)로 전달된 컴포넌트는 매개 변수를 사용하지 않아야 합니다. 필수 매개변수는 유형 오류입니다. 선택적인 유형 검사 중 하나는 런타임에 render()가 발생합니다. 소품을 사용하는 컴포넌트를 래핑합니다.

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

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

hydrate()

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

는 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()는 아무것도 자동으로 부착하지 않습니다. 소품을 사용하는 컴포넌트를 래핑합니다: hydrate(() => Page(props), root).
  • HTML 서버가 없으면 render()를 사용하세요.

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

클라이언트가 승리합니다.

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

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

서버 렌더링

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

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

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

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

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

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

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

서버에서 실행되는 것

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

renderToString()

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

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

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

작은 DOM이므로 Nuclo는 노드 및 Bun에서 트리를 구축할 수 있습니다. 첫 번째 렌더링 전에 한 번 가져옵니다. 대부분의 앱에는 일반 가져오기만 필요합니다.

  • 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 (표시를 설정하지 않음)
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

모든 속성에 대해 raw를 사용하고 이름은 CSS와 동일하게 작성됩니다. 숫자는 여전히 px를 가져오므로 문자열을 전달합니다.

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

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

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

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

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

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

동적 클래스

항상 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"가 해결되지 않았습니다.
  • @가 없는 화면 값은 @media <value>가 됩니다. @로 시작하는 값은 작성된 대로 사용됩니다(예: "@container (min-width: 400px)").
  • 화면은 createCss()로 만든 인스턴스에만 존재합니다. 전역 css()에는 키가 없지만 @media 키를 허용합니다.
  • 모든 인스턴스와 전역 도우미는 하나의 스타일시트를 공유합니다. 스크린 미디어 규칙은 스크린을 사용하는 순서가 아닌 선언된 순서대로 출력되므로 min-width 스크린을 작은 것부터 큰 것 순으로 나열합니다. 앱당 하나의 createCss() 인스턴스를 사용하여 해당 순서가 테마의 순서가 되도록 합니다.
  • theme 결과에 합격하신 테마입니다.

variants()

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

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

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

button(buttonClass(), 'Save')                                                // primary, sm
button(buttonClass({ intent: 'danger', size: 'lg', block: true }), 'Delete') // bold, full width
  • 스타일은 base, 각 그룹에서 선택한 값, 일치하는 각 화합물 순서로 병합됩니다. 최신 스타일은 속성별로 승리합니다.
  • 누락된 undefined 또는 null 옵션은 기본값을 사용합니다. 기본값이 없는 그룹은 건너뜁니다.
  • true 및 false 키만 있는 그룹은 부울을 사용합니다.
  • 나열된 모든 그룹이 일치할 때 화합물이 적용됩니다(기본값 포함).
  • 각 선택 항목은 한 번만 작성되고 캐시됩니다.

keyframes() 및 globalStyle()

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

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

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

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

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

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

getCssText() 및 resetStyles()

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

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

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

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

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

beforeEach(() => resetStyles())

nuclo-router

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

Nuclo용 라우팅: list() 및 update()를 기반으로 구축된 서버 렌더링, 하이드레이션, 코드 분할 경로 및 유휴 사전 로드. 4.9 정보 KB gzipped는 Nuclo 자체 이외의 종속성과 부작용이 없으므로 사용하지 않을 때 트리 쉐이크됩니다.

terminal
$ npm install nuclo nuclo-router

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

라우터는 로드할 what를 결정합니다. 앱에는 where with router.outlet()가 표시됩니다. 레이어 스택 위에 하나의 list()가 있고 레이어당 한 행이 있으므로 일반 페이지는 단일 행입니다. 서버와 클라이언트는 동일한 트리를 구축하므로 hydrate()는 서버 노드를 교체하는 대신 서버 노드를 요청합니다. 그 밖의 모든 것은 history, 하나의 위임된 click 수신기 및 requestIdleCallback입니다.

원 임포트

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

start()는 반환되기 전에 일치하는 경로의 모듈을 확인하므로 첫 번째 트리는 서버와 클라이언트 모두에서 완성됩니다. 그 후 라우터는 시작된 경로인 router.path, router.pending, router.go()를 읽습니다.

전체 앱

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

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

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

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

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

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

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

서버에서 start()는 요청당 Route를 반환하고 라우터를 그대로 두므로 동시 요청이 서로의 경로를 볼 수 없습니다. route.run()는 라우팅을 라우터로 만들고 콘센트에서 렌더링 길이를 읽습니다. 브라우저에는 페이지 로드당 하나의 경로가 있으며 start()는 이를 라우터 자체로 만듭니다. run()는 필요하지 않습니다.

페이지는 RouteContext 및 PageProps를 (ctx, { layer, data, outlet, beforeLeave })로 수신하며 더 적은 인수를 사용할 수 있습니다. list()가 허용하는 모든 것을 반환합니다. 레이아웃의 다른 부분도 채우려면 페이지 내부에서 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{ '*': '…' }

우선 순위는 정적이며 선언 순서에 따라 동적이며 가장 긴 접두사가 먼저 캐치됩니다. 따라서 '*'는 다른 어떤 것도 하지 않는 것과만 일치하며 테이블의 어느 곳에나 앉을 수 있습니다. 후행 슬래시, 중복 슬래시 및 백분율 인코딩이 정규화되고 param 값이 디코딩되어 도착합니다. 매칭은 패턴별 세그먼트 비교입니다. 정규식은 빌드되거나 실행되지 않습니다.

상대 경로

./로 작성된 패턴은 이를 선언하는 상위 패턴을 기준으로 하며, ./로 작성된 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() 오류입니다. 왜냐하면 포괄적인 것은 패턴을 종료해야 하기 때문입니다.

상위 페이지 및 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가 비활성화된 경우에도 여전히 정확합니다.

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

노드탐색 중
a 패턴과 경로가 변경되지 않은 상위kept: 동일한 노드이고 해당 load()는 그렇지 않습니다. 다시 실행
일치된 페이지kept 패턴과 경로가 변경되지 않고 load()가 없으므로 쿼리 또는 해시 변경으로 유지됩니다. 로더가 있는 경로는 방문할 때마다 새로 고쳐집니다.
a 경로가 변경된 상위재빌드됨 및 그 아래의 모든 항목
a 푸시 레이어는 일치하는 페이지만 렌더링합니다. 상위 항목은 이미 그 아래 행에 마운트되어 있습니다.

유지된 페이지는 빌드된 ctx를 유지합니다. 이것이 어디에서나 ctx의 의미입니다. 실시간 값을 보려면 router.*를 읽어보세요. 필터, 페이지 매김, 탭과 같은 쿼리 기반 상태는 동적 값 내에서 router.search를 읽고 router.go('?page=2', { replace: true })로 씁니다. 페이지, 해당 포커스 및 절반만 입력된 입력은 그대로 유지됩니다.

페이지 및 리전

#

router.outlet()는 페이지를 한 곳에 모아줍니다. 사이드바, 도구 모음 또는 레이아웃의 다른 위치에 제목도 필요한 페이지는 into()로 채워집니다. 레이아웃은 region()로 장소를 표시하고 페이지는 자체 콘텐츠 옆에 into()를 반환합니다. 레이아웃은 페이지 콘텐츠를 소품으로 받지 않으며 탐색이 다시 작성되지 않습니다.

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

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

페이지의 콘텐츠는 콘센트의 list() 행이며 into()는 이를 보유하는 행만큼 오래 유지됩니다. 따라서 사이드바 콘텐츠는 페이지와 함께 도착하고 페이지와 함께 떠납니다. into('sidebar', …) 없이 페이지로 이동하면 해당 지역의 empty 콘텐츠가 다시 표시됩니다. 두 부분 모두 동일한 기능으로 구축되었으므로 페이지에서는 두 부분 모두에 대해 ctx 및 data를 사용할 수 있습니다.

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

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

한 페이지의 여러 지역

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

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

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

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

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

라우트 로더 및 코드 분할

#

페이지 모듈은 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이고 현재 있던 페이지는 그대로 유지됩니다.
던지거나 거절하는 거죠착륙하다 router.error. go() 여전히 거부되지 않으며 탐색을 다시 시도할 수 있습니다.
start() 중에 start()에서 거부가 발생합니다. 아직 올려드릴 루트가 없습니다.
a 푸시 레이어자체 데이터로 자체 로더를 실행합니다
유휴 프리로더는 모듈만 따뜻하게 합니다. 전달할 컨텍스트가 없으며 결코 열 수 없는 경로에 대한 데이터를 가져옵니다.
동일한 URL로 다시 탐색로더가 다시 실행되고 행이 다시 작성됩니다. 로더가 없는 경로는 해당 행을 유지합니다.

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

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

청크로 데이터 시작하기

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

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

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

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

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

코드 분할 및 사전 로드

경로의 값은 항상 페이지 컴포넌트를 반환하는 함수이므로 컴포넌트에 느리게 도달합니다.

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

모듈 모양 양식만 load 내보내기를 수행할 수 있습니다. .then(m => m.BPage)를 사용하여 명명된 컴포넌트를 선택하면 라우터에 기능만 전달됩니다.

각 모듈은 한 번 가져오고 캐시됩니다. 동일한 경로에 대한 동시 탐색은 하나의 import()를 공유합니다. 사용자가 idleTimeout에 대한 페이지를 그대로 두면(기본적으로 1초, 탭, 키, 휠 또는 스크롤 없음) 남은 경로가 유휴 슬라이스(requestIdleCallback)당 하나씩 백그라운드에 로드됩니다. 사용자가 기다리고 있는 입력이나 탐색은 페이지가 다시 유휴 상태가 될 때까지 일시 중지됩니다.

사전 로드되거나 열성적인 경로는 router.pending를 뒤집지 않으며 다음 페인트 전에 착륙하지 않습니다. 스피너 프레임도 없고 레이아웃 전환도 없습니다. router.pending는 기다려야 하는 모듈이나 로더에 대해서만 적용되며, 나가는 페이지는 있는 동안 화면에 유지됩니다. 처리되지 않은 거부가 아닌 router.error로 표면을 로드하지 못한 청크는 간단히 재시도할 수 있습니다.

라우터 API

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

createRouter(table, options?)

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

동안 전화한 적이 없습니다.
옵션기본값
base'/'URL 접두사(예: '/docs')에서 앱이 제공됩니다. 세그먼트 경계에서만 일치합니다. /docs/intro는 '/docs' 내부에 있고 /docsearch는 그렇지 않습니다. 외부의 URL은 일치하지 않으므로 start()는 거부하고(서버 측 404) 이에 대한 링크는 브라우저에 남아 있습니다.
preloadtrue페이지가 유휴 상태가 되면 백그라운드에서 나머지 경로의 모듈을 로드합니다. false가 전원을 끕니다.
idleTimeout1000 미리 로드가 시작되기 전 탭, 키, 휠 또는 스크롤이 없는 밀리초 또는 사용자가 종료된 후 다시 시작 바쁘다.
onNavigate(ctx, kind) => void는 ​​DOM가 따라잡으면 초기 탐색을 포함하여 브라우저에서 탐색이 해결될 때마다 호출됩니다. kind는 'start', 'push', 'replace', 'pop'(뒤로, 앞으로, close()) 또는 'layer'입니다. document.title, 메타 태그, 포커스, 스크롤 및 분석을 위한 후크입니다. SSR.

라우터 start(), match(), outlet(), matches() 및 run()를 제외한 모든 Route 멤버를 활성 경로로 읽어 반환합니다. 이는 브라우저에 마지막으로 반환된 start() 또는 서버에서 실행 중인 run()의 것입니다. 던지지 않은 멤버를 읽습니다. 경로 테이블, 컴파일된 일치자 및 모듈 캐시는 라우터에 있으므로 서버의 요청 간에 공유됩니다.

router.start(url?)

url(브라우저의 기본값: location.href)를 페이지 모듈로 확인하고 Promise<Route>를 반환합니다. 일치하는 항목이 없고 테이블에 '*' 경로가 없으면 거부됩니다. 서버에서는 404입니다. 브라우저에서는 라우팅을 라우터의 활성 경로로 만들고 popstate 및 위임된 click 리스너를 연결하고 사전 로드를 예약합니다. 두 번째 start()는 이전 경로를 폐기합니다. 서버에서는 공유되는 항목이 없으므로 route.run() 내부에서 렌더링합니다.

router.match(url?)

아무것도 로드하지 않고 테이블에 대해 URL을 확인합니다. 가져오기 없음, 기록 없음, 리스너 없음, 경로 없음. URL이 base 외부에 있거나, 패턴과 일치하지 않거나, 브라우저에서 다른 출처에 있는 경우 RouteContext 또는 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()가 실행 중인 경로를 렌더링합니다.

router.matches(table)

활성 경로의 경로와 일치하는 두 번째 테이블의 뷰를 어디에나 배치할 수 있는 list()로 렌더링합니다. matches()를 참조하세요.

Route

run()를 제외한 모든 구성원은 라우터에도 있으며, 여기에서 앱이 이를 읽습니다.

에 도달합니다.
멤버
path, pattern, params, search, hash, url활성 일치합니다. search는 URLSearchParams입니다. search, hash 및 url가 라이브 중입니다.
pendinga 탐색이 모듈 또는 로더를 기다리고 있습니다.
error마지막으로 실패한 로드, 다음 탐색으로 지워짐
data서버가 직렬화하기 위해 일치하는 페이지의 load()가 반환한 내용입니다. 부모님의 정보가 없습니다.
depth스택에 레이어 수(일반 페이지의 경우 1개)
go(href, { replace })탐색하고 필요한 경우 모듈을 로드합니다. 절대로 거부하지 않음: 거부하거나 던져서 실패한 모듈은 error.
push(href) 경로를 맨 위에 새 레이어로 열고 닫히는 항목으로 해결합니다. ./를 허용합니다. 일치하지 않거나 실패한 모듈을 거부합니다.
href(path)a({ href })의 경우 경로 앞에 base를 붙입니다. 활성 경로에 대해 ./ 및 ../를 확인합니다.
stop()모든 리스너를 분리하고, 열린 레이어를 닫고, 콘센트를 비웁니다. 멱등성. 중지된 경로는 여전히 마지막 일치 항목으로 응답하고 go()는 아무 작업도 수행하지 않습니다.
run(fn)이 경로를 라우터의 활성 경로로 사용하여 fn를 호출하고 서버에서 결과: route.run(() => renderToString(App))를 반환합니다. 동기식만 가능합니다. fn가 반환되면 경로가 활성화되지 않습니다.

RouteContext

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

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

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

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

PageProps

페이지의 두 번째 인수입니다. 페이지에 필요한 것을 구조화합니다.

로 입력됨 를 참조하세요. 를 참조하십시오
속성
layer페이지가 스택에 있는 위치입니다. 아래 Layer를 참조하세요.
data경로의 load()가 반환된 내용 또는 undefined가 반환되지 않은 내용입니다. PageProps<TData>.
outlet여기서 상위 페이지는 하위 경로를 렌더링합니다. 중첩 경로.
beforeLeave 이 페이지가 삭제되기 전에 요청된 가드를 등록합니다. 가드를 떠나십시오.

Layer

페이지가 스택에 있는 위치와 나가는 길입니다.

와 동일합니다. 에서 무작동
멤버
depth0, 푸시된 레이어의 경우 1 이상
push(href)페이지 내부에서 다른 레이어를 엽니다. Route.push()
close(result?)이 레이어와 그 위의 모든 항목을 닫고 이를 연 push()를 해결합니다. 논쟁이 없다는 것은 기각을 의미합니다. 깊이 0.

DataLoader 및 withLoad()

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

유형

모든 유형은 nuclo-router의 명명된 내보내기이므로 경로에서 가져오지 않고도 페이지, 로더 또는 가드를 입력할 수 있습니다.

를 생성하므로 의 두 번째 인수 의 두 번째 인수
타입그것은 무엇입니까
Params<'/blog/:slug'> 패턴에 대한 params 개체입니다. RouteParams는 유형이 지정되지 않은 모양이고 Match는 일치자의 결과입니다.
PageComponenta 페이지: (ctx, { layer, data, outlet, beforeLeave }) => …
RouteModulea 페이지 모듈: 컴포넌트 자체 또는 { default, load? } import()가
RouteLoader() => RouteModule | Promise<RouteModule>, 선택적으로 운반 load ~에서 withLoad()
RouteTable테이블 createRouter()는 다음을 취합니다: 로더 또는 중첩 테이블에 대한 패턴
RouterOptions{ base?, preload?, idleTimeout?, onNavigate? }
NavigateOptions{ replace? }, go()
NavigationKind'start' | 'push' | 'replace' | 'pop' | 'layer', onNavigate
Outlet유형 outlet() ~에서 PageProps
LeaveGuard(to) => boolean | void | Promise<…>, beforeLeave()가 취하는 것
MatchViewmatches() 뷰는 무엇입니까: 매개변수 입력, 콘텐츠 또는 페이지 출력의 import()

레이어 및 이탈 가드

#

push()는 경로를 바꾸는 대신 현재 경로 위에 경로를 열고 해당 레이어가 닫히는 모든 것을 해결합니다. 페이지는 스택 위에 있는 단일 list()이므로 레이어를 여는 것은 추가입니다. 아래 페이지는 다시 렌더링되지 않으므로 레이어가 닫힐 때 DOM, 포커스 및 절반만 채워진 양식이 모두 그대로 유지됩니다.

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

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

레이어 중첩. 자체 대화 상자가 필요한 대화 상자는 layer.push()를 호출하고 경로에 대한 참조 없이 이를 기다립니다.

  • URL은 최상위 레이어입니다. push()는 기록 항목을 추가하므로 레이어를 연결할 수 있고 뒤로 가기가 레이어를 닫아 취소와 마찬가지로 해당 push()가 해제된 것으로 확인됩니다. Back과 close()는 하나의 코드 경로입니다. close()는 결과를 기록하고 기록을 되돌려보며 history.state의 깊이가 권한입니다. 브라우저는 탭당 약 50개의 항목을 유지하므로 레이어를 오랫동안 탐색한 후에 아래 항목이 사라질 수 있습니다. 그런 다음 close()는 ​​해당 레이어를 닫고 왼쪽에 표시된 페이지에 현재 항목을 다시 씁니다.
  • 일반 탐색은 스택을 지웁니다. 링크 클릭 또는 go()는 레이어가 아닙니다. 모든 것을 대체하고 열려 있는 모든 레이어를 닫으므로 대기 중인 호출자가 없습니다.
  • A 콜드 로드는 독립형 페이지입니다. URL은 최상위 레이어의 이름을 지정하지만 그 아래의 스택은 지정하지 않으므로 레이어의 URL을 열면 layer.depth === 0 및 비활성 close()를 사용하여 깊이 1에서 직접 렌더링합니다. 푸시된 경로는 그 자체로 의미가 있어야 합니다. 죽은 취소 버튼 대신 링크 출력을 제공하기 위해 layer.depth에서 분기합니다.
  • Promise는 클로저에 있기 때문에 다시 로드 후에도 유지되지 않습니다. 결과를 장기적으로 작동하는 흐름을 통한 지름길로 처리하십시오.
  • 기다리는 것이 없습니다. stop()는 열려 있는 모든 레이어를 닫고 탐색에 의해 버려진 레이어는 undefined로 해결됩니다.

가드를 남겨주세요

절반만 채워진 양식과 같이 보관할 가치가 있는 내용이 포함된 페이지는 가드를 등록합니다. 페이지가 삭제되기 전에 요청되며 false(또는 이에 대한 약속)를 반환하면 페이지가 유지됩니다.

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

export default function Edit(_ctx: RouteContext, { beforeLeave }: PageProps) {
  let dirty = false
  beforeLeave(() => !dirty || confirm('Discard your changes?'))
  return form(input({ onInput: () => (dirty = true) }), …)
}
  • 페이지가 이동하는 경우에만. 레이아웃 아래의 하위 탐색이나 페이지를 유지하는 쿼리 또는 해시 변경은 묻지 않습니다. 경비원은 페이지와 함께 살고 죽기 때문에 등록을 취소할 것이 없습니다.
  • Every way out. A 링크, go(), 뒤로 및 앞으로, 레이어의 close() 및 go()를 라우터가 소유하지 않은 URL로 연결합니다(앱에서 to가 null인 경우). 거부된 뒤로 또는 앞으로는 뒤로 이동하므로 주소 표시줄이 페이지에 유지됩니다. 탭을 닫거나 다시 로드하는 것은 beforeunload의 작업입니다.
  • to는 내비게이션이 향하고 있는 RouteContext입니다. 경비원은 가장 안쪽으로 먼저 달려가고 첫 번째 거부에 멈춥니다. 던지는 사람이 페이지를 유지합니다. close(result)도 가드를 통과하므로 결과로 종료되기 전에 플래그를 지우십시오.
  • 라우터는 자신이 작성한 기록 항목만 되돌릴 수 있습니다. 기본 #fragment 링크나 다른 스크립트의 pushState와 같이 이전에 실행되지 않은 항목으로의 뒤로 또는 앞으로는 요청되지 않습니다.

matches() 및 하위 라우터

#

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

app.ts
div(
  aside(
    router.matches({
      '/':           () => nav('Home links'),
      '/blog/new':   () => nav('Drafting tips'),
      '/blog/:slug': ({ slug }) => nav(`More like ${slug}`),
      '*':           () => import('./Sidebar.ts'),   // code-split, rendered once loaded
    }),
  ),
  main(router.outlet()),
)
  • 라우팅 테이블과 일치합니다. 패턴, 우선 순위, 원시 경로 일치 및 base 스트리핑은 라우터 자체이므로 뷰와 페이지는 항상 URL에 동의합니다. router.path와 같은 최상위 레이어의 경로를 읽습니다.
  • A view는 (params, ctx) => content입니다. 여기서 params는 해당 패턴에서 입력되고 ctx는 뷰의 pattern 및 params가 있는 RouteContext입니다. 대신 페이지 모듈의 import()를 반환할 수 있으며, 이는 일단 도착하면 ctx로 호출됩니다.
  • 일치하는 항목이 없으며 아무것도 렌더링되지 않습니다. 대체를 위해 '*' 보기를 추가합니다. 콘센트처럼 start() 이전과 stop() 이후는 비어 있습니다.
  • 경로가 변경될 때만 다시 렌더링됩니다. 따라서 뷰는 페이지를 유지하는 모든 항목에서 DOM를 유지합니다.
  • A 지연 보기는 해당 모듈이 도착할 때까지 아무것도 렌더링하지 않습니다. 실패한 import()는 console.error로 기록되고 router.error를 설정하지 않으며 다음 탐색에서 다시 시도됩니다. 일반 update()는 재시도하지 않습니다.
  • 서버에서 게으른 보기는 해당 모듈이 클라이언트에 있으므로 아무것도 전달하지 않습니다. 첫 번째 HTML에 포함되어야 하는 모든 항목에 대해 동기식 보기를 사용합니다.

서브 라우터

경로가 중첩되므로 하위 라우터는 일반적으로 중첩된 테이블입니다. 하나의 라우터: 섹션 자체의 '/' 페이지는 크롬이고 해당 하위 페이지는 해당 페이지의 outlet()에서 렌더링되므로 크롬은 섹션 내 탐색에서 유지됩니다.

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

또는 생성되었지만 시작되지 않은 자체 라우터 기능을 제공합니다. 부모는 단일 포괄 항목을 소유하고 자식은 자체 패턴, 매개변수 및 폴백에 대해 꼬리를 해결합니다.

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

절대 시작하지 않는 것이 포인트입니다. start()는 리스너를 연결하는 것이며 시작된 두 개의 라우터가 충돌합니다. 거래는 하위가 해상도를 소유하므로 시작된 라우터가 실행되는 경로 로더를 호출할 수 있다는 것입니다. 이 모양은 또한 꼬리가 변경될 때 셸을 다시 빌드합니다. 왜냐하면 라우터가 하나의 경로를 확인하고 셸이 나머지 경로를 자체적으로 해결하기 때문입니다. 중첩 테이블은 라우터가 계속 마운트된 상위 테이블을 제공하므로 해당 기능에 실제로 자체 라우터, 베이스 및 폴백이 필요한 경우가 아니면 이를 선호합니다.

여러 라우터 및 통합 로딩

#

앱은 영역 수만큼 라우터를 가질 수 있습니다. 이를 단순하게 유지하는 규칙은 다음과 같습니다. 문서당 정확히 하나만 시작하고 다른 모든 라우터는 시작되지 않은 상태로 유지하여 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입니다. 사전 로드되거나 열성적인 경로는 절대 반전하지 않습니다. 섹션 사이를 이동하면 레이아웃이 유지되므로 섹션의 로더만 다시 실행됩니다. 레이아웃의 데이터, DOM 및 절반 유형의 입력은 모두 유지됩니다. 동일한 URL로 다시 탐색하면 단일 페이지와 마찬가지로 리프의 로더가 다시 실행되고 해당 행이 다시 작성됩니다.

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

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

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

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

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

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

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

두 번째 라우터는 요청을 받지만 알려주지 않습니다. match()는 URL을 읽고 컨텍스트를 반환하며 사이드바에 대한 어떤 것도 기본 경로 테이블에 입력되지 않습니다. onNavigate는 초기 탐색을 포함하여 해결된 모든 탐색 후에 실행되므로 URL과 보조를 맞춰 독립된 영역을 유지하는 단일 장소입니다.

  • pending는 시작된 라우터에 속합니다. 시작되지 않은 라우터에는 경로, pending 및 error가 없습니다. 해당 영역은 자체 플래그를 추적하고 두 번째 스피너는 첫 번째 플래그 옆에 배치될 수 있습니다. 데모에서 사이드바는 800ms 동안 로드되는 반면 경로의 로더는 900ms 동안 실행되므로 페이지는 사이드바보다 먼저 완료되거나 그 반대 방향으로 완료될 수 있습니다.
  • match()는 아무것도 첨부하지 않습니다. 시작되지 않은 라우터 수에 관계없이 문서를 공유할 수 있습니다. 각각에는 자체 테이블, 기본 및 모듈 캐시가 있으며 그 중 어느 것도 기록을 건드리지 않습니다. 테이블과 매치 비용이 듭니다.
  • Its 로더는 실행되지 않습니다. 시작된 라우터는 일치하는 페이지의 load()를 실행합니다. 시작되지 않은 라우터는 확인만 하므로 보기에 데이터가 필요한 경우 직접 가져오십시오(데모의 refreshSidebar()). 로더를 원하는 경우 중첩 테이블 또는 페이지가 채워지는 영역는 ​​시작된 라우터에서 해당 작업을 유지합니다.
  • Guard 오래된 응답. 느린 사이드바 가져오기는 탐색한 섹션에 도달해서는 안 됩니다. 새로 고침당 생성 카운터를 높이고 더 이상 일치하지 않는 응답을 삭제합니다. 라우터 자체가 추월된 탐색을 폐기하는 방식입니다.
  • A 게으른 matches() 뷰에는 pending도 없습니다. 모듈이 착륙할 때까지 아무것도 렌더링하지 않습니다. 3개의 로딩 시스템, 3개의 독립 시계: 경로의 pending, 자신의 플래그, 아직 존재하지 않는 게으른 보기.

계산된 값

#

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

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

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

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

filter = 'an'
update()

비동기 처리 및 로딩

#

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

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

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

권장 사항

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

API 색인

#

모든 공개 기능과 그 출처.

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

nuclo(명명 내보내기 전용)

nuclo/ssr

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

nuclo/polyfill

  • A 부작용 import, document, Event, CustomEvent, Node, Element, HTMLElement 및 Nuclo* 클래스.

nuclo-router(별도 패키지)

  • createRouter(table, options?): 라우터, start(), match(), outlet(), matches(), go(), push(), href() 및 stop().
  • withLoad(loader, load): 데이터 로더가 해당 청크 옆에서 시작하는 경로입니다.
  • Types: RouteContext, PageProps, Params, DataLoader, RouteTable 등.

글로벌 전용

  • 175 태그 빌더: div, span, …, var_ 및 svgSvg, pathSvg, ….
  • Types: 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()가 실행될 때 해당 노드가 문서에 없었습니다. Nuclo는 해당 노드를 연결한 후에도 추적을 중지하므로 문서에 있는 컨테이너로 렌더링합니다. 다른 두 가지 원인: 조건이 발생하는 when()는 업데이트를 중지하고 첫 번째 실행에서 발생하는 텍스트 함수는 비어 있습니다. update()를 참조하세요.

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

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

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

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

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

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

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

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

Nuclo에 효과가 있나요?

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