@clean-jsdoc-theme/rang

@clean-jsdoc-theme/rang 是由 dwar 进行服务端渲染并打包的 Preact 组件库。它掌管页面外壳 HTML 的每一个字节——LayoutHeaderFooter—— 那些为渐进增强提供支持的可 hydration 的 island、MDX 元素 → 组件映射,以及用引用 CSS 变量的 Tailwind 工具类设置样式的 shadcn 风格基础组件。

为何取这个名字? rang(रंग)在印地语/梵语中意为颜色——这恰如其分地形容了这个 掌管主题整个视觉层面的包:布局、组件和样式。

该包仅依赖于浏览器安全的契约包 (@clean-jsdoc-theme/utils),外加 preactclass-variance-authorityclsxtailwind-mergelucide-preact——参见 package.json

如果你只是想使用该主题,你永远不需要安装这个包。它是一个由 dwar 为你打包的内部 构建块。关于你实际需要安装的部分,参见 Packages 一节。

为何它是一个独立的包

该流水线是有意拆分的:setu 产出一个 SiteManifest,dwar 渲染 它,而所有标记都存在于 rang 中。将组件层抽取到它自己的 包中,为项目带来了一道清晰的接缝:

  • rang 掌管标记;dwar 掌管编排。 dwar 的 layout.tsx 不添加任何自己的外壳——它的 SsrLayout 通过 rang LayoutheaderControls / sidebar / toc / tocMobile 插槽来组合它,除了把每个 交互组件包裹进一个 <div data-island="…"> hydration 标记之外,不做任何其他事情。正如该 文件自己的文档注释所言:"The chrome markup (header, grid shell, asides, footer) lives entirely in rang's Layout. dwar's only job here is hydration."
  • 从构造上即浏览器安全。 组件在浏览器中运行,因此 rang 只 导入无 node 的 utils 契约——绝不导入构建 侧。相同的组件在服务端渲染为字符串 (preact-render-to-string)并在浏览器中进行 hydration。
  • 一套样式约定。 组件用引用 CSS 变量的 Tailwind 工具 类来给自己设置样式(例如 bg-backgroundtext-(--clean-fg)border-(--clean-border))。dwar 将主题 token 注入到 :root 上的这些变量中,因此相同的标记无需重新编译即可重新换主题。参见 lib/cn.ts ——shadcn 的 cn() 辅助函数,它组合 clsx + tailwind-merge,从而让 调用方提供的类总能在 Tailwind 冲突中胜出。

外壳(Chrome)与 island

这是 rang 中的核心区分。

**外壳(Chrome)**是纯粹、静态的 SSR 标记,不含任何客户端 JavaScript。 Layout 外壳——header + 一个 3 列网格(sidebar · main · toc)+ footer——自身从不 引用任何 island。它只渲染调用方放入其 插槽中的任意节点。HeaderFooterBrand 同样是静态的。

island 是交互式的部分。它们在服务端渲染为纯 HTML, 随后 dwar 挂载一个小的、每个 island 各自的 JS 块来 hydrate 仅该子树—— 是渐进增强,而非整页的 SPA。island 的完整集合是 islands.ts 中那份权威的 ISLAND_REGISTRY, 一个以 dwar 标记 DOM 时所用的 island 名称为键的 Record<IslandName, ComponentType>

Island name (IslandName)Component
sidebarSidebar
mobile-navMobileNav
tocTOC
toc-mobileTocPopover
cmdkCtrlK
code-tabsCodeTabs
code-viewerCodeViewer
embedEmbedBody
copy-btnCopyBtn
copy-pageCopyPageButton
theme-toggleThemeToggle
settingsSettings
tabsTabs

其中有两个条目值得一提,直接引自注册表自己的 注释:

  • embed 映射到 EmbedBody(而非 Embed MDX 包装器):加载器 将 EmbedBody 挂载到 data-island="embed" 标记上,并从该标记的 data-* 属性读取其配置。
  • tabs 是完全 SSR 渲染的——Tabs 标记(ARIA tablist + panels) 在服务端发出,在客户端仅进行 DOM 增强(面板 内容是任意的 SSR HTML,因此它是被增强,而非重新 hydration)。它的条目存在*"purely to satisfy Record<IslandName, …>."*

第三组是 仅 SSR 且不是 islandSteps / Step(一个静态的带编号步进器)和 PageNav (上一页/下一页翻页器)是不含任何客户端 JS 的纯标记——它们被导出并 在 MDX/布局中使用,但它们从不出现在 ISLAND_REGISTRY 中。标题锚点 也类似:标记是仅 SSR 的,由一个委托的内联脚本处理 点击(参见 mdx-utils.tsx)。

MDX 元素映射

当 dwar 编译某个页面的 MDX 时,它用 rang 的 defaultMdxComponents 来渲染它——即 mdx-components.tsx 中那份元素名 → 组件的注册表。 它将 MDX 发出的内建标签(h1h6aimgpprecode、 列表、blockquote、表格、hr)映射到来自 mdx-tags.tsxCodeBlock.tsx 的带样式渲染器上,外加 setu 发出的那些首字母大写组件,以便它们也经由该映射进行路由: CalloutEmbedSourceLinkMemberMetaMemberHeadingSteps / Step,以及 Tabs / Tab。(关于这些组件面向用户的一面,参见 CalloutsTabs。)

dwar 如何消费它

dwar 在渲染时和打包时都会导入 rang:

  • SSR。 dwar/src/layout.tsx 导入 rang 的 Layout 和外壳 island(SidebarMobileNavTOCTocPopoverCtrlKThemeToggleSettings),通过 renderIsland 把每一个 包裹进一个 data-island 标记,为每个页面的载荷记录其 props, 并把包裹好的节点交给 Layout 的插槽。
  • Hydration。 dwar/src/islands-bundle.ts 运行一次拆分式 esbuild 构建,将每个 island 作为一个入口点。 esbuild 把跨入口共享的代码(Preact、rang 的注册表、共享 辅助函数)提取到一个 chunk-<hash>.js 中,供每个 island 块导入——这 正是将发出的 _islands/ 载荷从约 4.74 MB 削减到约 0.40 MB 的原因。island 源从 dwar 自己的 node_modules 中导入 @clean-jsdoc-theme/rang

阅读源码

维护者希望把你引向代码——从这里开始:

下一步