@lit-labs/ssr-dom-shim:Lit 服务端渲染 DOM 垫片的设计与版本演进深度解析
2026/9/13 11:54:36 网站建设 项目流程

@lit-labs/ssr-dom-shim:Lit 服务端渲染 DOM 垫片的设计与版本演进深度解析

【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit

在 Node.js 中服务端渲染(SSR)Web Components 时,最大的障碍是 DOM API 的缺失:HTMLElementCustomElementRegistryShadowRoot等浏览器内建对象在 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 的步骤。

该版本的核心改动有三点:

  1. 引入新的@lit-labs/ssr-dom-shim包,导出HTMLElementCustomElementRegistry以及默认的customElements单例;
  2. 旧的@lit-labs/ssr全局 DOM shim 依然可用,并且因为@lit-labs/ssr转而从@lit-labs/ssr-dom-shim导入,两者保持兼容;
  3. 官方建议:尽量移除对@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特殊情况),所有导出都从主模块显式引入。完整的导出值与各自的继承关系如下:

导出继承自提供的方法/属性
EventTargetaddEventListenerdispatchEventremoveEventListener
NodeEventTargetgetRootNode
ElementNodeattachShadowshadowRootattributeshasAttributegetAttributesetAttributeremoveAttribute
HTMLElementElement
CustomElementRegistrydefinegetgetNamewhenDefined
customElements默认的CustomElementRegistry单例实例
Event标准事件对象
CustomEventEventdetail
MutationObserverobservetakeRecordsdisconnect
ResizeObserverobserveunobservedisconnect
IntersectionObserverrootrootMarginthresholdsobservetakeRecordsunobservedisconnect
MediaListmediaTextappendMediumdeleteMediumitem
StyleSheetdisabledmediatype
CSSRule/CSSRuleList规则常量与item
CSSStyleSheetStyleSheetreplacereplaceSynccssRules

除此之外,index.ts 还额外导出了ElementInternalsariaMixinAttributesHYDRATE_INTERNALS_ATTR_PREFIXDocumentdocumentWindowwindowShadowRootHTMLSlotElement等符号(这些在 README 的导出表格之后随版本迭代逐步补齐)。

2.1 谁在使用这些导出

从源码调用链看:

  • packages/labs/ssr/src/lib/dom-shim.ts 在构造 SSR 的window对象时,逐个挂载EventTargetEventCustomEventElementHTMLElementDocumentdocumentCSSStyleSheetShadowRootCustomElementRegistry和独立的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"(见下文第四节):除customElementsEventCustomEvent外,其余 shim 都不写入全局,而是通过模块导入提供;唯独这二者被提升为全局,从而保证用户组件在 Node 中可以直接调用customElements.define(...)new Event(...)/new CustomEvent(...)

三、元素与注册表 shim 的演进史(1.1.0 → 1.4.0)

ElementHTMLElementCustomElementRegistry是整套垫片的地基,其能力在多个版本中逐步补齐:

3.1 1.1.0:attachInternals的粗略支持

1.1.0 为HTMLElement.prototype增加了attachInternals的粗略实现,对应源码 index.ts:

attachInternals(): 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.ts:ElementInternalsShim是"方法为空操作、属性默认值"的占位实现——checkValidity()固定返回true(并在服务器端打印一条警告日志),setFormValue/setValidity为空函数,formnulllabels为空数组。同时该文件导出了完整的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.ts:

if (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.0:toggleAttribute加入 Element shim

1.2.0 为Element增加了toggleAttribute(name, force?),其实现(index.ts)逐条对应 WHATWG DOM 规范#dom-element-toggleattribute的步骤:已存在属性时按force决定是否移除;不存在时按force决定是否以空串添加。

3.4 1.2.1:localNametagName,修复 issue #3375

1.2.1 实现了Element.localNameElement.tagName,修复了 issue 3375。其实现巧妙地复用了注册机制:localName返回构造函数上的__localName(由customElements.define写入),tagName返回其大写形式:

get localName() { return (this.constructor as NamedCustomHTMLElementConstructor).__localName; } get tagName() { return this.localName?.toUpperCase(); }

对于特殊元素(如<slot>),HTMLSlotElementShim 会覆写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。initializeupgrade在 SSR 中没有意义,直接抛出"not currently supported in SSR"的错误;whenDefined则以 Promise 形式支持注册完成后的回调。

四、事件系统的完整实现(1.3.0 → 1.6.0)

事件处理是 SSR 组件中监听器、状态派发得以工作的前提,也是该包最有技术含量的一部分。

4.1 1.3.0:SSR 事件处理与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/stopImmediatePropagationonce选项、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.0:ShadowRootdocument进入事件路径

1.6.0 的第一个 Minor Change 是"在事件路径中实现ShadowRootdocument"。此前,dispatchEvent解析出的完整事件路径[this, ...parent...]会在没有__eventTargetParent时以[this, documentShim, windowShim]结尾(index.ts);现在ShadowRoot实例(由attachShadow创建并设置__eventTargetParent = host__host = host)与全局document单例都能正确地作为事件传播链中的一环参与捕获与冒泡。测试文件 event-target-shim_test.ts 覆盖了相关场景。

五、Observer 家族:优雅的"空操作"(1.6.0)

MutationObserverResizeObserverIntersectionObserver的 shim 由 1.6.0 引入,实现在 lib/observers.ts。文件头注释直接说明了设计哲学:

这是 observer 类家族的有限实现,本质上是 no-op 实现,让使用它们的代码可以无错运行,但并不真正执行任何观察。这在 SSR 中可行,因为被观察的变化永远不会发生。

具体行为:

  • MutationObserverobserve/disconnect为空函数,takeRecords()恒返回[]
  • ResizeObserverobserve/unobserve/disconnect为空函数;
  • IntersectionObserver略微"有状态":rootrootMargin(默认'0px 0px 0px 0px')、thresholds(标量阈值包装为数组,默认[0])三个 getter 会如实回显构造时传入的IntersectionObserverInittakeRecords()返回[]

这套设计保证了组件中依赖观察器的代码(如虚拟滚动、可见性打点)在 SSR 阶段被安全引入而不抛错。

六、CSSStyleSheet 与 Node.js CSS 加载 Hook(1.5.0 → 1.6.0)

这是该包近年来最重要的一组能力,也是 README.md 单独开辟一节介绍的功能。

6.1 1.5.0:有限的CSSStyleSheet与 CSS loader

1.5.0 实现了CSSStyleSheet的有限 shim 及配套的 Node.js CSS 加载器。样式相关实现集中在 lib/css.ts:

  • MediaList直接extends Array<string>mediaText', '连接,appendMedium/deleteMedium提供去重与删除;
  • StyleSheet提供disabledmediatype === 'text/css'及恒为nullhref/ownerNode/parentStyleSheet/title
  • CSSRule定义了STYLE_RULEMEDIA_RULEKEYFRAMES_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:moduleregister('./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 原文):

# 方式 1:Node.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.0(Node.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.jsonfiles字段包含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 更新补齐的ariaColIndexTextariaRelevantariaRowIndexText等 ARIAMixin 属性(反映在 lib/element-internals.ts 的ariaMixinAttributesElementInternalsShim字段中),以及 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 的模式:

  1. 写入globalThis:把 shim 赋给全局对象,并确保赋值发生在用户代码运行之前;
  2. 模块直导 +nodeexport condition:从提供基类的模块直接导入 shim,借助 Node.js 的条件导出保证只在 Node 中生效、浏览器不受影响。

Lit 除customElementsEventCustomEvent外的所有 shim 都采用方式 #2,从而既满足组件在 Node 中对customElements.definenew Event()的调用需求,又不污染浏览器全局。

八、版本演进一览

版本类型核心变更
1.0.0Major引入本包;Lit 在 Node 中自动注入最小 DOM shims;导出HTMLElementCustomElementRegistrycustomElements单例
1.1.0Minor粗略支持HTMLElement.prototype.attachInternals
1.1.1Patch开发模式下重复注册自定义元素改为警告而非抛错
1.1.2 / 1.1.2-prePatchTypeScript 升级至 5.0 / ~5.2.0
1.2.0MinorElement shim 新增toggleAttribute
1.2.1Patch实现Element.localNameElement.tagName,修复 issue #3375
1.3.0Minor实现 SSR 事件处理;新增可选标志globalThis.litSsrCallConnectedCallback控制 SSR 中是否调用connectedCallback
1.4.0Minor完整实现CustomElementRegistry类型,提升保真度与可编译性
1.5.0Minor实现有限的CSSStyleSheetshim 与 Node.js CSS 加载器;随 TS 5.8 更新 ARIAMixin(ariaColIndexText等)
1.5.1Patch将 register css hook 文件加入发布清单
1.6.0Minor + PatchShadowRootdocument进入事件路径;新增MutationObserver/ResizeObserver/IntersectionObservershim;hook 导入 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、attachInternalslocalName/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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询