
lit-labs/ssr-dom-shimLit 服务端渲染 DOM 垫片的设计与版本演进深度解析【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit在 Node.js 中服务端渲染SSRWeb Components 时最大的障碍是 DOM API 的缺失HTMLElement、CustomElementRegistry、ShadowRoot等浏览器内建对象在 Node 环境中并不存在任何引用它们的组件代码都会在import阶段直接崩溃。Lit 给出的解决方案是位于 packages/labs/ssr-dom-shim 的lit-labs/ssr-dom-shim包——一套精心裁剪的最小 DOM 实现专门服务于 SSR 场景。本文以其 CHANGELOG.md 为主体脉络结合 README.md 与src源码实现完整梳理该包从 1.0.0 到 1.6.0 的能力演进、导出清单与底层原理帮助你理解 Lit SSR 为何能在 Node 中无痛运行自定义元素以及如何在自有框架中复用这套垫片。一、诞生背景Lit 在 Node 中自动注入的最小 DOM 垫片lit-labs/ssr-dom-shim首次出现于 1.0.0 版本CHANGELOG.md 1.0.0 条目它是一次重大变更Major Change在 Node 中运行 Lit 时Lit 会自动包含足以覆盖绝大多数 SSR 用例的最小 DOM shims从而移除此前从lit-labs/ssr手动导入全局 DOM shim 的步骤。该版本的核心改动有三点引入新的lit-labs/ssr-dom-shim包导出HTMLElement、CustomElementRegistry以及默认的customElements单例旧的lit-labs/ssr全局 DOM shim 依然可用并且因为lit-labs/ssr转而从lit-labs/ssr-dom-shim导入两者保持兼容官方建议尽量移除对lit-labs/ssrDOM shim 的依赖改用lit/reactive-element现在自动提供的更小、自动注入的垫片。也就是说这一版本标志着 Lit SSR 的架构从手动装一个全局大 shim转向按需自动注入的最小 shim。从源码看packages/labs/ssr/src/lib/dom-shim.ts 直接import ... from lit-labs/ssr-dom-shim后构造 vm 上下文证实了lit-labs/ssr对它的依赖关系。二、包导出的完整 API 清单根据 README.md 的 Exports 章节该包默认不设置任何全局变量除后文会提到的Event/CustomEvent特殊情况所有导出都从主模块显式引入。完整的导出值与各自的继承关系如下导出继承自提供的方法/属性EventTarget—addEventListener、dispatchEvent、removeEventListenerNodeEventTargetgetRootNodeElementNodeattachShadow、shadowRoot、attributes、hasAttribute、getAttribute、setAttribute、removeAttributeHTMLElementElement—CustomElementRegistry—define、get、getName、whenDefined等customElements—默认的CustomElementRegistry单例实例Event—标准事件对象CustomEventEventdetailMutationObserver—observe、takeRecords、disconnectResizeObserver—observe、unobserve、disconnectIntersectionObserver—root、rootMargin、thresholds、observe、takeRecords、unobserve、disconnectMediaList—mediaText、appendMedium、deleteMedium、itemStyleSheet—disabled、media、type等CSSRule/CSSRuleList—规则常量与itemCSSStyleSheetStyleSheetreplace、replaceSync、cssRules除此之外index.ts 还额外导出了ElementInternals、ariaMixinAttributes、HYDRATE_INTERNALS_ATTR_PREFIX、Document、document、Window、window、ShadowRoot、HTMLSlotElement等符号这些在 README 的导出表格之后随版本迭代逐步补齐。2.1 谁在使用这些导出从源码调用链看packages/labs/ssr/src/lib/dom-shim.ts 在构造 SSR 的window对象时逐个挂载EventTarget、Event、CustomEvent、Element、HTMLElement、Document、document、CSSStyleSheet、ShadowRoot、CustomElementRegistry和独立的customElements实例packages/labs/ssr/src/lib/lit-element-renderer.ts 与 render-value.ts 中的渲染逻辑依赖HTMLElement与事件路径相关元数据__eventTargetParent、__host、__slots来重建 shadow DOM 与事件冒泡关系。2.2 全局注入的特殊例外index.ts 中有两行显式的全局赋值globalThis.Event ?? EventShim; globalThis.CustomEvent ?? CustomEventShim;这是因为对应 README 的说明Lit 采用方式 #2见下文第四节除customElements、Event、CustomEvent外其余 shim 都不写入全局而是通过模块导入提供唯独这二者被提升为全局从而保证用户组件在 Node 中可以直接调用customElements.define(...)或new Event(...)/new CustomEvent(...)。三、元素与注册表 shim 的演进史1.1.0 → 1.4.0Element、HTMLElement与CustomElementRegistry是整套垫片的地基其能力在多个版本中逐步补齐3.1 1.1.0attachInternals的粗略支持1.1.0 为HTMLElement.prototype增加了attachInternals的粗略实现对应源码 index.tsattachInternals(): ElementInternals { if (this.__internals ! null) { throw new Error(... ElementInternals for the specified element was already attached.); } const internals new ElementInternalsShim(this as unknown as HTMLElement); this.__internals internals; return internals as ElementInternals; }其底层实现位于 lib/element-internals.tsElementInternalsShim是方法为空操作、属性默认值的占位实现——checkValidity()固定返回true并在服务器端打印一条警告日志setFormValue/setValidity为空函数form为nulllabels为空数组。同时该文件导出了完整的ariaMixinAttributes映射ariaAtomic - aria-atomic、role - role等 40 项并定义了HYDRATE_INTERNALS_ATTR_PREFIX hydrate-internals-前缀供客户端水合阶段恢复 internals 状态。值得注意的是shadowRootgetter 会绕过closed模式直接返回宿主元素的__shadowRoot因为服务器端需要保证 internals 实例总能拿到 shadow root源码注释明确说明这一点。3.2 1.1.1重复注册从抛错降级为警告在开发模式下对同一名称重复执行customElements.define(name, ctor)时1.1.1 起不再直接抛出异常而是输出警告。源码位于 index.tsif (this.__definitions.has(name)) { if (process.env.NODE_ENV development) { console.warn( CustomElementRegistry already has ${name} defined. This may have been caused by live reload or hot module replacement ... ); } else { throw new Error(... the name ${name} has already been used with this registry); } }这契合 SSR 开发中热重载HMR反复加载模块的现实——同一个自定义元素类可能在开发服务器中被多次注册此时警告比崩溃更友好生产构建NODE_ENV ! development仍保持规范要求的抛错行为。此外define还会做同一构造函数重复注册的检查并在注册时把__localName写到构造函数上为后面的localName/tagName支持埋下伏笔。3.3 1.2.0toggleAttribute加入 Element shim1.2.0 为Element增加了toggleAttribute(name, force?)其实现index.ts逐条对应 WHATWG DOM 规范#dom-element-toggleattribute的步骤已存在属性时按force决定是否移除不存在时按force决定是否以空串添加。3.4 1.2.1localName与tagName修复 issue #33751.2.1 实现了Element.localName与Element.tagName修复了 issue 3375。其实现巧妙地复用了注册机制localName返回构造函数上的__localName由customElements.define写入tagName返回其大写形式get localName() { return (this.constructor as NamedCustomHTMLElementConstructor).__localName; } get tagName() { return this.localName?.toUpperCase(); }对于特殊元素如slotHTMLSlotElementShim 会覆写localName固定返回slot。3.5 1.4.0完整的CustomElementRegistry类型1.4.0 将CustomElementRegistry的类型实现补齐到与标准一致提升了保真度与可编译性。从源码看index.ts该注册表用三个内部 Map 维护状态__definitions标签名 →{ctor, observedAttributes}__reverseDefinitions构造函数 → 标签名用于重复构造函数检测与getName__pendingWhenDefineds尚未注册的标签名 →PromiseWithResolvers用于whenDefined异步等待。define在注册时会读取observedAttributes——源码注释特别提醒这是必要的因为 Lit 中它是带副作用的 getter读取会触发类 finalization。initialize与upgrade在 SSR 中没有意义直接抛出not currently supported in SSR的错误whenDefined则以 Promise 形式支持注册完成后的回调。四、事件系统的完整实现1.3.0 → 1.6.0事件处理是 SSR 组件中监听器、状态派发得以工作的前提也是该包最有技术含量的一部分。4.1 1.3.0SSR 事件处理与litSsrCallConnectedCallback标志1.3.0 引入两件事SSR 事件处理机制以及可选全局标志globalThis.litSsrCallConnectedCallback——当该标志被设为true时SSR 过程中会调用组件的connectedCallback。事件机制的核心是 index.ts 中自实现的EventTarget类。它通过三个私有元数据字段重建事件传播链EventTargetShimMeta__eventTargetParent事件路径中的上一个/下一个目标注意不是 DOM 父节点__host若目标位于 shadow DOM 内部则指向其宿主元素__slots插槽名 → 对应slot元素的映射用于正确处理分配到具名插槽的节点上的事件重定向。dispatchEvent实现了完整的捕获 → 目标 → 冒泡三阶段流程先按composedPath反序执行捕获阶段再按正序执行冒泡阶段处理了composed: false事件在 shadow DOM 边界截断、target重定向retargeting、stopPropagation/stopImmediatePropagation、once选项、AbortSignal 自动解绑、eventPhase/currentTarget/srcElement等属性的动态补丁以及监听器既可以是函数也可以是handleEvent对象的规范行为。源码中还用一张详细的事件路径示例图main→my-el1→ shadow DOM →slot→ 嵌套 shadow DOM解释了__slots为什么必须显式跟踪shadow DOM 的渲染顺序导致插槽元素不在目标元素的同树路径上无法靠遍历还原。Event/CustomEvent的垫片实现位于 lib/events.ts标注为改编自 Node.js 的lib/internal/event_target.js定义了NONE / CAPTURING_PHASE / AT_TARGET / BUBBLING_PHASE四阶段常量并暴露实例与静态两套属性。4.2 1.6.0ShadowRoot与document进入事件路径1.6.0 的第一个 Minor Change 是在事件路径中实现ShadowRoot和document。此前dispatchEvent解析出的完整事件路径[this, ...parent...]会在没有__eventTargetParent时以[this, documentShim, windowShim]结尾index.ts现在ShadowRoot实例由attachShadow创建并设置__eventTargetParent host、__host host与全局document单例都能正确地作为事件传播链中的一环参与捕获与冒泡。测试文件 event-target-shim_test.ts 覆盖了相关场景。五、Observer 家族优雅的空操作1.6.0MutationObserver、ResizeObserver、IntersectionObserver的 shim 由 1.6.0 引入实现在 lib/observers.ts。文件头注释直接说明了设计哲学这是 observer 类家族的有限实现本质上是 no-op 实现让使用它们的代码可以无错运行但并不真正执行任何观察。这在 SSR 中可行因为被观察的变化永远不会发生。具体行为MutationObserverobserve/disconnect为空函数takeRecords()恒返回[]ResizeObserverobserve/unobserve/disconnect为空函数IntersectionObserver略微有状态root、rootMargin默认0px 0px 0px 0px、thresholds标量阈值包装为数组默认[0]三个 getter 会如实回显构造时传入的IntersectionObserverInittakeRecords()返回[]。这套设计保证了组件中依赖观察器的代码如虚拟滚动、可见性打点在 SSR 阶段被安全引入而不抛错。六、CSSStyleSheet 与 Node.js CSS 加载 Hook1.5.0 → 1.6.0这是该包近年来最重要的一组能力也是 README.md 单独开辟一节介绍的功能。6.1 1.5.0有限的CSSStyleSheet与 CSS loader1.5.0 实现了CSSStyleSheet的有限 shim 及配套的 Node.js CSS 加载器。样式相关实现集中在 lib/css.tsMediaList直接extends ArraystringmediaText以, 连接appendMedium/deleteMedium提供去重与删除StyleSheet提供disabled、media、type text/css及恒为null的href/ownerNode/parentStyleSheet/titleCSSRule定义了STYLE_RULE、MEDIA_RULE、KEYFRAMES_RULE等全套规则类型常量实例与静态各一份cssText字段承载规则文本CSSStyleSheet继承StyleSheetreplaceSync(text)会清空cssRules并压入一条cssText为原文的规则replace(text)则调用replaceSync后返回Promise.resolve(this)insertRule/deleteRule/addRule/removeRule明确抛出Method not implemented.。6.2 CSS 加载 Hook 的三种注册方式配套的加载钩子让 Node.js 能直接把 CSS 文件导入为CSSStyleSheet实例。入口文件 register-css-hook.ts 做了三件事探测当前环境是否原生支持 CSS 导入通过data:text/css;base64,...的with {type: css}导入试错不支持时把globalThis.CSSStyleSheet补上globalThis.CSSStyleSheet ?? CSSStyleSheet确保 reactive-element 的 css-tag.ts 中引用的全局符号在求值时可用最后调用node:module的register(./lib/css-hook.js, ...)注册钩子。钩子本身在 lib/css-hook.ts当context.importAttributes.type css时读取文件内容并生成一段模块代码——new CSSStyleSheet()→replaceSync(内容)→export default sheet当导入路径以.css结尾但没有携带 import attributes 时会给出友好警告提示应写作import s from ./a.css with {type: css}。因此代码中可以这样使用import styles from my-styles.css with {type: css}; // styles 现在是一个 CSSStyleSheet 实例注册钩子支持三种等价方式README 原文# 方式 1Node.js CLI 参数 node --import lit-labs/ssr-dom-shim/register-css-hook.js my-script.js # 方式 2环境变量 NODE_OPTIONS--import lit-labs/ssr-dom-shim/register-css-hook.js// 方式 3内联导入仅对之后动态导入的模块生效 import lit-labs/ssr-dom-shim/register-css-hook.js; await import(./my-component.js);前提是 Node.js 18.6.0Node.js Customization Hooks 在exports中声明了./register-css-hook.js子路径测试脚本也以NODE_OPTIONS--import ./register-css-hook.js运行可见这是官方支持的一等入口。6.3 1.5.1 与 1.6.0 的收尾修补1.5.1 把register-css-hook相关文件加入发布清单package.json的files字段包含register-css-hook.{d.ts,d.ts.map,js,js.map}与lib/1.6.0 的 Patch使用 Node.js hook 导入 CSS 文件时将CSSStyleSheetpolyfill 注册到全局作用域即上文globalThis.CSSStyleSheet ??那一行保证钩子生成代码中的new CSSStyleSheet()总能拿到构造函数1.6.0 另一项 Patch 为 TypeScript 6.0.3 兼容性补充了 stub 类型。类似的还有 1.5.0 随 TypeScript 5.8 更新补齐的ariaColIndexText、ariaRelevant、ariaRowIndexText等 ARIAMixin 属性反映在 lib/element-internals.ts 的ariaMixinAttributes与ElementInternalsShim字段中以及 1.1.2 系列对 TypeScript 5.0 / ~5.2.0 的升级。七、在 Lit 与第三方框架中的使用方式7.1 方式一直接使用Lit 用户Lit 在 Node 中运行时自动导入这些 shim因此普通 Lit 用户通常无需直接依赖或显式引入本包README 明确说明。你只需要按常规方式在 Node 中引入lit/lit/reactive-element即可安全地定义组件、调用customElements.define、构造Event。7.2 方式二面向其他库/框架的集成其他希望支持 SSR 的库或框架也可以依赖这套 shim该包计划未来迁移至webcomponents/ssr-dom-shim以更好体现其通用性。README 提供了两种向用户暴露 shim 的模式写入globalThis把 shim 赋给全局对象并确保赋值发生在用户代码运行之前模块直导 nodeexport condition从提供基类的模块直接导入 shim借助 Node.js 的条件导出保证只在 Node 中生效、浏览器不受影响。Lit 除customElements、Event、CustomEvent外的所有 shim 都采用方式 #2从而既满足组件在 Node 中对customElements.define、new Event()的调用需求又不污染浏览器全局。八、版本演进一览版本类型核心变更1.0.0Major引入本包Lit 在 Node 中自动注入最小 DOM shims导出HTMLElement、CustomElementRegistry、customElements单例1.1.0Minor粗略支持HTMLElement.prototype.attachInternals1.1.1Patch开发模式下重复注册自定义元素改为警告而非抛错1.1.2 / 1.1.2-prePatchTypeScript 升级至 5.0 / ~5.2.01.2.0MinorElement shim 新增toggleAttribute1.2.1Patch实现Element.localName与Element.tagName修复 issue #33751.3.0Minor实现 SSR 事件处理新增可选标志globalThis.litSsrCallConnectedCallback控制 SSR 中是否调用connectedCallback1.4.0Minor完整实现CustomElementRegistry类型提升保真度与可编译性1.5.0Minor实现有限的CSSStyleSheetshim 与 Node.js CSS 加载器随 TS 5.8 更新 ARIAMixinariaColIndexText等1.5.1Patch将 register css hook 文件加入发布清单1.6.0Minor PatchShadowRoot与document进入事件路径新增MutationObserver/ResizeObserver/IntersectionObservershimhook 导入 CSS 时全局注册CSSStyleSheet为 TS 6.0.3 补 stub九、验证与测试包内测试由 wireit 驱动test脚本以uvu运行并以--import ./register-css-hook.js方式预先加载 CSS 钩子。核心测试用例包括css-hook_test.ts分别验证静态导入、动态导入、含特殊字符的 CSS 文件经 hook 加载后sheet.cssRules中的cssText与源文件一致同时验证不带import attributes 的.css导入会按预期失败css_test.ts验证CSSStyleSheetshim 的replace/replaceSync行为element-shim_test.ts验证属性操作、shadow DOM、attachInternals、localName/tagName等event-target-shim_test.ts验证事件路径、捕获/冒泡、shadow DOM 边界与重定向。这些测试既是行为契约也为我们理解 shim 的能力边界提供了最直接的证据足够支撑组件代码在 Node 中安全导入与执行但刻意保持最小、不追求完整规范实现。十、小结从 1.0.0 的自动注入最小 shim到 1.6.0 的ShadowRoot 进入事件路径 Observer 家族 CSS 加载钩子全局化lit-labs/ssr-dom-shim的演进路线清晰可循始终围绕让基于 DOM API 的组件代码在 Node 中既不报错、又保持关键语义注册、事件、属性、样式这一核心目标。对于想要深入了解 Lit SSR 原理的开发者它是绝佳的入门切片对于自研 Web Components SSR 方案的团队它又是一套开箱即用、经过生产验证的基座——两者都可以从本文梳理的 源码 与 测试 中继续深挖。【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考