0005: WithDsdHydration Mixin
Self-built SSR + WithDsdHydration Mixin to bridge DSD hydration gaps without framework-native SSR.
Adopted
Source: docs/decisions/0005
<h1>WithDsdHydration Mixin + 自建 SSR 决策</h1>
<h2>Status: ✅ <strong>ADOPTED</strong> — v0.6.2 架构决策</h2>
<h2>上下文</h2>
<p>LessJS 使用 Lit 作为 UI 层框架,但 SSR 渲染器(<code>renderDSD()</code>)是自建的,不使用 <code>@lit-labs/ssr</code>。这导致 DSD 预渲染的组件存在水合缺口:框架的模板系统没有参与 SSR,所以 <code>@click</code> 事件绑定和响应式更新都不生效。</p>
<p>我们需要回答两个问题:</p>
<ol><li>为什么不用 Lit/FAST 的原生 SSR?</li><li>如何弥补自建 SSR 造成的水合缺口?</li></ol>
<h2>决策</h2>
<h3>1. 自建 SSR,不用框架原生 SSR</h3>
<table><thead><tr><th>原因</th><th>Lit SSR (<code>@lit-labs/ssr</code>)</th><th>FAST SSR</th><th>LessJS <code>renderDSD()</code></th></tr></thead><tbody><tr><td>Deno/Edge 可运行</td><td>❌ 依赖 <code>node:stream</code>、<code>node:buffer</code></td><td>❌ 同上</td><td>✅ 纯字符串拼接</td></tr><tr><td>输出标准 DSD</td><td>❌ 私有序列化格式,需 <code>@lit-labs/ssr-client</code> 解析</td><td>❌ 同上</td><td>✅ WHATWG 标准 <code><template shadowrootmode="open"></code></td></tr><tr><td>框架无关</td><td>❌ 绑定 Lit</td><td>❌ 绑定 FAST</td><td>✅ Core 零框架依赖</td></tr></tbody></table>
<p><strong>权重分析:</strong></p>
<ul><li><strong>Deno/Edge 可运行</strong> — 硬性约束。LessJS 目标运行时是 Deno + Edge Functions,<code>@lit-labs/ssr</code> 的 Node.js 依赖使其完全无法使用。单这一条就足够。</li><li><strong>标准 DSD</strong> — 用户体验约束。WHATWG 标准 DSD 在 HTML 解析时即挂载 shadow root,零 JS、零闪烁。Lit SSR 的私有序列化格式需要等 JS 加载完才能恢复。</li><li><strong>框架无关</strong> — 架构约束。LessJS Core 的核心承诺是 framework-agnostic,绑死某个框架的 SSR 会让这个承诺名存实亡。</li></ul>
<h3>2. 用 <code>WithDsdHydration</code> Mixin 弥补水合缺口</h3>
<p>将 <code>LitDsdElement</code> 中 <strong>80% 框架无关的逻辑</strong> 提取到 <code>@lessjs/core</code> 的通用 Mixin:</p>
<pre><code>@lessjs/core
└── WithDsdHydration<T extends HTMLElement>(Base: T): T
// DSD 检测(shadow root 已有内容?)
// _hydrateEvents() — HydrateEventDescriptor → addEventListener
// updateDsdElement() — querySelectorAll + 回调
// AbortController 自动清理
@lessjs/adapter-lit
└── LitDsdElement = WithDsdHydration(LitElement)
// render() → nothing(Lit 模板绕过)
// createRenderRoot() 检测已有 shadow root
@lessjs/adapter-fast (未来)
└── FastDsdElement = WithDsdHydration(FASTElement)
// FAST 模板绕过策略</code></pre>
<p><strong>为什么是 Mixin 而不是基类:</strong></p>
<ul><li>Mixin 接受任意基类(<code>LitElement</code>、<code>FASTElement</code>、<code>HTMLElement</code>),返回增强后的类</li><li>不需要组件继承特定基类,不与框架继承链冲突</li><li><code>@lessjs/ui</code> 不需要依赖 <code>@lessjs/adapter-lit</code>,消除了循环依赖风险</li><li>符合 LessJS "framework-agnostic core + pluggable adapters" 的架构理念</li></ul>
<p><strong>Mixin 中框架无关 vs 框架特定的边界:</strong></p>
<table><thead><tr><th>能力</th><th>通用?</th><th>依赖</th></tr></thead><tbody><tr><td>DSD 检测</td><td>✅</td><td>标准 DOM API</td></tr><tr><td>事件绑定 (<code>_hydrateEvents</code>)</td><td>✅</td><td>标准 DOM API</td></tr><tr><td>DOM 更新 (<code>updateDsdElement</code>)</td><td>✅</td><td>标准 DOM API</td></tr><tr><td>清理 (<code>AbortController</code>)</td><td>✅</td><td>标准 DOM API</td></tr><tr><td>渲染绕过</td><td>❌</td><td>每个框架不同(Lit→<code>nothing</code>, FAST→自己的方式, vanilla→不需要)</td></tr><tr><td>响应式集成</td><td>❌</td><td>每个框架不同(Lit <code>requestUpdate</code>, FAST <code>Observable</code>)</td></tr></tbody></table>
<h3>3. Stencil 不纳入 Mixin 体系</h3>
<p>Stencil 是编译器而非运行时库,它把 TSX 编译成标准 Custom Element。适配 Stencil 需要<strong>编译器插件</strong>或<strong>后处理 transform</strong>,跟运行时 Mixin 不是同一路径。当前不纳入规划。</p>
<h2>三层组件模型</h2>
<p>Mixin 只服务于 Layer 2(DSD Interactive)。三层各有不同的水合策略:</p>
<table><thead><tr><th>层级</th><th>名称</th><th>DSD</th><th>水合</th><th>适用场景</th></tr></thead><tbody><tr><td>Layer 1</td><td>DSD Static</td><td>✅</td><td>无需</td><td>纯展示</td></tr><tr><td>Layer 2</td><td>DSD Interactive</td><td>✅</td><td><code>WithDsdHydration</code> Mixin</td><td>需要首屏+交互</td></tr><tr><td>Layer 3</td><td>Pure Island</td><td>❌</td><td>框架原生</td><td>需要完整响应式</td></tr></tbody></table>
<p>三体困境(自有 SSR + 框架响应式 + DSD 无闪烁)在<strong>组件级别</strong>消解——不是所有组件都需要 DSD。</p>
<h2>后果</h2>
<p><strong>正面:</strong></p>
<ul><li>Core 保持 framework-agnostic,UI 组件不再依赖特定 adapter</li><li>适配新框架只需写一个薄壳(<code>WithDsdHydration(NewFrameworkElement)</code>)</li><li>手动水合的代价被控制在 <code>static hydrateEvents</code> 声明级别</li><li>标准 DSD 确保首屏零闪烁、零 JS</li></ul>
<p><strong>负面:</strong></p>
<ul><li>Layer 2 组件仍需手动声明 <code>hydrateEvents</code>(编译时无类型检查)</li><li>Layer 2 组件的 DOM 更新仍需手动 <code>updateDsdElement()</code>(无框架响应式)</li><li>这些局限需要等待 Lit 原生 hydration 或 <code>.less</code> 编译器才能根治</li></ul>
<p><strong>演进路径:</strong></p>
<ol><li><strong>v0.6.2</strong>:<code>WithDsdHydration</code> Mixin 提取到 Core,组件使用 <code>extends WithDsdHydration(LitElement)</code></li><li><strong>中期</strong>:<code>@lit-labs/ssr</code> hydration 模块成熟后,Lit 原生 diff 已有 DOM,Mixin 的水合逻辑自然废弃</li><li><strong>远期</strong>:<code>.less</code> 编译器输出 SSR HTML + 客户端水合代码,人类不再写水合代码</li></ol>
<h2>参考</h2>
<ul><li><a href="./0002-less-compiler-eliminate-lit.md">ADR 0002: .less Compiler</a> — 远期方向</li><li><a href="https://html.spec.whatwg.org/multipage/scripting.html">WHATWG HTML §13.4: Declarative Shadow DOM</a></li><li><a href="https://github.com/nicolo-ribaudo/lit/tree/main/packages/labs/ssr">@lit-labs/ssr</a> — Lit 官方 SSR(Node.js 绑定)</li></ul>
<hr>
<p>_决策日期: 2026-05-07 | 版本: v0.6.2_</p>