Nuclo 문서
Nuclo 설치, 명시적 업데이트를 통한 UI 구성, 타입이 지정된 CSS 스타일링, 서버 측 HTML 렌더링을 위한 실용적인 참조 문서입니다.
npm create nuclo@latest개요
#Nuclo는 TypeScript용 작은 DOM 라이브러리입니다. div() 및 button()와 같은 일반 함수를 사용하여 페이지를 빌드하고 일반 변수에 상태를 유지하며 페이지가 변경되어야 할 때 update()를 호출합니다.
Nuclo에는 가상 DOM, 컴파일러, 프록시 및 신호가 없습니다.
import 'nuclo' let count = 0 const App = () => div( h1(() => `Count: ${count}`), button('Add one', { onClick: () => { count++; update() } }), ) render(App, document.getElementById('app')!)
정신 모델
- 태그 빌더를 사용하여 UI를 구축하세요.
- 상태에 따라 달라지는 모든 값에 대해 함수를 전달합니다.
- 일반 JavaScript로 상태를 변경합니다.
update()에 전화하세요. Nuclo는 기능을 다시 실행하고 DOM를 패치합니다.
변수를 변경해도 DOM에는 영향을 미치지 않습니다. 페이지는 update()를 호출할 때만 변경됩니다.
빠른 시작
#create-nuclo는 Nuclo를 위해 준비된 Vite 및 TypeScript 프로젝트를 스캐폴드합니다.
프로젝트 이름을 묻습니다. 프롬프트를 건너뛰려면 이름과 플래그를 전달하세요. npm를 사용하면 플래그 앞에 --를 넣습니다.
$ npm create nuclo@latest my-app -- --template basic --yes
그런 다음 개발 서버를 설치하고 시작합니다.
$ cd my-app $ npm install $ npm run dev
옵션
[project-name]: 대상 폴더. 기본값은nuclo-app입니다.-t, --template <name>: 복사할 템플릿입니다. 현재는basic만 존재합니다.-y, --yes: 메시지를 표시하지 않고 기본값을 사용합니다.-f, --force: 비어 있지 않은 폴더에 씁니다.-h, --help: 도움말을 인쇄합니다.
설치 및 가져오기
#기존 프로젝트에 Nuclo를 추가하려면:
$ npm install nuclo
또는 pnpm add nuclo, yarn add nuclo 또는 bun add nuclo를 사용하세요. Nuclo에는 종속성이 없습니다.
라우팅은 핵심 패키지의 일부가 아닙니다. nuclo-router로 별도로 배송되며 필요할 때 nuclo 옆에 설치할 수 있습니다.
글로벌스
import 'nuclo'는 다음을 globalThis에 넣습니다.
- 175 태그 빌더: HTML(
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 모듈은 가져오기 순서대로 실행되므로 실패합니다.
import { App } from './app' // app.ts calls div() → ReferenceError: div is not defined import 'nuclo'
항목 파일에 import 'nuclo'를 먼저 넣거나 전역을 사용하는 모든 파일에서 가져옵니다.
명명된 수입품
도우미는 내보내기라고도 합니다. 전역변수와 동일한 기능입니다. getCssText() 및 resetStyles()는 명명된 내보내기 전용입니다. 태그 빌더는 전역 전용이므로 import { div } from 'nuclo'는 작동하지 않습니다.
import { render, update, getCssText } from 'nuclo'
진입점
nuclo: 전역을 등록하고 16개의 도우미와getCssText및resetStyles를 내보냅니다.nuclo/ssr:renderToString,renderManyToString,renderToStringWithContainer및getCssText. 서버 렌더링를 참조하세요.nuclo/polyfill: 노드 및 Bun용 소형 DOM. nuclo/polyfill를 참조하세요.nuclo/types: 유형만 해당. TypeScript를 참조하세요.
TypeScript
#Nuclo는 자체 유형을 제공합니다. 두 가지가 필요합니다.
"moduleResolution": "bundler".node16및nodenext는 아직 지원되지 않습니다.DOM라이브러리입니다. TypeScript는lib를 설정하지 않은 경우 기본적으로 포함됩니다.
프로젝트의 파일 중 하나라도 'nuclo'를 가져오면 모든 파일에 전역 유형이 표시됩니다. 다른 것은 필요하지 않습니다. 예를 들어 테스트용 별도의 tsconfig에서 가져오는 파일이 없으면 직접 유형을 추가하세요.
{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "types": ["nuclo/types"] } }
또는 /// <reference types="nuclo/types" />를 .d.ts 파일에 추가하세요.
유용한 유형
이러한 유형은 전역이므로 가져오기 없이 사용합니다.
NodeModFn<Tag>: 태그 빌더가 반환하는 것. 컴포넌트의 반환 유형으로 사용합니다.NodeModLike<Tag>: 태그 빌더가 인수로 허용하는 모든 것.ExpandedElement<Tag>:render()및hydrate()가 반환하는 내용입니다.ElementTagName: 모든 HTML 태그 이름.ExpandedElementAttributes<Tag>:on*핸들러,onMount및onDestroy를 포함하여 태그 빌더가 허용하는 속성 개체입니다. 속성을 전달하는 컴포넌트에 사용하세요.MountCallback및DestroyCallback:onMount및onDestroy핸들러.WhenBuilder,ListModifier:when()및list()가 반환하는 내용입니다.RegionOptions,RegionType:region()옵션이 사용됩니다.StyleResult,Style,ThemeConfig,CssInstance및 기타 스타일링 유형.import type { StyleResult } from 'nuclo'를 가져올 수도 있습니다.
function Badge(text: string): NodeModFn<'span'> { return span(text) }
ExpandedElement<Tag>는 모든 DOM 멤버를 선택 사항으로 표시합니다. 전체 요소 유형(render(App, root) as HTMLDivElement)이 필요할 때 캐스팅하세요.
태그 빌더
#모든 HTML 태그는 동일한 이름을 가진 전역 함수입니다: div, span, input, … 한 가지 예외는 <var>입니다. var는 예약어이므로 var_입니다. 모든 SVG 태그에는 Svg 접미사(svgSvg, pathSvg, circleSvg, …)가 있습니다.
태그 빌더를 호출해도 아직 요소가 생성되지 않습니다. div('Hi')는 나중에 요소를 생성하는 함수인 builder를 반환합니다. Nuclo는 render()로 마운트하거나 다른 빌더인 when() 또는 list() 내에 배치할 때 이를 호출합니다. 사용할 때마다 새로운 요소가 생성됩니다.
let name = 'Ada' const Card = () => div( { id: 'card', className: 'card' }, h2('Profile'), p(() => `Hello, ${name}`), button('Rename', { onClick: () => { name = 'Grace'; update() } }), ) render(Card, document.body)
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가 추가됩니다. 객체는 속성으로 적용됩니다.
const field = input((el) => { el.autocomplete = 'off' })
요소 가져오기
render()는 루트 요소를 반환합니다. 트리 내부에서는 on("mount", (el) => …) 또는 사용자 정의 수정자를 사용합니다. 분리된 요소를 생성하기 위해 빌더 함수를 직접 호출할 수도 있습니다.
const el = div('Hello')() as HTMLDivElement document.body.append(el)
다음 update() 전에 부착하세요. update()가 실행될 때 문서에 없는 노드는 업데이트를 영구적으로 중지합니다.
SVG
SVG 빌더는 동일한 인수를 사용합니다. Nuclo는 모든 SVG 속성을 setAttribute로 설정하므로 "stroke-width"와 같은 이름은 작성된 대로 작동합니다.
const Icon = () => svgSvg( { viewBox: '0 0 24 24', width: 24, height: 24, className: 'icon' }, circleSvg({ cx: 12, cy: 12, r: 10, fill: 'currentColor' }), )
모든 빌더
HTML: a abbr address area article aside audio b base bdi bdo blockquote body br button canvas caption cite code col colgroup data datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins kbd label legend li link main map mark menu meta meter nav noscript object ol optgroup option output p picture pre progress q rp rt ruby s samp script search section select slot small source span strong style sub summary sup table tbody td template textarea tfoot th thead time title tr track u ul var_ video wbr
SVG: aSvg animateSvg animateMotionSvg animateTransformSvg circleSvg clipPathSvg defsSvg descSvg ellipseSvg feBlendSvg feColorMatrixSvg feComponentTransferSvg feCompositeSvg feConvolveMatrixSvg feDiffuseLightingSvg feDisplacementMapSvg feDistantLightSvg feDropShadowSvg feFloodSvg feFuncASvg feFuncBSvg feFuncGSvg feFuncRSvg feGaussianBlurSvg feImageSvg feMergeSvg feMergeNodeSvg feMorphologySvg feOffsetSvg fePointLightSvg feSpecularLightingSvg feSpotLightSvg feTileSvg feTurbulenceSvg filterSvg foreignObjectSvg gSvg imageSvg lineSvg linearGradientSvg markerSvg maskSvg metadataSvg mpathSvg pathSvg patternSvg polygonSvg polylineSvg radialGradientSvg rectSvg scriptSvg setSvg stopSvg styleSvg svgSvg switchSvg symbolSvg textSvg textPathSvg titleSvg tspanSvg useSvg viewSvg
컴포넌트는 button(...)와 같은 빌더를 반환하는 일반 함수입니다. 컴포넌트 API가 없습니다. 렌더링하거나 트리에 배치할 때까지는 아무 것도 생성되지 않습니다.
function Counter(label: string) { let count = 0 // one per call return button(() => `${label}: ${count}`, { onClick: () => { count++; update() }, }) } render(() => div(Counter('A'), Counter('B')))
함수 내부에 선언된 상태는 해당 인스턴스에 속합니다. 외부에 선언된 상태는 모든 인스턴스에서 공유됩니다. forceUpdate()는 컴포넌트를 다시 호출하여 내부에 선언된 상태를 재설정합니다.
아이들은 단지 논쟁일 뿐입니다:
function Card(heading: string, ...children: NodeModLike<'div'>[]) { return div({ className: 'card' }, h2(heading), ...children) } render(() => Card('Hi', p('one'), p('two')))
속성을 설정하려면 일반 객체를 전달하세요. 여러 객체를 전달할 수 있습니다. 동일한 키의 경우 병합되는 className 및 style를 제외하고 나중 값이 우선합니다. 값은 정적이거나 모든 update()에서 다시 실행되는 매개변수가 없는 함수일 수 있습니다.
let busy = false const save = button( { type: 'submit', disabled: () => busy, 'aria-busy': () => String(busy), 'data-id': 42 }, 'Save', )
update()를 호출합니다. Save 버튼의 disabled와 aria-busy가 이에 따라 바뀝니다.열기 ↗값 설정 방법
- HTML 요소에 해당 이름(
value,checked,disabled,htmlFor,tabIndex, …)의 속성이 있는 경우 Nuclo가 속성을 설정합니다. 그렇지 않으면"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는 정적 클래스를 유지하고 ""를 반환하면 정적 클래스만 남습니다.
let active = false const base = css({ p: 8 }) const item = li(base, { className: 'item' }, { className: () => (active ? 'active' : '') }, 'Home') // class="<base> item", then "<base> item active" after active = true; update()
HTML 요소에는 class 키를 사용하지 마십시오. 원시 속성을 설정하고 css() 클래스를 포함하여 이전에 적용된 모든 클래스를 대체합니다.
인라인 스타일
style는 camelCase 키가 있는 객체 또는 이를 반환하는 함수를 사용합니다.
let progress = 30 const bar = div({ style: () => ({ width: `${progress}%`, backgroundColor: 'teal' }) })
- Nuclo는 장치를 추가하지 않습니다.
12가 아닌"12px"를 작성하세요.opacity및zIndex와 같은 단위 없는 속성은 숫자를 허용합니다. "",null및undefined속성을 지웁니다.- 동적 스타일은 반환하는 키만 설정합니다. 반환을 중지한 키는 이전 값을 유지하므로
""를 반환하여 삭제하세요. - 스타일 문자열(
"color: red") 및 사용자 정의 속성("--gap")은 지원되지 않습니다. 대신css()를 사용하세요.raw를 통해 사용자 정의 속성을 사용합니다.
이벤트를 수신하는 방법에는 on* 속성과 on() 수정자의 두 가지 방법이 있습니다.
on* 속성
속성 객체에서 onClick(on 다음에 대문자가 옴)와 같은 키 아래의 함수는 동적 값이 아닌 이벤트 핸들러입니다.
let query = '' const search = input({ placeholder: 'Search', value: () => query, onInput: (e) => { query = e.currentTarget.value; update() }, onKeyDown: (e) => { if (e.key === 'Escape') { query = ''; update() } }, })
- TypeScript는 키와 요소에서 이벤트 유형을 추론합니다.
e.currentTarget및this가 요소입니다. - Nuclo는 기본 속성을 설정하므로
onClick는el.onclick가 됩니다. 이벤트당 하나의 핸들러가 있습니다. 최신onClick가 이전 핸들러를 대체합니다. onDoubleClick및onDblClick는 모두dblclick를 의미합니다.onclick와 같은 소문자 키는 유형 오류입니다.- 알려진 이벤트가 아닌 이름은 소문자 형식을 수신합니다.
onMyEvent는"myevent"를 수신합니다. - 핸들러에서 발생하는 오류는 포착되지 않습니다.
Type something, then press Escape.
on()
on()는 요소에서 addEventListener를 호출하는 수정자를 반환합니다.
const panel = div( on('scroll', onScroll, { passive: true }), on('click', logClick), on('click', closeMenu), )
- 모든
on()는 수신기를 추가합니다. 모두 순서대로 실행됩니다. options는addEventListener:capture,once,passive,signal로 바로 이동합니다.on()를 빌더에 직접 전달하면 요소 유형이input(on('input', (e) => e.currentTarget.value))로 유추됩니다.- 리스너가 오류를 발생시키면 Nuclo는
console.error로 오류를 기록하고 다른 리스너는 계속 실행됩니다. - Nuclo는 요소를 제거할 때 수신기를 제거합니다. 서버에서
on()는 아무 작업도 수행하지 않습니다. - 하나의
on()값을 여러 요소에서 재사용할 수 있습니다.
사용자 정의 이벤트의 경우 이벤트 유형을 일반으로 전달합니다.
type PickEvent = CustomEvent<{ id: number }> const picker = ul(on<'pick', PickEvent>('pick', (e) => choose(e.detail.id)))
어느 것을 사용할 것인가
일반적인 경우에는 on* 속성을 사용합니다. 이벤트, 리스너 옵션 또는 "my-event"와 같은 사용자 정의 이벤트 이름에 대해 둘 이상의 리스너가 필요한 경우 on()를 사용하십시오.
onMount는 요소가 삽입된 후 한 번 실행됩니다. onDestroy는 Nuclo가 요소를 제거할 때 한 번 실행됩니다. 속성으로 작성하거나 on()를 사용하여 작성하십시오. 두 형식 모두 동일하게 동작하며 요소는 해당 태그에서 입력됩니다.
let seconds = 0 const Timer = () => span( { onMount: () => { const id = setInterval(() => { seconds++; update() }, 1000) return () => clearInterval(id) // runs when the span is removed }, }, () => `${seconds}s`, ) const search = input(on('mount', (el) => el.focus()))
실행될 때
onMount는 요소를 삽입한render(),hydrate(),update()또는forceUpdate()호출이 끝날 때 실행됩니다. 트리가 만들어지는 동안에는 절대로 실행되지 않습니다.onMount가 함수를 반환하는 경우 해당 함수는 요소의onDestroy콜백 이후 소멸 시 실행됩니다.onDestroy는 Nuclo가 요소를 제거할 때 실행됩니다. 즉,list()행이 제거되고,when()분기가 전환되고,into()가 해당 영역을 떠나거나,forceUpdate()가 요소를 삭제합니다. 제거 직전에 실행되므로 해당 요소는 여전히 문서에 있습니다.
원시 DOM 제거
node.remove() 또는 innerHTML = ''를 사용하여 요소를 직접 제거하면 해당 onDestroy 및 정리가 실행되지 않으며 타이머는 계속 실행됩니다. Nuclo에는 unmount()가 없습니다. 정리를 통해 앱을 해체하려면 when()로 래핑하세요.
let mounted = true render(() => div(when(() => mounted, App())), root) mounted = false update() // App's onDestroy hooks and cleanups run
주문 및 오류
- 어느 형식으로든 원하는 만큼 후크를 등록할 수 있습니다. 등록순으로 진행됩니다.
onDestroy는 항상 하위 항목을 먼저 실행한 다음 상위 항목을 실행합니다.- 요소는 인수 순서를 따르는 첫 번째
onMount후크 순서로 마운트되며 각 요소는 모든 후크를 함께 실행합니다. 부모의 후크를 자식이 의존하는 경우 자식보다 먼저 전달합니다. - 던지는 후크는
console.error로 기록됩니다. 다른 후크는 여전히 실행됩니다. - 콜백은 아무것도 반환하지 않거나 정리 함수를 반환해야 하므로
async로 만들지 마세요. 반환된 Promise는 정리 용도로 사용되지 않습니다. 내부에서 비동기 작업을 시작하세요:{ onMount: (el) => { void load(el) } }. - 후크는 서버에서 실행되지 않습니다. 클라이언트에서는
hydrate()이후에 실행됩니다.
Nuclo는 자체적으로 페이지를 업데이트하지 않습니다. 상태를 변경한 다음 update()에 전화하세요.
let count = 0 const Counter = () => button(() => `Clicked ${count} times`, { onClick: () => { count++; update() }, }) render(Counter)
Clicked 0 times
원하는 만큼 상태를 변경한 다음 update()를 한 번 호출합니다.
let first = 'Ada' let last = 'Lovelace' render(() => p(() => `${first} ${last}`)) first = 'Grace' last = 'Hopper' update() // one pass, both changes
이벤트 핸들러, 타이머, 페치 콜백 또는 onMount에서 update()를 호출합니다. 동적 값 내부에서 호출하지 마세요.
매개변수가 없는 함수는 동적 값입니다. Nuclo는 요소를 빌드할 때 한 번 실행하고 모든 update()에서 다시 실행합니다. 텍스트 하위 항목, 속성 값, style 및 className에 대해 동적 값을 사용합니다. 나타나거나 사라지거나 반복되는 자식 노드의 경우 when() 및 list()를 사용하세요.
let name = 'Alice' let busy = false const Profile = () => div( h1('Welcome'), // static p(() => `Hello, ${name}`), // dynamic text button({ disabled: () => busy }, 'Save'), )
update()마다 다시 계산됩니다. forceUpdate()는 트리를 다시 구성하므로 정적 텍스트도 반영됩니다.열기 ↗static: Hello, Alice
dynamic: Hello, Alice
Nuclo는 매개변수 개수(fn.length)를 확인합니다. 매개변수를 선언하는 함수는 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 실행을 던지는 텍스트 함수는 영원히 비어 있습니다.
페이지의 모든 동적 값에 대해 하나의 동기 전달을 실행합니다. ID를 사용하면 해당 scope() ID로 표시된 요소 내부의 내용만 업데이트됩니다. 각 패스는 다음 단계를 순서대로 실행합니다.
list(): 항목을 다시 읽고 행을 추가, 이동 또는 제거합니다.when(): 조건을 다시 확인합니다. 다른 분기가 활성화될 때만 분기가 다시 작성됩니다.style및className를 포함한 동적 속성.- 동적 텍스트.
onMount입니다.
- 그것은 모든 것을 실행합니다. 모든 동적 값은 데이터가 변경되지 않은 경우에도 모든 호출에서 실행됩니다. DOM 쓰기만 건너뜁니다. 동적 값을 신속하고 부작용 없이 유지하세요.
- 문서의 노드만.
update()가 실행될 때 문서에 노드가 없으면 Nuclo는 노드 추적을 중지합니다. 연결한 후에도 다시 업데이트되지 않습니다. 이는list()및when()에도 적용됩니다. 문서에 있는 컨테이너로 렌더링합니다. - 정적 값은 절대 변경되지 않습니다. 일반 문자열 및 속성 값은 요소가 빌드될 때 고정됩니다. 다시 빌드하려면
forceUpdate()를 사용하세요. - Errors. 텍스트 및 속성 오류가 발견되고 패스가 계속됩니다.
when()조건이 발생하면 오류가 기록되고when()업데이트가 중지됩니다.list()items또는renderItem함수가 발생하면 오류가update()를 이스케이프하고 나머지 단계가 실행되지 않습니다. - Server.
update()는 서버에서 아무 작업도 수행하지 않습니다.
This dynamic value has run 1 times.
scope()
#하나 이상의 범위 ID에 전달되는 요소를 제공합니다. 그런 다음 update('id')는 요소 자체 속성을 포함하여 해당 요소 내부에 있는 내용만 업데이트합니다.
let cartItems: string[] = [] let user = 'Ada' const App = () => div( header(() => `Hi, ${user}`), aside(scope('cart'), span(() => `Items: ${cartItems.length}`)), ) render(App) cartItems.push('apple') user = 'Grace' update('cart') // "Items: 1", the header still says "Hi, Ada" update() // everything, the header now says "Hi, Grace"
- 요소에는
scope('cart', 'sidebar')라는 여러 ID가 있을 수 있습니다. update('a', 'b')는a또는b내부의 모든 것을 업데이트합니다. 여러 요소가 동일한 ID를 사용하면 모두 업데이트됩니다.- 중첩된 범위를 사용하는
update('outer')에는 내부 범위가 포함됩니다.update('inner')는 외부에 닿지 않습니다. - 문서에 요소가 없는 ID는 nothing를 업데이트하고 오류가 발생하지 않으므로 철자를 확인하세요.
scope()는 서버에서 아무 작업도 수행하지 않습니다.
조건에 따라 콘텐츠를 렌더링합니다. Nuclo는 모든 update()에서 조건을 다시 확인합니다. 첫 번째 참 조건이 승리합니다. true가 아닌 경우 .else() 콘텐츠가 렌더링됩니다. .else()가 없으면 아무것도 렌더링되지 않습니다.
let status: 'loading' | 'error' | 'ready' = 'loading' const Status = () => div( when(() => status === 'loading', p('Loading…')) .when(() => status === 'error', p('Something went wrong')) .else(p('Ready')), )
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()를 두 번 호출하면 마지막 호출이 승리합니다.
list()
#항목당 하나의 행을 렌더링합니다. 모든 update()에서 Nuclo는 items()를 다시 호출하고 ID(===)를 기준으로 새 항목을 기존 행과 일치시킵니다.
let fruits = ['Apple', 'Banana'] const Fruits = () => ul(list(() => fruits, (fruit) => li(fruit))) render(Fruits) fruits.push('Cherry') update()
행 일치 방법
- 여전히 존재하는 항목이 해당 행을 유지합니다. Nuclo는 필요한 경우 행을 이동하고
renderItem를 다시 호출하지 않습니다. - 새 항목은 새 행을 가져옵니다. 제거된 항목은 해당 행을 잃고 해당
onDestroy후크가 실행됩니다. - 중복 항목은 각각 고유한 행을 갖습니다.
- 현재 배열(
push,splice,sort)을 변경하거나 새 배열을 반환할 수 있습니다.
유지된 행은 다시 렌더링되지 않으므로 변경되는 필드에 동적 값을 사용하십시오.
const todos = [{ text: 'Write docs', done: false }] const Todos = () => ul( list(() => todos, (todo) => li({ className: () => (todo.done ? 'done' : '') }, () => todo.text), ), ) render(Todos) todos[0].done = true update() // same <li>, its class is now "done"
복사본은 새 항목이므로 어레이를 todos.map((t) => ({ ...t }))로 교체하면 모든 행이 다시 빌드됩니다.
renderItem가 반환하는 것
li(...),tr(...)또는 SVG 빌더와 같은 하나의 요소를 반환합니다. 항목을 건너뛰려면null또는undefined를 반환하세요. 문자열, 텍스트 노드 및 조각은 아무것도 렌더링하지 않습니다.when()또는list()를 직접 반환하지 마세요. 행 요소li(todo.text, when(() => todo.done, ' ✓'))안에 넣으세요.- 항목을 숨기려면
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()는 어느 곳에서나 어떤 깊이에서도 채울 수 있습니다. 레이아웃은 콘텐츠를 소품으로 받지 않으며 콘텐츠를 구성하는 컴포넌트는 콘텐츠가 어디에 배치되는지 알지 못합니다.
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, 초점 및 형태 유지. 페이지 위의 대화 상자와 같은 레이어에 사용하세요.
let open = false const App = () => div( region({ id: 'main', type: 'stack' }), into('main', form(input({ placeholder: 'Keeps its text' }))), when(() => open, into('main', div('On top'))), )
여러 지역을 동시에
단일 into()에서 두 개 이상의 영역을 채우려면 객체를 전달하십시오. 값은 하나의 하위 항목일 수도 있고 하위 항목의 배열일 수도 있습니다. 어느 쪽이든 중첩됩니다. 다른 보기의 콘텐츠 내부에 있는 into()는 자체 지역을 대상으로 합니다.
const Page = () => into({ main: article(h1('Eager route')), sidebar: [h2('Related'), ul(li('One'), li('Two'))], })
트리 깊은 곳에서 열린 대화 상자
오버레이 영역은 루트에 위치합니다. 모든 깊이의 모든 컴포넌트는 into()를 when()로 래핑하여 대화 상자를 엽니다. when()를 닫으면 해당 영역에서 대화 상자가 제거됩니다.
let confirming = false const App = () => div( Page(), div({ className: 'overlay' }, region({ id: 'overlay' })), ) // Deep inside Page() const DeleteButton = () => div( button('Delete', { onClick: () => { confirming = true; update() } }), when(() => confirming, into('overlay', dialog({ open: true }, p('Delete this item?'), button('Cancel', { onClick: () => { confirming = false; update() } }), ), ), ), )
목록 행의 토스트
list()의 각 행은 뷰입니다. 누적된 영역에는 해당 항목이 순서대로 표시되며 배열에서 항목을 제거하면 알림이 제거됩니다.
const toasts: { text: string }[] = [] const notify = (text: string) => { toasts.push({ text }) update() } const dismiss = (toast: { text: string }) => { toasts.splice(toasts.indexOf(toast), 1) update() } const App = () => div( button('Save', { onClick: () => notify('Saved.') }), div({ className: 'toasts' }, region({ id: 'toasts', type: 'stack' })), list(() => toasts, (toast) => into('toasts', div(toast.text, button('×', { onClick: () => dismiss(toast) }))), ), )
페이지에서 설정한 페이지 제목
기본 type: 'latest'를 사용하면 최신 보기가 우선합니다. 레이아웃은 제목을 소유하고 열려 있는 페이지가 제목의 이름을 지정합니다. into('title', …)가 없는 페이지는 empty 텍스트를 남깁니다.
let page: 'home' | 'settings' = 'home' const Layout = () => div( h1(region({ id: 'title', empty: 'My app' })), when(() => page === 'home', Home()).else(Settings()), ) const Home = () => p('Welcome.') const Settings = () => div( into('title', 'Settings'), form(/* … */), )
뷰 내부의 동적 값
into()의 내용은 일반 Nuclo 내용입니다. 그 안의 기능은 상태에 따른 값이며 지역이 어디에 있든 update()에서 새로 고쳐집니다.
const selected = new Set<string>() const Layout = () => div( header(region({ id: 'status' })), Table(), ) const Table = () => div( into('status', () => `${selected.size} selected`), list(() => rows, (row) => label( input({ type: 'checkbox', onChange: (e) => { e.currentTarget.checked ? selected.add(row.id) : selected.delete(row.id) update() } }), row.name, ), ), )
왔다가 가는 지역
대시보드의 into()가 한 번 작성됩니다. 사용자가 로그아웃한 동안에는 main 영역이 없으므로 보기가 기다립니다. 로그인하면 지역이 구축되고 보기가 표시됩니다. 로그아웃하면 지역이 제거되고 보기가 다시 기다립니다.
let user: { name: string } | null = null const App = () => div( when(() => user !== null, main(region({ id: 'main' })), ).else( LoginForm(), ), into('main', Dashboard()), )
자체 지역이 포함된 뷰
뷰는 region()를 포함할 수 있으며 다른 뷰는 이를 대상으로 할 수 있습니다. 여기서 편집기 페이지는 tabs 영역을 가져오고 각 탭이 이를 채웁니다. 편집기가 닫히면 해당 영역도 함께 이동됩니다.
let tab: 'code' | 'preview' = 'code' const Editor = () => into('main', div( nav( button('Code', { onClick: () => { tab = 'code'; update() } }), button('Preview', { onClick: () => { tab = 'preview'; update() } }), ), section(region({ id: 'tabs' })), ), when(() => tab === 'code', into('tabs', textarea())) .else(into('tabs', iframe({ src: '/preview' }))), )
라이브 예시
import 'nuclo' let page: 'home' | 'article' | null = 'home' let dialog = false const go = (next: typeof page) => () => { page = next dialog = false update() } const Layout = () => div( nav( button('Home', { onClick: go('home') }), button('Article', { onClick: go('article') }), button('Close page', { onClick: go(null) }), ), main( region({ id: 'main', type: 'stack', empty: p('Nothing open.') }), ), aside(region({ id: 'side', empty: p('No related links.') })), ) // Pages fill the layout's regions from outside it. const Home = () => into('main', input({ placeholder: 'Type here, then open the dialog' }), button('Open dialog', { onClick: () => { dialog = true; update() }, }), ) const Article = () => into({ main: article(h2('Article'), p('One into() fills both regions.')), side: [h2('Related'), ul(li('One'), li('Two'))], }) // Stacks over Home, so Home keeps its DOM and the text you typed. const Dialog = () => into('main', div( p('On top of Home.'), button('Close', { onClick: () => { dialog = false; update() } }), ), ) export const RegionApp = () => div( Layout(), when(() => page === 'home', Home()), when(() => page === 'article', Article()), when(() => dialog, Dialog()), )
into() 한 번으로 두 리전을 채웁니다.열기 ↗No related links.
update()는 동적 값만 실행합니다. h1(labels[lang].title)의 텍스트와 같은 정적 값은 트리가 작성될 때 고정됩니다. forceUpdate()는 컴포넌트를 다시 호출하고 정적 값이 포함된 일치하도록 라이브 DOM를 패치합니다. 언어 전환과 같이 앱 전반에 걸쳐 드물게 변경하는 경우에 사용하세요. 다른 모든 것에는 update()를 사용하십시오.
const labels = { en: { title: 'Hello' }, pt: { title: 'Olá' } } let lang: keyof typeof labels = 'en' const App = () => div(h1(labels[lang].title)) render(App, root) lang = 'pt' forceUpdate() // same <h1> element, now "Olá"
update()는 동적인 lang 행만 업데이트합니다. forceUpdate()는 App을 다시 호출해 같은 <h1>을 패치합니다. 명시적인 형식은 페이지 전체가 아닌 이 트리만 다시 구성합니다.열기 ↗lang = 'en'
Hello
Static text, fixed on build.
인수가 없으면 forceUpdate()는 render(App) 또는 hydrate(App)로 렌더링된 모든 루트를 다시 작성합니다. render(App())로 렌더링된 루트는 건너뜁니다. 컴포넌트가 발생하면 오류가 기록되고 해당 루트는 이전 DOM를 유지합니다. 새 빌드가 적용되는 동안 나중에 발생하는 오류도 기록되지만 이미 패치된 부분은 패치된 상태로 유지됩니다.
남는 것과 변화하는 것
- 자식은 태그별로 순서대로 일치됩니다. 일치하는 요소가 재사용되므로 포커스와 입력된 입력 값이 유지되고 해당
onMount후크가 다시 실행되지 않습니다. 한 자녀의 태그가 다르면 해당 자녀와 이후의 모든 형제 자매는 새로 만들어집니다. - 텍스트, 속성, 클래스, 스타일, 리스너,
when()분기 및list()행은 새 빌드를 따릅니다. - 새 요소가 생성되고 해당
onMount후크가 실행됩니다. 새 빌드에서 더 이상 만들지 않는 요소는 제거되고 해당onDestroy후크가 실행됩니다. list()행은 항목이 아닌 위치로도 일치하므로 재사용된 행 요소가 결국 다른 항목을 표시할 수 있습니다.- 새 빌드에서 더 이상 설정되지 않는 속성은 제거되지 않습니다.
EExplicit form. forceUpdate(App(), parent)는 parent에서 하나의 트리를 다시 작성하고 해당 루트를 반환합니다. 이후 호출을 위해 루트를 등록하지 않습니다. hydrate()와 마찬가지로 루트가 parent의 첫 번째 노드가 될 것으로 예상합니다. 첫 번째 노드에 다른 태그가 있으면 교체됩니다.
서버에서 forceUpdate()는 아무 작업도 수행하지 않습니다.
render()
#트리를 구축하고 해당 루트 요소를 parent에 추가합니다. parent의 기본값은 document.body입니다. 루트 요소를 반환합니다.
import 'nuclo' import { App } from './app.ts' render(App, document.getElementById('app')!)
- 컴포넌트(
render(App)) 또는 컴포넌트가 반환하는 빌더(render(App()))를 전달할 수 있습니다. DOM도 동일합니다.render(App)만forceUpdate()가 나중에 루트를 다시 빌드할 수 있으므로 선호합니다. - 첨부합니다.
parent를 지우지 않으므로 두 번 호출하면 두 개의 복사본이 추가됩니다. render()가 반환되면 트리의 모든onMount가 실행되었습니다.- 문서에 있는 요소로 렌더링합니다.
update()를 참조하세요. - 루트는
when()또는list()가 아닌div(...)와 같은 요소여야 합니다. index는 내부입니다. 요소가 삽입되는 위치는 선택하지 않습니다. 그것을 그대로 두십시오.
render(App)로 전달된 컴포넌트는 매개 변수를 사용하지 않아야 합니다. 필수 매개변수는 유형 오류입니다. 선택적인 유형 검사 중 하나는 런타임에 render()가 발생합니다. 소품을 사용하는 컴포넌트를 래핑합니다.
const Page = (props: { name: string }) => div(h1('Hi ', props.name)) render(() => Page({ name: 'Ana' }), root)
hydrate()
#는 renderToString()가 만든 HTML를 인수합니다. 기존 요소를 재사용하고, 이벤트 리스너를 연결하고, 동적 값 when() 및 list()를 연결하므로 나중에 update()가 작동합니다. 그런 다음 onMount 후크를 실행합니다. parent의 기본값은 document.body입니다. 루트 요소를 반환합니다.
import 'nuclo' import { App } from './app.ts' hydrate(App, document.getElementById('root')!)
- 서버와 동일한 컴포넌트, 동일한 데이터를 사용하십시오.
- 전용 용기에 수화시킵니다. 공백은 괜찮지만 앱의 루트는 첫 번째 노드여야 합니다. 다른 노드가 먼저 오면 Nuclo는 해당 노드를 교체하고 서버 복사본을 남겨 둡니다.
render()와 마찬가지로App()가 아닌App를 전달하므로forceUpdate()는 나중에 루트를 다시 빌드할 수 있습니다.App는 매개변수를 사용하지 않아야 합니다. 옵션인hydrate()는 아무것도 자동으로 부착하지 않습니다. 소품을 사용하는 컴포넌트를 래핑합니다:hydrate(() => Page(props), root).- HTML 서버가 없으면
render()를 사용하세요.
서버와 클라이언트가 다른 경우
클라이언트가 승리합니다.
- 다른 루트 태그: 해당 노드는 새로운 DOM로 대체됩니다. 빈 컨테이너: 새로운 DOM가 추가됩니다.
- 다른 텍스트가 패치되었습니다. 다양한
list()항목과when()분기가 클라이언트를 따릅니다. - 추가 서버 하위 항목이 제거되고 누락된 항목이 생성됩니다.
- 서버에서 작성했지만 클라이언트에서 설정하지 않은 속성은 그대로 유지됩니다.
renderToString()는 HTML 주석을 각 텍스트 노드 앞에 <!-- text-0 -->와 같은 마커로 씁니다. hydrate()에는 주석이 필요하므로 서버 HTML에서 주석을 제거하지 마십시오. 마커가 없으면 하이드레이션은 list() 행을 삭제하고 list() 및 when() 업데이트를 중지합니다.
서버 렌더링
#서버의 HTML 문자열로 컴포넌트를 렌더링합니다. 브라우저에서 동일한 컴포넌트로 hydrate()를 호출하여 HTML를 대화형으로 만듭니다. 서버 런타임에는 document가 없으므로 먼저 nuclo/polyfill를 가져옵니다.
let count = 0 const heading = css({ color: 'red' }) export const App = () => main( h1(heading, () => `Count: ${count}`), button({ onClick: () => { count++; update() } }, '+1'), )
import 'nuclo/polyfill' import 'nuclo' import { renderToString, getCssText } from 'nuclo/ssr' import { App } from './app.ts' const html = renderToString(App) const page = `<!doctype html> <html> <head><style id="nuclo-styles">${getCssText()}</style></head> <body> <div id="root">${html}</div> <script type="module" src="/client.js"></script> </body> </html>`
import 'nuclo' import { App } from './app.ts' hydrate(App, document.getElementById('root')!)
스타일 태그에 id="nuclo-styles"를 유지합니다. 클라이언트는 해당 요소를 재사용하고 새 규칙만 추가합니다. getCssText()를 참조하세요.
서버에서 실행되는 것
- 각
renderToString()호출은 트리를 구축하고 각 동적 값을 한 번 실행합니다. - 이벤트 핸들러
onMount및onDestroy는 실행되지 않습니다.update(),scope()및forceUpdate()는 아무것도 수행하지 않습니다. - 렌더링은 동기식입니다. 먼저 데이터를 로드한 다음 렌더링하세요.
- 렌더링 후에는 등록된 상태가 유지되지 않으므로 하나의 프로세스가 여러 요청을 처리할 수 있습니다. 요청 데이터를
renderToString(Page(user))와 같은 인수로 전달합니다. 모듈 수준 변수는 모든 요청에서 공유됩니다. - 서버에서
render()를 호출하지 마십시오. 공유document.body에 추가됩니다.
renderToString()
빌더(App()), 컴포넌트(App) 또는 DOM 노드를 사용합니다. null 및 undefined는 ""를 반환합니다. 루트는 요소여야 합니다. renderToString(Page)로 전달된 컴포넌트는 매개 변수를 사용하지 않아야 하므로 데이터를 사용하는 컴포넌트에는 Page(user)를 전달합니다.
renderToString(p('Hello')) // '<p><!-- text-0 -->Hello</p>'
- 텍스트 및 속성 값은 HTML로 이스케이프됩니다.
script()및style()내부의 텍스트는 닫는 태그만 분리된 상태로 있는 그대로 작성되므로 사용자 입력을 거기에 넣지 마십시오. - 던지지 않습니다. 컴포넌트,
list()함수 또는when()조건이 발생하거나 폴리필이 누락된 경우 오류를 기록하고""를 반환합니다. 요청이 실패하면 빈 결과를 확인하세요. 빈 렌더링만 발생시키는 텍스트 또는 속성 함수입니다.
renderManyToString()
inputs.map(renderToString)와 동일합니다.
renderToStringWithContainer()
출력을 하나의 요소로 래핑합니다. containerTag의 기본값은 "div"이고, containerAttrs는 {}입니다. 속성 값이 이스케이프됩니다. 속성 이름은 주어진 대로 작성하므로 class와 같은 HTML 이름을 사용합니다. 사용자 입력을 태그로 전달하지 마십시오.
renderToStringWithContainer(p('Hi'), 'section', { id: 'main', class: 'page' }) // '<section id="main" class="page"><p><!-- text-0 -->Hi</p></section>'
nuclo/polyfill
#작은 DOM이므로 Nuclo는 노드 및 Bun에서 트리를 구축할 수 있습니다. 첫 번째 렌더링 전에 한 번 가져옵니다. 대부분의 앱에는 일반 가져오기만 필요합니다.
window가 없을 경우globalThis.document,Node,Element,HTMLElement를 설정하되, 누락된 것만 설정합니다.window를 생성하지 않습니다.- 브라우저나 jsdom에서는 아무 작업도 수행하지 않습니다.
Event및CustomEvent는 런타임 자체 생성자입니다.Node,Element및HTMLElement는 폴리필 클래스의 별칭입니다.
브라우저가 아닌 트리 구축용 DOM입니다. innerHTML 및 getElementById가 없으며 querySelector()는 null를 반환하고 이벤트 리스너는 무시됩니다. renderToString()를 사용하여 HTML를 얻으세요.
입력된 스타일 개체를 생성된 CSS 클래스로 변환합니다. 결과를 태그 빌더에 전달합니다. 브라우저에서 규칙은 하나의 <style id="nuclo-styles"> 요소로 들어갑니다. 서버에서는 getCssText()가 이를 반환합니다.
const card = css({ p: 16, rounded: 8, bg: '#fff7ed', hover: { bg: '#ffedd5' }, }) const Card = () => div(card, 'Simple card')
css()는 기본 선언에 대해 하나의 클래스를 만들고 각 의사 키, 선택기 및 미디어 블록에 대해 하나의 클래스를 만듭니다. 위의 카드는 두 가지 클래스를 얻습니다.- 클래스 이름은 콘텐츠의 해시입니다. 동일한 스타일은 클래스를 공유하며 서버와 클라이언트는 동일한 이름을 생성합니다.
- 모듈 수준에서 스타일을 한 번 정의합니다.
css()는 개체별로 캐시하므로 호출할 때마다 새 개체가 매번 다시 처리됩니다. - 스타일 개체를
css()에 전달한 후 변경하면 아무 작업도 수행되지 않습니다. - 모든 스타일은 페이지 또는 서버 프로세스의 수명 동안 등록된 상태로 유지됩니다. 데이터의 너비와 같이 항목별로 변경되는 값의 경우
style속성를 사용하세요.
명명된 스타일
읽을 수 있는 클래스 이름을 얻으려면 먼저 이름을 전달하십시오.
const pageTitle = css('page-title', { text: 24, hover: { color: 'red' } }) pageTitle.className // "page-title page-title-<hash>"
- 기본 클래스는 바로 이름입니다. 다른 블록은
name-<hash>를 얻습니다. - 이름은 유효한 클래스 이름이어야 합니다. 그렇지 않으면 Nuclo가 경고하고 생성합니다.
- 이름은 전역적입니다. 두 가지 다른 스타일이 동일한 이름을 사용하는 경우 Nuclo가 경고합니다.
결과 사용
const card = css({ p: 8 }) div(card) // best div(card, { className: 'featured' }) // className merges div(card, { class: 'featured' }) // replaces: only "featured" is left
String(card) 및 card.className는 모두 클래스 이름을 반환합니다.
스타일 객체
#css()에 전달하는 개체는 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"를 작성하세요.
css({ p: 16, m: 0, opacity: 0.5, leading: 1.5, z: 10, w: '50%' }) // padding:16px; margin:0; opacity:0.5; line-height:1.5; z-index:10; width:50%
축약형
| 키 | CSS |
|---|---|
p, pt, pr, pb, pl | 패딩 및 각 사이드 |
px / py | 좌우 패딩 / 상하 패딩 |
m, mt, mr, mb, ml, mx, my | 여백도 동일 |
w, h, minW, maxW, minH, maxH | 폭, 높이 및 최소 및 최대 |
size | 폭 및 높이 |
bg | 배경 |
text / font / weight | font-size / font-family / font-weight |
leading / tracking | line-height / letter-spacing |
align / items / justify | text-align / align-items / justify-content |
z / rounded / shadow / select | z-index / border-radius / box-shadow / user-select |
이 키에는 true가 사용됩니다.
| 키 | CSS |
|---|---|
row | display: flex; flex-direction: row |
col | display: flex; flex-direction: column |
center | align-items: center; justify-content: center (표시를 설정하지 않음) |
truncate | overflow: hidden; text-overflow: ellipsis; white-space: nowrap |
의사 상태
hover, focus, focusVisible, focusWithin, active, visited, disabled, enabled, checked, required, invalid, valid, readOnly, first(:first-child), last(:last-child), only(:only-child), odd, even, empty, placeholderShown, placeholder, before, after, selection, marker, firstLine 및 firstLetter. 그들은 중첩됩니다: hover: { focus: {…} }는 :hover:focus를 제공합니다.
선택기 및 at-규칙
&로 시작하는 키는 선택자이며 각 &는 클래스가 됩니다. @로 시작하는 키는 at-rule(@media, @container 또는 @supports)입니다.
const menu = css({ p: 8, '& > li': { py: 4 }, '& > li:hover': { color: 'red' }, '&:is(.dark *)': { color: 'white' }, '@media (min-width: 768px)': { p: 16 }, })
- 키는
&로 시작해야 합니다.".dark &"와 같은 키는 무시되므로"&:is(.dark *)"를 작성하세요. - 두 개의 중첩된
@media블록이and와 결합됩니다. 문법이 처음에만 유형을 허용하기 때문에 미디어 유형(print,only screen)이 이끄는 쿼리가 먼저 배치됩니다. 평면화할 수 없는 쌍(두 개의 미디어 유형 또는 선행not)은 경고하고 내부 쿼리만 사용합니다. 다른 중첩된 at-규칙은 외부 규칙을 대체합니다.
raw
모든 속성에 대해 raw를 사용하고 이름은 CSS와 동일하게 작성됩니다. 숫자는 여전히 px를 가져오므로 문자열을 전달합니다.
css({ raw: { '--gap': '8px', '-webkit-line-clamp': '2' } })
다른 일반 키는 케밥 케이스로 변환되어 그대로 작성되므로 colr: 'red'와 같은 오타는 브라우저가 삭제하는 선언이 됩니다. 의사 상태가 아닌 키, 화면, & 또는 @ 키 아래에 중첩된 객체는 무시됩니다. TypeScript는 둘 다 보고합니다.
스타일을 결합합니다. 두 입력이 동일한 속성을 설정하면 나중에 입력이 우선합니다.
const base = css({ p: 8, color: 'red' }) const blue = css({ color: 'blue' }) cx(base, blue) // padding: 8px; color: blue cx(base, false, null, 'extra') // base's classes plus "extra" div(cx(base, blue), 'Blue text')
- 거짓 입력을 건너뛰고, 배열을 평면화하고, 문자열을 분할하고 중복을 제거합니다.
- 동일한 블록(기본, 동일한 의사, 동일한 미디어 쿼리)에 대해 생성된 클래스가 하나의 새로운 클래스로 병합됩니다. 다른 클래스 이름은 변경되지 않고 전달됩니다.
cx()만이 인수 순서를 알고 있습니다.div(base, blue)를 사용하면 두 클래스가 모두 추가되고 마지막에 생성된 규칙이 적용됩니다. 하나의cx()호출에 서로 재정의되는 스타일을 넣습니다.- 명명된 스타일의 블록이 다른 입력의 블록과 병합될 때 병합된 클래스는 기본 이름이 아닌
name-<hash>입니다. 병합되지 않는 블록은 클래스를 유지하므로cx(pageTitle, 'extra')에는 여전히page-title가 있습니다.
동적 클래스
항상 cx(...)를 반환하는 함수를 전달합니다.
const baseButton = css({ px: 12, py: 8, border: '1px solid #ccc' }) const activeButton = css({ bg: '#ff3f00', color: 'white' }) let active = false const Toggle = () => button( () => cx(baseButton, active && activeButton), () => (active ? 'Active' : 'Inactive'), { onClick: () => { active = !active; update() } }, )
함수는 처음 실행 시 스타일 결과를 반환해야 합니다. () => active && activeButton는 클래스 대신 "false" 텍스트를 렌더링합니다.
테마에 바인딩된 자체 css, cx, variants, keyframes 및 globalStyle를 반환합니다. 테마는 값에 이름을 부여합니다. 아래 테마를 사용하면 borderColor: 'border'가 #e5e7eb를 쓰고 TypeScript가 이름을 자동 완성합니다. screens의 각 항목은 md: {…}와 같은 미디어 쿼리의 스타일 키가 됩니다.
export const { css, cx, variants } = createCss({ colors: { primary: '#ff3f00', border: '#e5e7eb' }, radii: { card: '12px' }, screens: { md: '(min-width: 768px)' }, }) const panel = css({ p: 12, rounded: 'card', border: '1px solid', borderColor: 'border', md: { p: 24 }, })
| 테마 키 | 사용 대상 |
|---|---|
colors | bg, backgroundColor, color, borderColor 및 각 측면, outlineColor, caretColor, textDecorationColor |
fonts | font, fontFamily |
shadows | shadow, boxShadow |
radii | rounded, borderRadius |
screens | md: {…} |
- 테마 이름은 전체 값일 때만 해결됩니다.
border: "1px solid primary"가 해결되지 않았습니다. @가 없는 화면 값은@media <value>가 됩니다.@로 시작하는 값은 작성된 대로 사용됩니다(예:"@container (min-width: 400px)").- 화면은
createCss()로 만든 인스턴스에만 존재합니다. 전역css()에는 키가 없지만@media키를 허용합니다. - 모든 인스턴스와 전역 도우미는 하나의 스타일시트를 공유합니다. 스크린 미디어 규칙은 스크린을 사용하는 순서가 아닌 선언된 순서대로 출력되므로
min-width스크린을 작은 것부터 큰 것 순으로 나열합니다. 앱당 하나의createCss()인스턴스를 사용하여 해당 순서가 테마의 순서가 되도록 합니다. theme결과에 합격하신 테마입니다.
버튼의 의도 및 크기와 같은 옵션 세트에 대한 스타일을 반환하는 함수를 빌드합니다.
const buttonClass = variants({ base: { px: 12, py: 8, rounded: 6 }, variants: { intent: { primary: { bg: '#2563eb' }, danger: { bg: '#dc2626' } }, size: { sm: { text: 12 }, lg: { text: 18 } }, block: { true: { w: '100%' } }, }, defaultVariants: { intent: 'primary', size: 'sm' }, compoundVariants: [{ intent: 'danger', size: 'lg', css: { weight: 700 } }], }) button(buttonClass(), 'Save') // primary, sm button(buttonClass({ intent: 'danger', size: 'lg', block: true }), 'Delete') // bold, full width
- 스타일은
base, 각 그룹에서 선택한 값, 일치하는 각 화합물 순서로 병합됩니다. 최신 스타일은 속성별로 승리합니다. - 누락된
undefined또는null옵션은 기본값을 사용합니다. 기본값이 없는 그룹은 건너뜁니다. true및false키만 있는 그룹은 부울을 사용합니다.- 나열된 모든 그룹이 일치할 때 화합물이 적용됩니다(기본값 포함).
- 각 선택 항목은 한 번만 작성되고 캐시됩니다.
keyframes()는 애니메이션을 등록하고 생성된 이름을 반환합니다. 프레임 키는 from, to 및 백분율(예: "50%" 또는 "0%, 100%")입니다. 프레임은 평면 속성만 사용합니다. 동일한 프레임은 동일한 이름을 반환합니다.
const fadeIn = keyframes({ from: { opacity: 0 }, to: { opacity: 1 }, }) const panel = css({ animation: `${fadeIn} 200ms ease-out` })
globalStyle()는 모든 선택기에 대한 규칙을 추가합니다.
globalStyle('*, *::before, *::after', { boxSizing: 'border-box' }) globalStyle('body', { m: 0, font: 'system-ui, sans-serif' })
- 선택기는 작성된 대로 정확하게 사용됩니다.
- 평면 속성만 작동합니다. 의사 키, 화면 및
@키는 무시되므로globalStyle()는 미디어 규칙을 작성할 수 없습니다. - 동일한 규칙이 한 번 추가됩니다. 동일한 선택기에 대한 다른 스타일은 두 번째 규칙을 추가하고 나중에 규칙이 적용됩니다.
getCssText() 및 resetStyles()
#getCssText()는 이 프로세스에서 css(), cx(), variants(), keyframes() 및 globalStyle()가 생성한 모든 규칙을 반환합니다. 기본 규칙을 먼저 반환한 다음 @media, @container 및 @supports 블록. 서버 렌더링에 사용합니다.
renderToString()이후에 호출하세요. 렌더링 중에 생성된 규칙이 포함됩니다.- 하나의 페이지나 요청이 아닌 전체 프로세스를 다루며 성장만 합니다. 클래스 이름은 콘텐츠 해시이므로 안전합니다.
<style id="nuclo-styles">에 넣으세요. 클라이언트는 해당 요소를 재사용하고 이미 포함된 규칙을 건너뜁니다. 다른 ID를 사용하거나 없음을 사용하면 클라이언트는 모든 규칙에 두 번째 스타일시트를 다시 추가합니다.- 텍스트는 이스케이프되지 않으므로 신뢰할 수 없는 입력을 스타일 값에 넣지 마십시오.
resetStyles()는 테스트 도우미입니다. 모든 규칙과 #nuclo-styles 요소를 제거합니다. 이전에 생성한 스타일은 클래스 이름을 유지하지만 CSS를 잃습니다. 앱이나 서버에서 호출하지 마세요.
import { resetStyles } from 'nuclo' beforeEach(() => resetStyles())
nuclo-router는 Nuclo 코어 패키지의 일부가 아닙니다. nuclo 옆에 설치하세요. 미리보기 릴리스이므로 API가 여전히 변경될 수 있습니다.Nuclo용 라우팅: list() 및 update()를 기반으로 구축된 서버 렌더링, 하이드레이션, 코드 분할 경로 및 유휴 사전 로드. 4.9 정보 KB gzipped는 Nuclo 자체 이외의 종속성과 부작용이 없으므로 사용하지 않을 때 트리 쉐이크됩니다.
$ npm install nuclo nuclo-router
또는 동일한 두 이름으로 pnpm add, yarn add 또는 bun add를 사용합니다. 피어 종속성으로 nuclo 0.3 이상이 필요합니다.
라우터는 로드할 what를 결정합니다. 앱에는 where with router.outlet()가 표시됩니다. 레이어 스택 위에 하나의 list()가 있고 레이어당 한 행이 있으므로 일반 페이지는 단일 행입니다. 서버와 클라이언트는 동일한 트리를 구축하므로 hydrate()는 서버 노드를 교체하는 대신 서버 노드를 요청합니다. 그 밖의 모든 것은 history, 하나의 위임된 click 수신기 및 requestIdleCallback입니다.
원 임포트
export const router = createRouter(routes) // define once, import anywhere await router.start() // resolve the current URL render(App, document.getElementById('app')!) // App places router.outlet() and reads router.path, router.go(), …
start()는 반환되기 전에 일치하는 경로의 모듈을 확인하므로 첫 번째 트리는 서버와 클라이언트 모두에서 완성됩니다. 그 후 라우터는 시작된 경로인 router.path, router.pending, router.go()를 읽습니다.
전체 앱
// isomorphic: imported by the browser and the server import { createRouter } from 'nuclo-router' export const router = createRouter({ '/': () => import('./pages/Home.ts'), '/docs': () => import('./pages/Docs.ts'), '/blog/:slug': () => import('./pages/Post.ts'), '*': () => import('./pages/NotFound.ts'), }) export const App = () => div( header(a({ href: router.href('/') }, 'Home'), a({ href: router.href('/docs') }, 'Docs')), main( when(() => router.pending, Spinner()), when(() => router.error !== null, ErrorView()), router.outlet(), // where the pages land ), footer('© nuclo'), )
import type { RouteContext } from 'nuclo-router' import { router } from '../routes.ts' // A page is plain content export default function Post(ctx: RouteContext) { // A query-only change keeps the page, so the query is read live. return article(h1(ctx.params.slug), p(() => (router.search.has('preview') ? 'draft' : ''))) }
import 'nuclo' import { router, App } from './routes.ts' await router.start() hydrate(App, document.getElementById('app')!) // or render() without SSR
import 'nuclo/polyfill' import 'nuclo' import { renderToString } from 'nuclo/ssr' import { router, App } from './routes.ts' // one Route per request const route = await router.start(request.url) const html = route.run(() => renderToString(App))
이것이 전체 통합입니다. <a href>는 단지 <a href>이기 때문에 링크가 작동합니다. document의 위임된 리스너 중 하나는 라우터가 소유한 링크를 가로채고 다른 모든 링크를 브라우저에 남겨둡니다.
서버에서 start()는 요청당 Route를 반환하고 라우터를 그대로 두므로 동시 요청이 서로의 경로를 볼 수 없습니다. route.run()는 라우팅을 라우터로 만들고 콘센트에서 렌더링 길이를 읽습니다. 브라우저에는 페이지 로드당 하나의 경로가 있으며 start()는 이를 라우터 자체로 만듭니다. run()는 필요하지 않습니다.
페이지는 RouteContext 및 PageProps를 (ctx, { layer, data, outlet, beforeLeave })로 수신하며 더 적은 인수를 사용할 수 있습니다. list()가 허용하는 모든 것을 반환합니다. 레이아웃의 다른 부분도 채우려면 페이지 내부에서 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는 활성 경로를 기준으로 합니다. 그들은 함께 모든 부모 아래에서 동일한 것을 의미하는 재사용 가능한 섹션을 만듭니다. 테이블의 값은 로더이거나 키가 이에 상대적인 다른 테이블입니다.
import type { RouteTable } from 'nuclo-router' // a section, with no idea where it lives export const recordSection: RouteTable = { '/': () => import('./Record.ts'), // the parent path itself './preview': () => import('./Preview.ts'), './preview/raw': () => import('./Raw.ts'), }
// declare it wherever it belongs createRouter({ '/': () => import('./pages/Home.ts'), '/invoices/:id': recordSection, '/customers/:id': recordSection, '*': () => import('./pages/NotFound.ts'), })
이 두 줄은 경로를 두 번 쓰지 않고 6개 패턴을 모두 생성합니다. 두 상위 요소 모두 동일한 로더 개체를 가져오므로 하나의 모듈 캐시와 하나의 import()를 공유합니다.
- 하위 테이블 내부에서
'./x','/x'및'x'는 모두 같은 의미이고,'/'는 상위 경로 자체를 의미하며,'*rest'는 섹션에 자체 catch-all을 제공합니다. 중첩은 원하는 만큼 깊어집니다. - 최상위 키는 작성된 대로 사용되므로
'*'는'*'를 유지합니다. *catch-all부모 아래의 자식은createRouter()오류입니다. 왜냐하면 포괄적인 것은 패턴을 종료해야 하기 때문입니다.
상위 페이지 및 outlet()
부모와 그 자식은 실제 계층 구조입니다. outlet()를 호출할 때마다 상위가 렌더링되고 하위가 그 내부에서 렌더링됩니다.
import type { PageProps, RouteContext } from 'nuclo-router' // a parent page takes its outlet from (ctx, { outlet }) export default function Record(ctx: RouteContext, { outlet }: PageProps) { return div(Header(ctx), input({ id: 'notes' }), main(outlet())) }
하위 항목 간을 탐색해도 상위 항목이 다시 작성되지 않습니다. DOM 노드는 동일한 노드이므로 포커스, 스크롤 위치, 절반 유형의 입력 및 클로저의 모든 상태가 모두 유지됩니다.
- 하위 경로가 없는 페이지는 콘센트에서 아무것도 렌더링하지 않으므로 레이아웃에서 무조건 호출할 수 있습니다. 아이가 있는데
outlet()라고 부르지 않는 부모는 아이를 삼켜버립니다. - 인덱스 경로가 없습니다.
'/'는 레이아웃 자체이므로 상위 경로는 내부에 별도의 인덱스 페이지가 아닌 빈 콘센트가 있는 레이아웃을 렌더링합니다. 랜딩 콘텐츠를 레이아웃에 넣거나 실제 하위 경로를 제공하세요.
그런 다음 동일한 호출이 상위 페이지와 어느 페이지에 있는지 모르는 페이지에서 작동합니다.
router.href('./preview') // '/invoices/42/preview' or '/customers/7/preview' await router.go('./preview') await layer.push('./preview') // the usual shape: a child layer
./x, ../x, . 및 ..는 URL의 "디렉토리"가 아닌 현재 경로의 경로를 확인합니다. /invoices/42에서 ./preview는 /invoices/42/preview입니다. 여기서 브라우저는 /invoices/preview라고 말하며 URL의 후행 슬래시는 아무것도 변경하지 않습니다. ../는 세그먼트를 탐색하고 루트에서 중지합니다. 쿼리와 해시가 수행되고 base가 다시 추가되며 해결된 절대 경로가 기록에 기록됩니다. 이는 React Router가 <Link to="./x">에 대해 만드는 URL 확인과 동일한 편차입니다.
./및../만 경로 기준입니다. href의 기본preview는 일반 브라우저 규칙에 따릅니다.href="./preview"를 직접 작성하는 대신router.href('./preview')로 상대 링크를 구축하세요.href()는 렌더링 시 확인되므로 제공되는 마크업은 절대적이며 크롤러, 중간 클릭 및 JavaScript가 비활성화된 경우에도 여전히 정확합니다.
유지되는 것과 재구축되는 것
| 노드 | 탐색 중 |
|---|---|
| a 패턴과 경로가 변경되지 않은 상위 | kept: 동일한 노드이고 해당 load()는 그렇지 않습니다. 다시 실행 |
| 일치된 페이지 | kept 패턴과 경로가 변경되지 않고 load()가 없으므로 쿼리 또는 해시 변경으로 유지됩니다. 로더가 있는 경로는 방문할 때마다 새로 고쳐집니다. |
| a 경로가 변경된 상위 | 재빌드됨 및 그 아래의 모든 항목 |
| a 푸시 레이어 | 는 일치하는 페이지만 렌더링합니다. 상위 항목은 이미 그 아래 행에 마운트되어 있습니다. |
유지된 페이지는 빌드된 ctx를 유지합니다. 이것이 어디에서나 ctx의 의미입니다. 실시간 값을 보려면 router.*를 읽어보세요. 필터, 페이지 매김, 탭과 같은 쿼리 기반 상태는 동적 값 내에서 router.search를 읽고 router.go('?page=2', { replace: true })로 씁니다. 페이지, 해당 포커스 및 절반만 입력된 입력은 그대로 유지됩니다.
페이지 및 리전
#router.outlet()는 페이지를 한 곳에 모아줍니다. 사이드바, 도구 모음 또는 레이아웃의 다른 위치에 제목도 필요한 페이지는 into()로 채워집니다. 레이아웃은 region()로 장소를 표시하고 페이지는 자체 콘텐츠 옆에 into()를 반환합니다. 레이아웃은 페이지 콘텐츠를 소품으로 받지 않으며 탐색이 다시 작성되지 않습니다.
export const App = () => div( aside( input({ placeholder: 'Filter' }), // keeps its text across navigations region({ id: 'sidebar', empty: p('Nothing to show.') }), ), main(router.outlet()), )
import type { PageProps, RouteContext } from 'nuclo-router' export default function Post(_ctx: RouteContext, { data: post }: PageProps<Post>) { return div( article(h1(post.title), p(post.body)), into('sidebar', nav(h2('In this post'), ul(list(() => post.headings, (h) => li(h))))), ) }
페이지의 콘텐츠는 콘센트의 list() 행이며 into()는 이를 보유하는 행만큼 오래 유지됩니다. 따라서 사이드바 콘텐츠는 페이지와 함께 도착하고 페이지와 함께 떠납니다. into('sidebar', …) 없이 페이지로 이동하면 해당 지역의 empty 콘텐츠가 다시 표시됩니다. 두 부분 모두 동일한 기능으로 구축되었으므로 페이지에서는 두 부분 모두에 대해 ctx 및 data를 사용할 수 있습니다.
내비게이션에서 발생하는 현상
- A 보관된 페이지는 해당 영역을 유지합니다. 가 페이지를 유지하는 쿼리 또는 해시 변경은 영역 콘텐츠에도 영향을 주지 않습니다. 사이드바가 이에 의존하는 경우 동적 값 내부에서
router.search를 읽습니다. - A 재구성된 페이지는 이를 다시 채웁니다. 경로가 변경되면 이전 행이 제거되고 새 페이지의
into()보기가 이어받습니다. 아무것도 남지 않았습니다. - A 푸시 레이어는 뷰를 추가합니다. 레이어는 콘센트의 나중 행이므로
into()는 동일한 지역의 새로운 뷰입니다. 기본type: 'latest'를 사용하면 레이어가 닫힐 때까지 페이지 보기를 덮고 페이지 보기가 다시 표시됩니다.type: 'stack'를 사용하면 둘 다 표시됩니다. - A 상위 페이지도 영역을 채울 수 있습니다. 상위 페이지가 다시 작성되지 않기 때문에 해당 하위 페이지가 변경되는 동안 해당 뷰는 그대로 유지됩니다.
- Server 렌더링은 있는 그대로 작동합니다. 서버는
route.run()내부에 페이지를 구축하므로 해당into()뷰는 HTML에 있고hydrate()는 이를 주장합니다.
한 페이지의 여러 지역
한 번에 두 개 이상의 영역을 채우려면 객체를 전달하세요. 페이지의 주요 콘텐츠는 여전히 콘센트를 통과하므로 into() 옆에 반환합니다.
export default function Report(ctx: RouteContext) { return div( table(…), into({ toolbar: [button('Export'), button('Print')], sidebar: nav(a({ href: router.href('./summary') }, 'Summary')), }), ) }
어떤 작업에 어떤 도구가 필요합니까?
outlet()는 하위 경로용입니다. URL이 결정하는 콘텐츠는 상위 페이지 내부에 렌더링됩니다. 중첩 경로를 참조하세요.into()는 레이아웃 전체에 퍼지는 일치하는 페이지용입니다. 콘텐츠는 페이지에 속하며 해당 데이터를 사용합니다.matches()는 페이지가 포함되지 않고 자체 작은 테이블을 사용하여 URL만으로 레이아웃을 결정하는 데 사용됩니다.matches()를 참조하세요.
이들 모두는 일치하는 하나의 경로에 속합니다. 라우터는 하나의 URL에서 두 개의 독립적인 영역을 구동하지 않으므로 자체 경로가 있는 사이드바는 두 번째 콘센트가 아니라 페이지가 채우는 영역입니다. 자체 라우터, 로딩 및 폴백을 원하는 사이드바는 하나를 시작하지 않은 상태로 유지하고 match()로 해결합니다. 다중 라우터 & 결합 로딩를 참조하세요.
페이지 모듈은 load 기능을 내보낼 수 있습니다. 페이지가 구축되기 전에 해당 경로를 탐색할 때마다 실행되며 반환되는 모든 내용은 data로 페이지에 전달됩니다.
import type { DataLoader, PageProps, RouteContext } from 'nuclo-router' export const load: DataLoader<Post> = (ctx) => fetchPost(ctx.params.slug) export default function Post(_ctx: RouteContext, { data: post }: PageProps<Post>) { return article(h1(post.title), p(post.body)) }
경로 테이블은 변경되지 않았습니다. 로더는 페이지와 함께 이동합니다. 일치하는 RouteContext를 가져오므로 params, search 및 path를 모두 사용할 수 있습니다. 동기식 또는 비동기식일 수 있으며 서버에서도 실행되므로 서버 렌더링 페이지는 하이드레이션 후 가져오는 대신 이미 HTML에 있는 데이터와 함께 제공됩니다.
모듈이 캐시되었습니다. 데이터는 그렇지 않습니다. 경로의 청크는 한 번 가져와 재사용되며 해당 로더는 탐색할 때마다 다시 실행됩니다. 이것이 바로 /blog/a와 /blog/b를 혼동할 수 없는 이유이며, 실패한 로더를 간단히 재시도할 수 있는 이유입니다.
| 상황 | 행동 |
|---|---|
| 실행되는 동안 | router.pending는 true이고 현재 있던 페이지는 그대로 유지됩니다. |
| 던지거나 거절하는 거죠 | 착륙하다 router.error. go() 여전히 거부되지 않으며 탐색을 다시 시도할 수 있습니다. |
start() | 중에 start()에서 거부가 발생합니다. 아직 올려드릴 루트가 없습니다. |
| a 푸시 레이어 | 자체 데이터로 자체 로더를 실행합니다 |
| 유휴 프리로더 | 는 모듈만 따뜻하게 합니다. 전달할 컨텍스트가 없으며 결코 열 수 없는 경로에 대한 데이터를 가져옵니다. |
| 동일한 URL로 다시 탐색 | 로더가 다시 실행되고 행이 다시 작성됩니다. 로더가 없는 경로는 해당 행을 유지합니다. |
재검증하려면 기록 항목을 추가하지 않고 이미 있는 위치로 이동하세요.
await router.go(router.url, { replace: true }) // runs load() again
청크로 데이터 시작하기
페이지 모듈에서 내보낸 load는 해당 모듈이 도착한 후에만 실행될 수 있으므로 콜드 탐색은 청크를 가져온 다음 데이터를 가져옵니다. 두 가지를 동시에 시작하려면 withLoad()를 사용하여 테이블에서 로더를 선언하세요.
import { createRouter, withLoad } from 'nuclo-router' import { loadPost } from './data.ts' // small, in the main bundle createRouter({ '/blog/:slug': withLoad(() => import('./pages/Post.ts'), loadPost), })
모듈의 자체 load를 대체하고 방문할 때마다 실행됩니다. 청크가 여전히 로드되는 동안 거부는 다른 것과 마찬가지로 router.error에 도달합니다.
설계상 HTML에 서버 데이터가 직렬화되지 않습니다. 클라이언트의 load가 수화로 다시 실행됩니다. 이것이 중요한 경우 서버가 route.data(예: window.__DATA__)를 내보내고 로더가 이를 먼저 가져오도록 합니다. route.data는 일치하는 페이지의 데이터만입니다. 부모의 load() 결과가 여기에 없습니다.
코드 분할 및 사전 로드
경로의 값은 항상 페이지 컴포넌트를 반환하는 함수이므로 컴포넌트에 느리게 도달합니다.
'/a': () => import('./A.ts'), // default export (+ optional load) '/b': () => import('./B.ts').then(m => m.BPage), // named export '/c': () => CPage, // eager, not split '/d': () => ({ default: DPage, load: loadD }), // eager, module-shaped
모듈 모양 양식만 load 내보내기를 수행할 수 있습니다. .then(m => m.BPage)를 사용하여 명명된 컴포넌트를 선택하면 라우터에 기능만 전달됩니다.
각 모듈은 한 번 가져오고 캐시됩니다. 동일한 경로에 대한 동시 탐색은 하나의 import()를 공유합니다. 사용자가 idleTimeout에 대한 페이지를 그대로 두면(기본적으로 1초, 탭, 키, 휠 또는 스크롤 없음) 남은 경로가 유휴 슬라이스(requestIdleCallback)당 하나씩 백그라운드에 로드됩니다. 사용자가 기다리고 있는 입력이나 탐색은 페이지가 다시 유휴 상태가 될 때까지 일시 중지됩니다.
사전 로드되거나 열성적인 경로는 router.pending를 뒤집지 않으며 다음 페인트 전에 착륙하지 않습니다. 스피너 프레임도 없고 레이아웃 전환도 없습니다. router.pending는 기다려야 하는 모듈이나 로더에 대해서만 적용되며, 나가는 페이지는 있는 동안 화면에 유지됩니다. 처리되지 않은 거부가 아닌 router.error로 표면을 로드하지 못한 청크는 간단히 재시도할 수 있습니다.
라우터 API
#createRouter(table, options?)
table는 패턴을 로더 또는 중첩 테이블에 매핑합니다. 옵션:
| 옵션 | 기본값 | |
|---|---|---|
base | '/' | URL 접두사(예: '/docs')에서 앱이 제공됩니다. 세그먼트 경계에서만 일치합니다. /docs/intro는 '/docs' 내부에 있고 /docsearch는 그렇지 않습니다. 외부의 URL은 일치하지 않으므로 start()는 거부하고(서버 측 404) 이에 대한 링크는 브라우저에 남아 있습니다. |
preload | true | 페이지가 유휴 상태가 되면 백그라운드에서 나머지 경로의 모듈을 로드합니다. false가 전원을 끕니다. |
idleTimeout | 1000 | 미리 로드가 시작되기 전 탭, 키, 휠 또는 스크롤이 없는 밀리초 또는 사용자가 종료된 후 다시 시작 바쁘다. |
onNavigate | (ctx, kind) => void는 DOM가 따라잡으면 초기 탐색을 포함하여 브라우저에서 탐색이 해결될 때마다 호출됩니다. kind는 'start', 'push', 'replace', 'pop'(뒤로, 앞으로, close()) 또는 'layer'입니다. document.title, 메타 태그, 포커스, 스크롤 및 분석을 위한 후크입니다. SSR. |
라우터 start(), match(), outlet(), matches() 및 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만 해당됩니다.
// 404 before importing a single chunk if (!router.match(request.url)) return new Response(null, { status: 404 }) // "is this link ours?" for a custom link component or a prefetch rule const owned = router.match(href) !== null
router.outlet()
활성 경로의 페이지는 list()입니다. 페이지가 어디로 이동하든 한 번만 배치하세요. 푸시된 레이어는 그 레이어의 나중 행입니다. start()가 해결되기 전과 stop()가 해결된 후에는 비어 있습니다. 서버에서는 run()가 실행 중인 경로를 렌더링합니다.
router.matches(table)
활성 경로의 경로와 일치하는 두 번째 테이블의 뷰를 어디에나 배치할 수 있는 list()로 렌더링합니다. matches()를 참조하세요.
Route
run()를 제외한 모든 구성원은 라우터에도 있으며, 여기에서 앱이 이를 읽습니다.
| 멤버 | |
|---|---|
path, pattern, params, search, hash, url | 활성 일치합니다. search는 URLSearchParams입니다. search, hash 및 url가 라이브 중입니다. |
pending | a 탐색이 모듈 또는 로더를 기다리고 있습니다. |
error | 마지막으로 실패한 로드, 다음 탐색으로 지워짐 |
data | 서버가 직렬화하기 위해 일치하는 페이지의 load()가 반환한 내용입니다. 부모님의 정보가 없습니다. |
depth | 스택에 레이어 수(일반 페이지의 경우 1개) |
go(href, { replace }) | 탐색하고 필요한 경우 모듈을 로드합니다. 절대로 거부하지 않음: 거부하거나 던져서 실패한 모듈은 error. |
push(href) | 경로를 맨 위에 새 레이어로 열고 닫히는 항목으로 해결합니다. ./를 허용합니다. 일치하지 않거나 실패한 모듈을 거부합니다. |
href(path) | a({ href })의 경우 경로 앞에 base를 붙입니다. 활성 경로에 대해 ./ 및 ../를 확인합니다. |
stop() | 모든 리스너를 분리하고, 열린 레이어를 닫고, 콘센트를 비웁니다. 멱등성. 중지된 경로는 여전히 마지막 일치 항목으로 응답하고 go()는 아무 작업도 수행하지 않습니다. |
run(fn) | 이 경로를 라우터의 활성 경로로 사용하여 fn를 호출하고 서버에서 결과: route.run(() => renderToString(App))를 반환합니다. 동기식만 가능합니다. fn가 반환되면 경로가 활성화되지 않습니다. |
RouteContext
페이지의 첫 번째 인수: path, pattern, params, search, hash, url. 페이지가 작성된 컨텍스트입니다. 실시간 값을 보려면 router.*를 읽어보세요. 다시 작성하지 않고 쿼리나 해시가 변경된 경우에만 다릅니다.
params는 기본적으로 { [name]: string }입니다. 페이지는 Params를 사용하여 자체 패턴에서 이를 입력할 수 있으며, 이는 '/blog/:slug'를 { slug: string }로, '/files/*rest'를 { rest: string }로 바꿉니다.
import type { Params, RouteContext } from 'nuclo-router' export default function Post(ctx: RouteContext<Params<'/blog/:slug'>>) { return article(h1(ctx.params.slug)) // slug: string, nothing else exists }
PageProps
페이지의 두 번째 인수입니다. 페이지에 필요한 것을 구조화합니다.
| 속성 | |
|---|---|
layer | 페이지가 스택에 있는 위치입니다. 아래 Layer를 참조하세요. |
data | 경로의 load()가 반환된 내용 또는 undefined가 반환되지 않은 내용입니다. PageProps<TData>. |
outlet | 여기서 상위 페이지는 하위 경로를 렌더링합니다. 중첩 경로. |
beforeLeave | 이 페이지가 삭제되기 전에 요청된 가드를 등록합니다. 가드를 떠나십시오. |
Layer
페이지가 스택에 있는 위치와 나가는 길입니다.
| 멤버 | |
|---|---|
depth | 0, 푸시된 레이어의 경우 1 이상 |
push(href) | 페이지 내부에서 다른 레이어를 엽니다. Route.push() |
close(result?) | 이 레이어와 그 위의 모든 항목을 닫고 이를 연 push()를 해결합니다. 논쟁이 없다는 것은 기각을 의미합니다. 깊이 0. |
DataLoader 및 withLoad()
DataLoader는 페이지 모듈의 선택적 load 내보내기인 (ctx: RouteContext) => TData | Promise<TData>입니다. withLoad(loader, load)는 load가 청크 가져오기 뒤가 아닌 옆에서 시작하는 경로를 만듭니다. 경로 로더를 참조하세요.
유형
모든 유형은 nuclo-router의 명명된 내보내기이므로 경로에서 가져오지 않고도 페이지, 로더 또는 가드를 입력할 수 있습니다.
| 타입 | 그것은 무엇입니까 |
|---|---|
Params<'/blog/:slug'> | 패턴에 대한 params 개체입니다. RouteParams는 유형이 지정되지 않은 모양이고 Match는 일치자의 결과입니다. |
PageComponent | a 페이지: (ctx, { layer, data, outlet, beforeLeave }) => … |
RouteModule | a 페이지 모듈: 컴포넌트 자체 또는 { default, load? } import()가 |
RouteLoader | () => RouteModule | Promise<RouteModule>, 선택적으로 운반 load ~에서 withLoad() |
RouteTable | 테이블 createRouter()는 다음을 취합니다: 로더 또는 중첩 테이블에 대한 패턴 |
RouterOptions | { base?, preload?, idleTimeout?, onNavigate? } |
NavigateOptions | { replace? }, go() |
NavigationKind | 'start' | 'push' | 'replace' | 'pop' | 'layer', onNavigate |
Outlet | 유형 outlet() ~에서 PageProps |
LeaveGuard | (to) => boolean | void | Promise<…>, beforeLeave()가 취하는 것 |
MatchView | matches() 뷰는 무엇입니까: 매개변수 입력, 콘텐츠 또는 페이지 출력의 import() |
push()는 경로를 바꾸는 대신 현재 경로 위에 경로를 열고 해당 레이어가 닫히는 모든 것을 해결합니다. 페이지는 스택 위에 있는 단일 list()이므로 레이어를 여는 것은 추가입니다. 아래 페이지는 다시 렌더링되지 않으므로 레이어가 닫힐 때 DOM, 포커스 및 절반만 채워진 양식이 모두 그대로 유지됩니다.
이렇게 하면 사용자가 작성하고 있던 양식을 버리지 않고도 "아직 존재하지 않는 레코드가 필요합니다" 흐름이 작동합니다.
// the page underneath const created = await layer.push<Option>('/options/new') if (created) { options.push(created); selected = created.id; update() }
// the pushed page: (ctx, { layer }) layer.close(created) // resolves the push() above with the new option layer.close() // dismissed: resolves with undefined
레이어 중첩. 자체 대화 상자가 필요한 대화 상자는 layer.push()를 호출하고 경로에 대한 참조 없이 이를 기다립니다.
- 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(또는 이에 대한 약속)를 반환하면 페이지가 유지됩니다.
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)는 경로를 로드하거나 페이지를 추가하지 않고도 자체 작은 테이블에서 사이드바, 이동 경로 또는 도구 모음과 같이 사용자의 위치에 따라 달라지는 다른 항목을 렌더링합니다.
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()에서 렌더링되므로 크롬은 섹션 내 탐색에서 유지됩니다.
createRouter({ '/': …, '/docs': docsRoutes, '*': … })
또는 생성되었지만 시작되지 않은 자체 라우터 기능을 제공합니다. 부모는 단일 포괄 항목을 소유하고 자식은 자체 패턴, 매개변수 및 폴백에 대해 꼬리를 해결합니다.
// parent table '/docs/*rest': () => import('./docs/Shell.ts'),
const docs = createRouter(docsRoutes, { base: '/docs' }) const hit = docs.match() // { pattern: '/:topic', … } const page = await docsRoutes[hit.pattern]() // the app's own lookup
절대 시작하지 않는 것이 포인트입니다. start()는 리스너를 연결하는 것이며 시작된 두 개의 라우터가 충돌합니다. 거래는 하위가 해상도를 소유하므로 시작된 라우터가 실행되는 경로 로더를 호출할 수 있다는 것입니다. 이 모양은 또한 꼬리가 변경될 때 셸을 다시 빌드합니다. 왜냐하면 라우터가 하나의 경로를 확인하고 셸이 나머지 경로를 자체적으로 해결하기 때문입니다. 중첩 테이블은 라우터가 계속 마운트된 상위 테이블을 제공하므로 해당 기능에 실제로 자체 라우터, 베이스 및 폴백이 필요한 경우가 아니면 이를 선호합니다.
앱은 영역 수만큼 라우터를 가질 수 있습니다. 이를 단순하게 유지하는 규칙은 다음과 같습니다. 문서당 정확히 하나만 시작하고 다른 모든 라우터는 시작되지 않은 상태로 유지하여 match()로 URL을 확인합니다. 시작된 라우터만이 URL, 기록 및 클릭 리스너를 소유합니다. 나머지는 사용자가 질문하는 테이블입니다. 각 영역은 자체 스피너를 사용하여 자체 시계에 로드할 수 있습니다.
결합 로딩: 로더 체인
일치하는 체인의 모든 페이지는 load()를 내보낼 수 있습니다. 하위 경로에 대한 콜드 탐색에서 레이아웃의 로더와 페이지의 로더는 를 병렬로 로 실행하고 하나의 router.pending가 전체 체인을 덮습니다. 이전 페이지는 쌍이 해결될 때까지 화면에 유지되므로 절반만 로드된 트리는 페인트되지 않습니다. 각 컴포넌트는 자체 data를 받습니다.
'/shop': { '/': () => import('./pages/Shop.ts'), // layout, has a load() './:section': () => import('./pages/Section.ts'), // page, has a load() },
import type { DataLoader, PageProps, RouteContext } from 'nuclo-router' export const load: DataLoader<ShopData> = async () => { await wait(500) // a slow API return { sections: ['toys', 'books', 'music'] } } // the layout gets its own data, and its outlet for the child page export default function Shop(_ctx: RouteContext, { data, outlet }: PageProps<ShopData>) { return div( h1('Shop'), nav(...data.sections.map((s) => a({ href: router.href(`/shop/${s}`) }, s))), main(outlet()), ) }
export const load: DataLoader<SectionData> = async (ctx) => { await wait(900) // slower than the layout's return { section: ctx.params.section, items: catalog[ctx.params.section] } } // the page gets its own data: the two loaders ran beside each other export default function Section(_ctx: RouteContext, { data }: PageProps<SectionData>) { return article(h2(data.section), ul(...data.items.map((i) => li(i)))) }
두 로더가 차례로 시작되지 않고 함께 시작됩니다. router.pending는 둘 중 하나가 대기하는 동안 true이고 나머지 시간에는 false입니다. 사전 로드되거나 열성적인 경로는 절대 반전하지 않습니다. 섹션 사이를 이동하면 레이아웃이 유지되므로 섹션의 로더만 다시 실행됩니다. 레이아웃의 데이터, DOM 및 절반 유형의 입력은 모두 유지됩니다. 동일한 URL로 다시 탐색하면 단일 페이지와 마찬가지로 리프의 로더가 다시 실행되고 해당 행이 다시 작성됩니다.
다중 로딩: 자체 스피너가 있는 두 번째 라우터
독립 영역(사이드바, 트레이, 라이브 영역)은 생성되었지만 시작되지 않은 자체 라우터와 자체 로딩 플래그를 갖습니다. router.pending는 시작된 라우터에 속하며 이 영역에 대해 아무 말도 하지 않으므로 두 라우터가 나란히 로드될 수 있으며 둘 중 하나가 먼저 완료될 수 있습니다.
import { createRouter } from 'nuclo-router' // Created but never started: start() is what attaches the history and click // listeners, and only the main router is started. match() attaches nothing. const standIn = () => ({ default: () => div() }) // never rendered const sidebarRouter = createRouter({ '/shop': standIn, '/shop/:section': standIn, '*': standIn, }, { base: '/docs' }) // the same base as the main router export const sidebar = { loading: false, info: null as SidebarInfo | null } export async function refreshSidebar(at: string) { const hit = sidebarRouter.match(at) // the second router resolves if (!hit || (hit.pattern !== '/shop' && hit.pattern !== '/shop/:section')) { sidebar.info = null; update(); return } sidebar.loading = true // its own flag, its own spinner sidebar.info = null; update() const data = await fetchSidebar(hit.params.section) sidebar.loading = false; sidebar.info = data update() }
createRouter(routes, { onNavigate: (ctx, kind) => { announce(kind) refreshSidebar(ctx.url) // every navigation re-resolves the sidebar }, })
두 번째 라우터는 요청을 받지만 알려주지 않습니다. match()는 URL을 읽고 컨텍스트를 반환하며 사이드바에 대한 어떤 것도 기본 경로 테이블에 입력되지 않습니다. onNavigate는 초기 탐색을 포함하여 해결된 모든 탐색 후에 실행되므로 URL과 보조를 맞춰 독립된 영역을 유지하는 단일 장소입니다.
pending는 시작된 라우터에 속합니다. 시작되지 않은 라우터에는 경로,pending및error가 없습니다. 해당 영역은 자체 플래그를 추적하고 두 번째 스피너는 첫 번째 플래그 옆에 배치될 수 있습니다. 데모에서 사이드바는 800ms 동안 로드되는 반면 경로의 로더는 900ms 동안 실행되므로 페이지는 사이드바보다 먼저 완료되거나 그 반대 방향으로 완료될 수 있습니다.match()는 아무것도 첨부하지 않습니다. 시작되지 않은 라우터 수에 관계없이 문서를 공유할 수 있습니다. 각각에는 자체 테이블, 기본 및 모듈 캐시가 있으며 그 중 어느 것도 기록을 건드리지 않습니다. 테이블과 매치 비용이 듭니다.- Its 로더는 실행되지 않습니다. 시작된 라우터는 일치하는 페이지의
load()를 실행합니다. 시작되지 않은 라우터는 확인만 하므로 보기에 데이터가 필요한 경우 직접 가져오십시오(데모의refreshSidebar()). 로더를 원하는 경우 중첩 테이블 또는 페이지가 채워지는 영역는 시작된 라우터에서 해당 작업을 유지합니다. - Guard 오래된 응답. 느린 사이드바 가져오기는 탐색한 섹션에 도달해서는 안 됩니다. 새로 고침당 생성 카운터를 높이고 더 이상 일치하지 않는 응답을 삭제합니다. 라우터 자체가 추월된 탐색을 폐기하는 방식입니다.
- A 게으른
matches()뷰에는pending도 없습니다. 모듈이 착륙할 때까지 아무것도 렌더링하지 않습니다. 3개의 로딩 시스템, 3개의 독립 시계: 경로의pending, 자신의 플래그, 아직 존재하지 않는 게으른 보기.
링크 처리
#위임된 리스너는 SVG <a>를 포함하여 <a href>를 마우스 왼쪽 버튼으로 클릭하고 그 안에 중첩된 항목을 클릭하고 다른 모든 항목(수정자 클릭 및 중간 클릭, _self 이외의 target)을 위해 한 단계 물러납니다. download, rel="external", 교차 출처 href, 현재 페이지의 해시 전용 링크, base 외부 경로, 경로가 일치하지 않는 경로(그래서 서버 렌더링된 404에 여전히 도달 가능) 및 앱 핸들러가 이미 preventDefault()를 호출한 모든 이벤트. data-nuclo-router="off"를 사용하여 링크를 명시적으로 선택 해제하세요.
스크롤링
푸시된 탐색은 현재 스크롤 위치를 유지합니다. 새 URL에 페이지의 요소와 일치하는 #hash가 있으면 해당 요소가 스크롤되어 표시됩니다. popstate는 브라우저에 스크롤을 남깁니다. 상단에 새 페이지를 표시하려면 onNavigate에서 수행하세요. 탐색이 어떻게 진행되었는지 알려줍니다.
let shown = '' createRouter(routes, { onNavigate: (ctx, kind) => { // Not on Back/Forward (the browser restores those), nor for a query change on the same page. if (kind === 'push' && ctx.path !== shown && !ctx.hash) window.scrollTo(0, 0) shown = ctx.path }, })
주의 사항
- 아무것도 유지되지 않습니다. 경로는 DOM가 아닌 페이지 기능과 해당 컨텍스트를 유지합니다. 다른 페이지로 이동하면 이전 페이지가 해제됩니다.
stop()는 청취자와 모든 비행 중 로드를 해제하고pending를 지우므로 폐기된 경로는 스피너를 좌초시키지 않습니다.stop()가 자동으로 삭제된 후 해결되는 모듈입니다. - 서버에서 동시성 안전.
start()는 요청별 상태를 반환하고run()는 이를 하나의 동기 렌더링으로 범위 지정하므로 인터리브된 요청은 서로의 경로를 볼 수 없습니다. 모듈 캐시만 공유됩니다.run()의 함수가 반환되면 범위가 종료됩니다.renderToString()는 동기식이므로 충분합니다. 비동기 또는 스트리밍 렌더링에는run()가 수행하지 않는await를 통해 전달되는 경로가 필요합니다. - 스크롤 복원 사용
popstate브라우저의 (scrollRestoration), 라우터가 아닙니다. - 문서당 하나의 라우터를 시작합니다. 각
start()는 자체 위임click수신기를 추가하고 두 번째 라우터의start()는 첫 번째 수신기를 폐기하지 않습니다. 동일한 라우터에 있는 두 번째start()만 작동합니다. 다른 테이블에 대해 URL을 확인하려면 라우터를 만들고match()만 호출하세요. 아무것도 첨부하지 않습니다. 독립된 영역은 경로의pending옆에서 자체 로딩을 실행할 수 있습니다. 다중 라우터 & 결합 로딩를 참조하세요. - 경로가 경로 이름에 있고 서버가 모든 경로를 제공해야 하기 때문에 의도적으로 포함되지 않습니다: 해시 및 메모리 기록. 페이지가
into()로 여러 영역을 채울 수 있지만 모두 하나의 일치하는 경로에 속하기 때문에 하나의 URL로 구동되는 두 개의 독립적으로 라우팅된 영역 — Multiple Routers & Combined Loading에서와 같이 독립적인 영역은 여전히 시작되지 않은 자체 라우터와 로딩을 유지할 수 있습니다.mouseenter에서 경로 자체 로더를 호출하여 얻을 수 있는 호버 사전 로드.
예
저장소에서 실행 가능한 두 가지 예제는 서로를 보완하므로 모든 설정을 어딘가에서 관찰할 수 있습니다.
examples/router: 전체 API에 대한 클라이언트 측 둘러보기입니다. 모든 패턴 형식 및 우선 순위 규칙, 링크 처리 옵트아웃, 라이브 제어가 포함된 모든 경로 멤버, 하위 라우터, 3단계 깊이의 레이어 스택, 두 상위 항목에 마운트된 상대 섹션,pending및error가 있는 경로 로더. 해당 상태가 계속 표시되도록preload: false를 설정합니다.examples/router-ssr: 요청당renderToString(), 서버 노드를 주장하는 것으로 입증된 하이드레이션, 실제 404 상태 코드, 루트가 아닌base, 네트워크 패널에서 볼 수 있는 유휴 사전 로드, 서버에서 서버로 데이터가 전달되는 로더 클라이언트가 수분 공급을 다시 가져오지 않도록 합니다.
Nuclo에는 계산된 API가 없습니다. 일반 함수를 사용하고 동적 값에서 호출하세요.
let items = ['apple', 'banana', 'cherry'] let filter = '' const visible = () => items.filter((item) => item.includes(filter)) const Fruits = () => div( p(() => `Showing ${visible().length} of ${items.length}`), ul(list(visible, (item) => li(item))), ) filter = 'an' update()
일반 async/await를 사용하고 상태가 변경될 때마다 update()를 호출하세요. 가져오기의 경우 이는 await 이전에 한 번, 이후에 한 번을 의미합니다.
let status: 'idle' | 'loading' | 'done' | 'error' = 'idle' let result: unknown = null async function loadData() { status = 'loading' update() try { result = await fetch('/api/data').then((r) => r.json()) status = 'done' } catch { status = 'error' } update() }
권장 사항
#- 일괄 변경. 필요한 모든 상태를 변경한 다음
update()를 한 번 호출합니다. - 동적 값을 빠르고 순수하게 유지하세요. 이들 모두는 모든
update()에서 실행됩니다. - 입력이나 타이머 등 작고 빈번한 업데이트에는
scope()를 사용하세요. - Render를 문서로 보냅니다.
update()가 실행될 때 문서에 없는 노드는 업데이트를 중지합니다. - 노드를 반환하는 함수가 아니라 변경되는 하위 항목에 대해
when()및list()를 사용합니다. - 사용
className, 아니다class, 그리고 사용() => cx(...)또는{ className: () => … }동적 수업의 경우. - 모듈 수준에서 스타일을 한 번 정의합니다..
- 서버 렌더링: 먼저 렌더링한 다음
getCssText()를<style id="nuclo-styles">에 넣습니다. 요청 데이터를 인수로 전달하고 출력에 HTML 주석을 유지합니다.
API 색인
#모든 공개 기능과 그 출처.
nuclo(전역 및 명명된 내보내기)
update(...scopeIds): 모든 동적 값 또는 지정된 범위에 있는 값을 실행합니다.scope(...ids):update(id)에 대한 요소 범위 ID를 제공합니다.when(condition, ...content):.when()및.else()가 포함된 조건부 콘텐츠.list(items, renderItem): 항목당 하나의 행, ID와 일치.region(options)및into(id, ...content): 트리의 명명된 위치 및 어디에서나 이를 채우는 콘텐츠입니다.on(type, listener, options?): 이벤트 리스너 또는on('mount')/on('destroy').render(App, parent?): 빌드 및 추가.hydrate(App, parent?): 서버 HTML를 인수합니다.forceUpdate(): 컴포넌트를 다시 호출하고 DOM를 패치합니다.css(name?, style): 스타일 개체에서 생성된 클래스.cx(...inputs): 스타일을 결합하면 나중에 승리합니다.createCss(theme): 테마css,cx,variants,keyframes및globalStyle.variants(config): 옵션의 스타일.keyframes(frames)및globalStyle(selector, style).
nuclo(명명 내보내기 전용)
nuclo/ssr
renderToString(input),renderManyToString(inputs),renderToStringWithContainer(input, tag?, attrs?)및getCssText().
nuclo/polyfill
- A 부작용 import,
document,Event,CustomEvent,Node,Element,HTMLElement및Nuclo*클래스.
nuclo-router(별도 패키지)
createRouter(table, options?): 라우터,start(),match(),outlet(),matches(),go(),push(),href()및stop().withLoad(loader, load): 데이터 로더가 해당 청크 옆에서 시작하는 경로입니다.- Types:
RouteContext,PageProps,Params,DataLoader,RouteTable등.
글로벌 전용
- 175 태그 빌더:
div,span, …,var_및svgSvg,pathSvg, …. - Types:
NodeModFn,NodeModLike,ExpandedElement등.StyleResult와 같은 스타일 유형을'nuclo'에서 가져올 수도 있습니다.
특수 키
- 속성:
className,style,on*처리기,onMount및onDestroy. - Style 개체: 축약형, 의사 상태,
&선택기,@규칙 및raw.
자주 묻는 질문
#상태 변경이 UI를 업데이트합니까?
아니요. DOM는 update()를 호출할 때까지 그대로 유지됩니다.
Nuclo는 신호, 프록시 또는 가상 DOM를 사용합니까?
번호. 상태는 일반 JavaScript로 유지됩니다. 트리가 렌더링되면 Nuclo는 실제 요소와 텍스트 노드를 생성합니다. update()는 등록된 모든 동적 값 또는 지정된 범위 내의 값을 실행하고 값이 변경된 동일한 노드를 패치합니다. 종속성 그래프와 가상 트리가 없습니다.
내 페이지에 "false"가 표시되는 이유는 무엇입니까?
부울은 텍스트로 렌더링됩니다. div(ok && span('Done'))에는 "false"가 표시됩니다. 자식 노드용으로는 when(() => ok, span('Done'))를, 텍스트용으로는 ok ? 'Done' : ''를 사용하세요.
항목을 편집할 때 목록 행이 변경되지 않는 이유는 무엇입니까?
목록에 아직 남아 있는 항목의 행은 유지되며 다시 렌더링되지 않습니다. 변경되는 필드에는 () => todo.text와 같은 동적 값을 사용하십시오. list()를 참조하세요.
UI의 일부가 업데이트를 중지하는 이유는 무엇입니까?
일반적으로 update()가 실행될 때 해당 노드가 문서에 없었습니다. Nuclo는 해당 노드를 연결한 후에도 추적을 중지하므로 문서에 있는 컨테이너로 렌더링합니다. 다른 두 가지 원인: 조건이 발생하는 when()는 업데이트를 중지하고 첫 번째 실행에서 발생하는 텍스트 함수는 비어 있습니다. update()를 참조하세요.
언제 forceUpdate()를 사용해야 합니까?
언어 전환과 같이 정적 값이 거의 변경되지 않는 경우에만 해당됩니다. 그 밖의 모든 것에는 update()를 사용하십시오. forceUpdate()를 참조하세요.
소품으로 전달하지 않고 페이지를 레이아웃에 어떻게 넣나요?
region({ id: 'main' })로 장소를 표시하고 페이지가 into('main', …)를 반환하도록 합니다. region() & into()를 참조하세요.
앱을 어떻게 마운트 해제하나요?
when()로 감싸고 조건을 끕니다. DOM를 직접 제거하면 onDestroy가 실행되지 않습니다. Lifecycle를 참조하세요.
Nuclo에는 라우터가 포함되어 있습니까?
코어 패키지에는 없습니다. 라우팅은 서버 렌더링, 하이드레이션, 코드 분할 경로 및 유휴 사전 로드 기능을 갖춘 nuclo-router로 별도로 배송됩니다. npm install nuclo nuclo-router로 설치하세요. nuclo-router를 참조하세요.
Nuclo에 효과가 있나요?
번호 onMount 및 onDestroy는 요소당 한 번 실행되며 삽입 및 제거와 연결됩니다. 일부 상태가 변경되었기 때문에 아무 것도 다시 실행되지 않습니다.