1111Web Component 代码在模块顶层执行 ` customElements.define() ` 、` class Foo extends HTMLElement ` 、` new CSSStyleSheet() ` 等浏览器 API。SSG/SSR 构建环境(Node.js via Vite)和运行时(Deno)不提供这些浏览器原生 API。
1212
1313当前处理方式:
14+
1415- ** CSSStyleSheet** :在 SSG entry code 顶部 ` import { StyleSheet } from '@lessjs/core' ` 后手动 shim(` build-ssg.ts ` L274-280)
15- - ** HTMLElement** :通过 Rolldown output banner 注入 ` @lit-labs/ssr-dom-shim ` polyfill(L405-407 )
16+ - ** HTMLElement** :通过 ` @lessjs/core/dsd-element.ts ` 的 ` _SsrHTMLElementStub ` 自包含( [ SOP-016 ] )
1617- ** customElements** :** 未处理** — 这是当前导致 SSR 崩溃的直接原因
1718
1819问题表现:
20+
1921``` ts
2022// app/islands/my-counter.ts
2123if (typeof customElements !== ' undefined' && ! customElements .get (tagName )) {
22- customElements .define (tagName , MyCounter ); // ← customElements is undefined in SSR
24+ customElements .define (tagName , MyCounter ); // ← customElements is undefined in SSR
2325}
2426```
2527
@@ -31,64 +33,42 @@ if (typeof customElements !== 'undefined' && !customElements.get(tagName)) {
3133
3234### Polyfill 清单
3335
34- | API | Polyfill 来源 | 注入位置 | 必要性 |
35- | -----| -------------| ---------| --------|
36- | ` CSSStyleSheet ` | ` @lessjs/core ` StyleSheet | SSG entry code 顶部 | Lit 内部引用 CSSStyleSheet,模块顶层即需 |
37- | ` HTMLElement ` | ` @lit-labs/ssr-dom-shim ` | SSG entry code 顶部 | ` class Foo extends HTMLElement ` 在模块顶层执行 |
38- | ` customElements ` | 自实现轻量 shim | SSG entry code 顶部 | ` customElements.define() ` 在模块顶层调用 |
39- | ` document ` | happy-dom(已有) | ssgRender 阶段 | DOM 操作在渲染时发生 |
40- | ` window ` | happy-dom(已有) | ssgRender 阶段 | 组件的 connectedCallback 可能访问 |
36+ | API | Polyfill 来源 | 注入位置 | 必要性 |
37+ | ---------------- | ---------------------------------- | ------------------------- | ---------------------------------------------- |
38+ | ` customElements ` | Map-backed shim([ SOP-016] ) | ` output.banner ` | ` customElements.define() ` 在模块顶层调用 |
39+ | ` HTMLElement ` | ` @lessjs/core ` 自包含([ SOP-016] ) | ` dsd-element.ts ` 模块求值 | ` class Foo extends HTMLElement ` 在模块顶层执行 |
40+ | ` CSSStyleSheet ` | ` @lessjs/core ` StyleSheet | SSG entry code 顶部 | Lit 内部引用 CSSStyleSheet,模块顶层即需 |
41+ | ` document ` | happy-dom(已有) | ssgRender 阶段 | DOM 操作在渲染时发生 |
42+ | ` window ` | happy-dom(已有) | ssgRender 阶段 | 组件的 connectedCallback 可能访问 |
43+
44+ [ SOP-016 ] : ../sop/v0.21.x/SOP-016-ssr-htmlelement-self-contained.md
4145
4246### 注入机制
4347
44- 从分散的 polyfill(entry code + output banner)统一为单一 polyfill 模块 :
48+ polyfill 按执行顺序分三层( [ SOP-016 ] ) :
4549
46- ``` ts
47- // packages/adapter-vite/src/ssr-polyfills.ts
48- export function generateSsrPolyfillBanner(): string {
49- return [
50- // Layer 1: CSSStyleSheet (no dependencies)
51- ` import { StyleSheet } from '@lessjs/core'; ` ,
52- ` if (typeof globalThis.CSSStyleSheet === 'undefined') { ` ,
53- ` globalThis.CSSStyleSheet = class { ` ,
54- ` replaceSync(_css: string) {} ` ,
55- ` get cssRules() { return []; } ` ,
56- ` }; ` ,
57- ` } ` ,
58- ' ' ,
59- // Layer 2: HTMLElement (no dependencies)
60- ` import { HTMLElement as _SsrHTMLElement } from '@lit-labs/ssr-dom-shim'; ` ,
61- ` if (!globalThis.HTMLElement) { ` ,
62- ` globalThis.HTMLElement = _SsrHTMLElement; ` ,
63- ` } ` ,
64- ' ' ,
65- // Layer 3: customElements (depends on HTMLElement existing)
66- ` if (typeof globalThis.customElements === 'undefined') { ` ,
67- ` const registry = new Map<string, typeof globalThis.HTMLElement>(); ` ,
68- ` globalThis.customElements = { ` ,
69- ` define(name: string, ctor: typeof globalThis.HTMLElement) { ` ,
70- ` registry.set(name, ctor); ` ,
71- ` }, ` ,
72- ` get(name: string) { ` ,
73- ` return registry.get(name); ` ,
74- ` }, ` ,
75- ` whenDefined(_name: string) { ` ,
76- ` return Promise.resolve(); ` ,
77- ` }, ` ,
78- ` upgrade(_root: Node) {}, ` ,
79- ` }; ` ,
80- ` } ` ,
81- ].join (' \n ' );
82- }
83- ```
50+ 1 . ** Layer 1 — ` output.banner ` :customElements(Map-backed)**
51+ - 最先执行,确保任何 ` customElements.define() ` 调用在模块图求值前可用
52+ - ` define() ` 存入 Map,` get() ` 从 Map 取出 — ` renderDSDByName() ` 依赖此行为
53+ - 见 ` packages/adapter-vite/src/cli/build-ssg.ts `
54+
55+ 2 . ** Layer 2 — ` @lessjs/core/dsd-element.ts ` :HTMLElement(自包含 stub)**
56+ - ` _SsrHTMLElementStub ` 提供 6 个成员
57+ - 在 ` typeof HTMLElement === 'undefined' ` 时赋值到 ` globalThis.HTMLElement `
58+ - ` @lessjs/core ` 不再依赖 ` @lit-labs/ssr-dom-shim `
8459
60+ 3 . ** Layer 3 — SSG entry code:CSSStyleSheet**
61+ - ` import { StyleSheet } from '@lessjs/core' ` 后在 entry code body 中 polyfill
62+ - 见 ` packages/adapter-vite/src/ssr-polyfills.ts `
63+
64+ ````
8565### 构建集成
8666
8767在 `build-ssg.ts` 的 SSG entry code 生成阶段,polyfill banner 作为 entry code 的第一部分:
8868
8969```ts
9070const rawSsgEntryCode = generateSsrPolyfillBanner() + '\n' + generateHonoEntryCode(routes, {...});
91- ```
71+ ````
9272
9373替代原来分散在 entry code 和 output banner 中的 polyfill 代码。
9474
@@ -117,29 +97,28 @@ const rawSsgEntryCode = generateSsrPolyfillBanner() + '\n' + generateHonoEntryCo
11797
11898Polyfill banner 通过 Vite virtual module 机制作为 entry code 的静态前缀注入。Rolldown 将其与业务代码一起打包,但 polyfill 的副作用(在 ` globalThis ` 上设置属性)保证在所有其他模块加载前执行。
11999
120- ## 与 Lit SSR DOM Shim 的关系
100+ ## HTMLElement 自包含策略
121101
122- ` @lit-labs/ssr-dom-shim ` 提供 ` HTMLElement ` 的 shim 实现。我们 ** 继续使用它 ** 作为 HTMLElement polyfill 的来源,但将其从 output banner 移到 entry code 中。这样做的好处 :
102+ [ SOP-016 ] 将 HTMLElement polyfill 从 ` @lit-labs/ssr-dom-shim ` 迁移到 ` @lessjs/core/dsd-element.ts ` 自包含 :
123103
124- 1 . ** 统一 polyfill 位置 ** :所有 polyfill 在同一处管理
125- 2 . ** 去除 output banner ** :output banner 机制不保证在所有 Vite 版本间行为一致
126- 3 . ** 明确依赖关系 ** :entry code 中的 import 顺序天然保证执行顺序
104+ 1 . ** ` @lessjs/core ` 自包含 ** :拥有 ` DsdElement ` 的包必须自己提供 SSR-safe 的 HTMLElement 基类
105+ 2 . ** 最小 stub ** :` _SsrHTMLElementStub ` 只提供内部代码实际调用的 6 个方法
106+ 3 . ** 去除外部依赖 ** :不再依赖 ` @lit-labs/ssr-dom-shim ` 用于核心 SSR 功能
127107
128108## Consequences
129109
130110### Positive
131111
132- - ** 消除 customElements undefined 错误** :所有 WC 代码可在 SSR 环境安全加载
133- - ** 统一 polyfill 管理** :从 3 个分散位置(entry code、output banner、@lit-labs/ssr-dom-shim )合并为 1 个模块
134- - ** 明确执行顺序** :CSSStyleSheet → HTMLElement → customElements 的依赖链在 entry code 中可见
135- - ** 易于扩展** :新增浏览器 API polyfill 只需在 ` ssr-polyfills.ts ` 中添加
136- - ** 去除 output banner 依赖** :不再依赖 Rolldown 特定 API
112+ - ** 消除 customElements undefined 错误** :Map-backed polyfill 使 ` define() ` /` get() ` 真正工作
113+ - ** 消除 DSD 渲染空白** :HTMLElement stub 提供组件 render() 所需的最小 DOM 方法
114+ - ** ` @lessjs/core ` 自包含** :不再依赖 ` @lit-labs/ssr-dom-shim ` 用于核心 SSR
115+ - ** 最小表面** :stub 只包含 6 个成员,不模拟完整 DOM
137116
138117### Negative
139118
140- - ** customElements shim 是轻量级的 ** :不支持完整的 Custom Elements 生命周期( ` attributeChangedCallback ` 等),仅支持 ` define() ` / ` get() ` 注册
141- - ** 模块顶层副作用 ** :polyfill 在模块顶层修改 ` globalThis ` ,如果将来需要隔离 polyfill 作用域,需要重构
142- - ** polyfill 代码进入 bundle ** :~ 1KB gzip 的额外开销(可接受)
119+ - ** HTMLElement stub 是最小实现 ** :仅提供 SSR render() 内部使用的 6 个方法,不支持完整 DOM 行为。实际 DOM 操作由 happy-dom 在 ssgRender 阶段提供
120+ - ** customElements shim 轻量级 ** :不支持完整的 Custom Elements 生命周期( ` attributeChangedCallback ` 等),仅支持 ` define() ` / ` get() ` 注册
121+ - ** 模块顶层副作用 ** :polyfill 在模块顶层修改 ` globalThis `
143122
144123### Neutral
145124
0 commit comments