ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

@lit-labs/ssr-client 客户端水合支持模块解析:renderLight 指令与 defer-hydration 机制

@lit-labs/ssr-client 客户端水合支持模块解析:renderLight 指令与 defer-hydration 机制 lit-labs/ssr-client 客户端水合支持模块解析renderLight 指令与 defer-hydration 机制【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/litlit-labs/ssr-client是 Lit 仓库中与lit-labs/ssr配套的客户端侧支持模块集负责把服务端渲染SSR产出的静态 HTML 在浏览器端恢复为 lit-html 的模板实例与 Parts 数据结构从而让后续更新走高效的增量渲染路径。本文基于 packages/labs/ssr-client/README.md 及仓库源码深入讲解renderLight指令、hydrate()水合算法与defer-hydration属性延迟启用机制读完即可在自有 Lit 项目中落地SSR 输出 客户端水合的完整链路。包定位服务端渲染的客户端搭档lit-labs/ssr-client的官方定位是 A set of client-side support modules for rendering Lit components and templates on the server usinglit-labs/ssr——即一组配合lit-labs/ssr在服务器上渲染 Lit 组件与模板所需的浏览器端模块。其 package.json 描述为 Client package for lit-labs/ssr当前版本 1.1.8许可证 BSD-3-Clause。包内容由三部分组成对应 README.md 与 rollup.config.js 中的三个入口点入口源码职责index.js包主入口src/index.ts对外导出hydrate()水合函数lit-element-hydrate-support.jssrc/lit-element-hydrate-support.ts为LitElement打补丁支持defer-hydration延迟水合directives/render-light.jssrc/directives/render-light.tsrenderLight指令以父元素的renderLight()方法作为渲染值src/index.ts仅一行export * from ./lib/hydrate-lit-html.js也就是说整个包的核心能力都汇聚在 src/lib/hydrate-lit-html.ts 的hydrate()上。从 CHANGELOG.md 的 1.1.0 版本记录可以确认本包的hydrate与litElementHydrateSupport分别是从lit-html/experimental-hydrate.js和lit-element/experimental-hydrate-support.js迁移而来原位置的模块已被标记为废弃。安装与模块入口lit-labs/ssr-client依赖lit、lit-html与lit/reactive-element版本见 package.json 的dependencies支持 lit v2/v3。安装后三种模块的导入路径如下依据 package.json 的exports字段// 主入口水合函数 import {hydrate} from lit-labs/ssr-client; // LitElement 水合支持副作用模块导入即打补丁 import lit-labs/ssr-client/lit-element-hydrate-support.js; // renderLight 指令 import {renderLight} from lit-labs/ssr-client/directives/render-light.js;package.json的exports为每个入口同时声明了browser、node与development条件导出其中node条件指向./node/index.js由 Rollup 额外产出 Node 构建rollup.config.js中includeNodeBuild: true。CHANGELOG 1.1.1 修复过 Cannot find module ... ssr-client/node/index.js 的问题即 Node 构建产物必须在发布包中完整包含。renderLight 指令让组件与使用者各管各的 light DOM背景与用途renderLight是 README.md 唯一显式列出的包内容A child-position directive that invokes and renders the parent custom elementsrenderLightmethod as its value——一个子位置child-position指令把父级自定义元素的renderLight()方法的返回值作为自身渲染值。它解决的典型场景宿主元素在shadow DOM 之外渲染关键内容如正文标题、段落同时只在前端渲染 shadow DOM 中的外壳插槽、按钮等交互元素。这样 SSR 输出的 HTML 中关键内容可以直接以普通 DOM 形式出现不依赖组件定义即可被索引和渲染也不会把组件的实现标记拷贝进 HTML 造成载荷膨胀。接口与实现src/directives/render-light.ts 定义了宿主契约export interface RenderLightHost extends HTMLElement { renderLight(): unknown; }指令类RenderLightDirective通过静态标记static _$litRenderLight true供服务端识别update()在客户端拿到part.parentNode作为宿主实例并调用其renderLight()。配套导出isRenderLightDirective(value)用于判断某个值是否由该指令产生内部借助lit/directive-helpers.js的getDirectiveClass反查指令类。服务端如何配合服务端渲染时lit-labs/ssr的 src/lib/render-value.ts 会导入isRenderLightDirective并在渲染值解析时检测到 renderLight 指令后调用宿主的renderLight(renderInfo)方法并渲染其返回值。由此形成闭环服务端遇到renderLight()→ 调用并输出renderLight()的返回值客户端遇到renderLight()→update()中调用宿主的renderLight()取得渲染值。这正是文档注释中所说的 The component doesnt actually render to its light DOM, its user does. The component provides the implementation——组件只提供实现真正把内容放进 light DOM 的是组件使用者从而避免组件与使用者在 light DOM 上产生竞争。完整示例以下示例完整继承自 src/directives/render-light.ts 的文档注释。先定义组件light DOM 只包含内容shadow DOM 包含外壳与交互元素。class StoryElement extends LitElement implements RenderLightHost { property() title; property() body; renderLight() { return html h1${this.title}/h1 p${this.body}/p ; } render() { return html slot/slot button client${this.like}Like/button } }服务端输出light DOM 即内容本身x-story h1Hello World/h1 pThis is a story about greeting the earth./p /x-story客户端水合完成后shadow DOM 附加外壳与交互元素x-story #shadow-root slot/slot buttonLike/button h1Hello World/h1 pThis is a story about greeting the earth./p /x-story组件使用方必须在书写模板时显式使用renderLight()指令来启用该行为const story { title: Hello World, body: This is a story about greeting the earth., }; const t (story) html x-story .title${story.title} .body${story.body} ${renderLight()} /x-story源码注释同时指出了该方案的一个已知待办TBD组件需要为它的 light DOM 提供样式但显然无法享受 shadow DOM 的样式隔离。作为权衡其优势是关键内容在服务端渲染、无需加载组件定义即可索引与渲染同时 HTML 载荷中不会重复出现组件实现标记社区中类似拆分 light/shadow 渲染的实践反馈其首次内容绘制FCP通常快于常见的深水合deep SSR方案。hydrate()把 SSR 标记恢复为可更新的模板实例前置约束src/lib/hydrate-lit-html.ts 的文档注释明确了水合的硬性前提hydrate()必须在符合 lit-ssr 结构输出的 DOM 上调用——ChildPart必须用成对的开始/结束注释标记表示包含TemplateInstance的ChildPart必须把模板摘要template digest写进注释数据中由于render()的输出总被包裹在一个根ChildPart中因此每个容器必须恰好存在一个根 ChildPart。SSR 输出结构与标记约定以模板htmldiv class${x}${y} 为例SSR 输出的 DOM 结构为!--lit-part AEmR7WR0Ak-- !-- 根 ChildPart 的开始标记内含模板 digest -- !--lit-node 0-- !-- 指示下一节点含属性绑定数字为模板中该节点的深度优先索引 -- div classTEST_X !--lit-part-- !-- ${y} 表达式的开始标记 -- TEST_Y !--/lit-part-- !-- ${y} 表达式的结束标记 -- /div !--/lit-part-- !-- 根 ChildPart 的结束标记 --三类标记注释构成了水合的地图!--lit-part [digest]--打开一个 ChildPart若该 part 承载 TemplateResultdigest 必须与服务端写入的一致!--lit-node N--指示下一个兄弟元素带有属性/属性property/事件/元素绑定或是一个需要移除defer-hydration属性的自定义元素N是模板内节点深度优先索引用于完整性校验!--/lit-part--关闭当前 ChildPart。算法流程hydrate(rootValue, container, options)的核心是一个基于document.createTreeWalker(container, NodeFilter.SHOW_COMMENT)的单遍注释遍历src/lib/hydrate-lit-html.ts若容器已存在_$litPart$活跃渲染直接抛出container already contains a live render遇到lit-part标记时调用openChildPart()按栈顶状态叶子/可迭代/模板实例构造ChildPart并按照ChildPart.commit()的级联顺序directive → noChange → primitive → TemplateResult → Iterable → 兜底初始化提交值若值为 TemplateResult会校验标记中的 digest 与服务端一致lit-part ${digestForTemplateResult(value)}不一致则抛出Hydration value mismatch遇到lit-node标记时调用createAttributeParts()移除后续节点的defer-hydration属性并根据TemplateInstance的模板 parts 逐一重建AttributePart/ElementPart事件与属性property绑定因未被序列化需要真正提交noCommit为 false普通属性绑定则只初始化提交值避免不必要的 DOM 触碰性能考量遇到/lit-part标记时调用closeChildPart()闭合 part 并弹栈遍历结束把根 part 写入容器的_$litPart$缓存此后 lit-html 行为等同于首次客户端渲染后续更新走正常高效增量路径。模板摘要digest算法digestForTemplateResult()为模板生成跨环境一致的摘要字符串用于 SSR 与服务端双端匹配模板。实现要点见 src/lib/hydrate-lit-html.ts采用 2 个 32 位哈希digestSize 2的 DJB2 变体源于 string-hash 并改为哈希数组以增加位数种子为 5381逐字符处理模板的strings数组目标为极低碰撞率、极快、极小代码体积且不追求密码学安全性浏览器端用btoa编码Node 端通过 Rollup 注入的Buffer做 base64注释说明这是因为 Node 的隔离 VM 上下文可能没有全局btoa且要兼容 Node 14摘要结果缓存在WeakMapTemplateStringsArray, stringCHANGELOG 1.1.8 记录该缓存用于降低重复渲染同一模板的开销。已知限制与错误信息从源码可直接确认的边界条件不支持编译模板compiled templatesopenChildPart()中若isCompiledTemplateResult(value)为真直接抛错CHANGELOG 1.1.3 优化了该错误信息服务器与客户端渲染结果不一致时抛出Hydration value mismatch: Unexpected TemplateResult rendered to part模板分支变化或Primitive found where TemplateResult expected条件渲染导致的值类型变化见createAttributeParts()中的详细错误提示CHANGELOG 1.1.6 改进了此信息迭代器长度不一致会抛出Unhandled shorter than expected iterable/unexpected longer than expected iterable容器内根 part 缺失或重复会给出There should be exactly one root part per container的报错无根 part 时向console.error输出容器描述ShadowRoot 场景会指明宿主元素名。源码中还标注了未决问题如 leaf part 意外承载 TemplateResult 的情况关联 lit 仓库 issue #1434属已知的待完善边界而非已承诺行为。lit-element-hydrate-supportdefer-hydration 延迟启用机制src/lit-element-hydrate-support.ts 是一个副作用模块通过globalThis.litElementHydrateSupport类型声明见 src/env.d.ts为LitElement打补丁实现SSR 输出的元素先保持静态待水合时机成熟再启用。整体流程如下监听defer-hydration属性覆写observedAttributes的 getter把defer-hydration追加进观察列表常量HYDRATE_INTERNALS_ATTR_PREFIX hydrate-internals-与lit-labs/ssr-dom-shim保持一致用于清理 SSR 期间由 internals shim 添加的 aria 属性延迟 connectedCallback覆写connectedCallback当元素带有defer-hydration属性时不执行基类逻辑等待属性被移除同时在attributeChangedCallback中监听defer-hydration被移除值为 null时调用原始的connectedCallback完成启用复用 SSR 生成的 shadowRoot覆写createRenderRoot若元素已存在shadowRootSSR 输出的template shadowroot声明式 shadow DOM 已被浏览器解析则标记_$needsHydration true并直接复用不再调用基类实现重新创建/附加样式首次更新时水合覆写update先调用原型链上的原始update完成属性更新逻辑若需要水合则先清理hydrate-internals-*前缀对应的 aria 属性再调用hydrate(value, this.renderRoot, this.renderOptions)否则走普通render()路径。其中 removedefer-hydration 的职责实际由水合遍历中的createAttributeParts()承担——它在处理lit-node标记时对后续节点调用node.removeAttribute(defer-hydration)src/lib/hydrate-lit-html.ts 中注释明确说明 lit-node 标记的一个用途就是标记需要移除 defer-hydration 属性的自定义元素。测试验证src/test/attribute-hydration_test.ts 通过setHTMLUnsafe模拟 SSR 输出的 HTML含defer-hydration与hydrate-internals-aria-label等成对属性、template shadowroot声明式 shadow DOM、以及!--lit-part T5fUn6aagr0--这类带 digest 的水合标记验证了两个关键行为水合前defer-hydration与所有hydrate-internals-*/aria 属性均存在移除defer-hydration并等待updateComplete后所有 aria 与hydrate-internals-*属性被全部清除CHANGELOG 1.1.8 修复了此前的清理不完整问题。测试本身也印证了 lit-element-hydrate-support.ts 的行为水合成功后SSR 期由 internals shim 临时写出的aria-label、role等会从元素上移除因为此时 ElementInternals 已可用属性不再必要。构建、测试与工程细节构建tsc --build输出development/随后 Rolluprollup.config.js基于仓库根级 rollup-common.js 的litProdConfig产出浏览器与 NodeincludeNodeBuild: true两套构建treemirror负责把.d.ts映射到包根目录。package.json的wireit配置声明build依赖../../lit:build即构建前需先构建 lit 主包测试test:dev/test:prod通过 packages/tests 的run-web-tests.js配合 web-test-runner.config.ts 在浏览器中分别运行 development 与 production 两种构建的测试环境变量BROWSERS可外部注入发布发布产物仅包含development/、directives/、lib/、node/及根级入口文件不含src/CHANGELOG 1.0.0 记录 Dont publish src/ to npm。版本演进要点从 CHANGELOG.md 可梳理出该模块的关键演进1.1.0lit-html/experimental-hydrate.js与lit-element/experimental-hydrate-support.js迁移至本包原位置标记废弃1.1.1修复 Node 构建产物缺失导致的加载失败1.1.4避免??空值逻辑赋值修复 Next.js 生产打包下的水合错误关联 issue #42891.1.5放宽lit依赖范围以兼容 lit v21.1.6改进水合值不匹配的错误信息1.1.8修复hydrate-internals-/aria 属性清理不完整并引入 digest 缓存。小结lit-labs/ssr-client通过三件套完成SSR 之后、客户端接管的衔接renderLight指令让组件可以把 light DOM 的渲染权交给使用者服务端输出纯内容、客户端补 shadow DOMhydrate()以注释标记为地图重建 lit-html 的 Parts 数据结构使后续更新无需全量重渲染lit-element-hydrate-support以defer-hydration属性为开关控制 SSR 元素在合适时机才启用并复用既有 shadowRoot。配合 packages/labs/ssr 服务端渲染与 src/test/attribute-hydration_test.ts 中的端到端形态即可在 Lit 项目中构建服务端输出可索引内容、客户端无缝接管的完整水合链路。【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进