跳至正文

架构

uiify分为五层,各层独立发布,绝不导入上层。所有层都遵循同一条规则:组件必须在水合前就能工作。

水合空窗期

流式SSR在首次绘制与水合完成之间留有空窗期;使用Suspense时,每个延迟加载的边界都有自己的空窗期。此时,基于useState的折叠面板虽然可见,却无法交互,而基于<details>的折叠面板从首次绘制起就能使用。

这两个文档渲染同一个折叠面板,均不包含任何<script>标签:一个基于`<details>`,另一个基于`useState`。分别打开:前者能够展开,后者不能。查看源代码即可确认。

目标是消除不必要的渲染、由provider向所有消费者扩散的更新,以及库JavaScript引发的同步布局工作,而非完全不渲染——状态总要在某处渲染。默认不提供JavaScript,只在无障碍需要时加入少量代码。

分级

每个组件都在清单中声明级别,构建时读取该清单。清单决定体积预算、RSC纯度检查、Playwright测试套件,以及各组件页面上的级别徽标。

级别预算禁用JavaScript时含义
Tier 0
RSC——零客户端JavaScript
0 kB完整通过禁用JavaScript的测试套件。状态保存在原生元素中,或组件没有状态。禁止使用任何React Hook,包括useMemo和useRef。
Tier 1
RSC外壳+轻量客户端孤岛
≤ 1 kB行为有所降级,各组件分别说明。状态仍由原生元素管理。JavaScript只同步ARIA,或补足平台缺少的能力,例如悬停意图判断或自动关闭。
Tier 2
客户端组件——水合前SSR
按组件确定水合前渲染有意义的标记。单一客户端入口。服务端尽可能输出原生优先的标记;键盘导航、过滤和ARIA同步需等到水合后。

分级也用于迁移:门禁按组件启用,让架构要求逐个组件收紧。

分层结构

@gunkin/uiify/hooks       SSR-safe React hooks — the substrate
@gunkin/uiify/core        headless behaviour: focus scope, dismiss layers, anchoring
@gunkin/uiify/elements    native-mechanism wrappers — zero client JavaScript
@gunkin/uiify/components  unstyled components, RSC by default
@gunkin/uiify/styles      optional CSS design layer

下层绝不导入上层。@gunkin/uiify/elements的限制最严格:只能导入@gunkin/uiify/core/render和React类型。

为什么将elements公开为独立层

大多数应用不会直接导入@gunkin/uiify/elements。将其公开发布有两个原因。

首先,让设计可以验证。当组件页面标注*基于elements/dialog构建*时,对应的元素页面会展示底层的确切标记和data-*契约。

其次,便于组合:如果组件的组成结构不适合你的设计,可以沿用相同的连接方式自行构建,无须重新实现底层机制。

模块只有满足以下五条规则才能进入这一层:只封装一种原生机制、不包含JavaScript行为、至少被两个@gunkin/uiify/components组件使用、状态可由CSS选择器读取,以及减少重复标记而非增加间接层。

模块按平台功能命名,如details、dialog、popover和invoker,而非按UI概念命名。组件与元素绝不同名,因此Modal基于dialog构建。浏览元素

为什么Tier 0禁用React context

如果组成部分仍通过React context通信,将状态移入DOM也无济于事。Server Component不能使用context,因此共享provider的Root和Item无法在服务端渲染,无论它们的标记多么原生。

连接关系必须能在序列化后保留,因此组成部分通过HTML连接:用<details name>分组,用commandfor和popovertarget建立由浏览器解析的引用,并从props生成id。没有context,useId的值就无法传递给兄弟部分。

<Accordion id="faq">
  <Accordion.Item value="shipping">   {/* id="faq-shipping" */}

代价是根元素需要一个id。不需要组合时,组件接收数据并自行渲染整棵树。查看Server Component

本页由机器翻译,尚未经人工审核。