在 Vue 3 的服务端渲染(SSR)编译体系中,TransitionGroup 是一个非常特殊的组件。
它既是一个过渡容器,又需要在服务端生成结构化 HTML,并在客户端保持可 hydration 的一致性。
为了实现这一目标,Vue 编译器对它进行了专门的两阶段处理------
也就是本文要深入解析的 ssrTransformTransitionGroup 与 ssrProcessTransitionGroup。
一、背景与整体概念
Vue 的 SSR 编译过程与普通编译(即 DOM 渲染编译)不同。
在 SSR 模式下,模板需要被转换为可直接输出 HTML 字符串的渲染函数。
对于一般组件来说,SSR 编译器会自动将 props、children 等信息编译为字符串。
但对于 <TransitionGroup> 这种动态包装容器组件,则必须特殊处理:
- 它允许开发者通过
tag或:tag自定义包裹元素; - 它会在运行时将子节点展平(flatten) ;
- 它会在运行时过滤注释节点;
- 同时还要保证 SSR 与 Hydration 阶段 DOM 结构对齐。
于是 Vue 内部引入了一个双阶段设计:
Phase 1:构建阶段(Transform)
Phase 2:输出阶段(Process)
这就是本文件实现的核心逻辑。
二、源码全貌
以下是 Vue 源码中的关键实现(简化版):
javascript
export function ssrTransformTransitionGroup(node, context) {
return (): void => {
const tag = findProp(node, 'tag')
if (tag) {
const otherProps = node.props.filter(p => p !== tag)
const { props, directives } = buildProps(node, context, otherProps, true, false, true)
let propsExp = null
if (props || directives.length) {
propsExp = createCallExpression(context.helper(SSR_RENDER_ATTRS), [
buildSSRProps(props, directives, context),
])
}
wipMap.set(node, {
tag,
propsExp,
scopeId: context.scopeId || null,
})
}
}
}
export function ssrProcessTransitionGroup(node, context) {
const entry = wipMap.get(node)
if (entry) {
const { tag, propsExp, scopeId } = entry
if (tag.type === NodeTypes.DIRECTIVE) {
// 动态 tag
context.pushStringPart(`<`)
context.pushStringPart(tag.exp!)
if (propsExp) context.pushStringPart(propsExp)
if (scopeId) context.pushStringPart(` ${scopeId}`)
context.pushStringPart(`>`)
processChildren(node, context, false, true, true)
context.pushStringPart(`</`)
context.pushStringPart(tag.exp!)
context.pushStringPart(`>`)
} else {
// 静态 tag
context.pushStringPart(`<${tag.value!.content}`)
if (propsExp) context.pushStringPart(propsExp)
if (scopeId) context.pushStringPart(` ${scopeId}`)
context.pushStringPart(`>`)
processChildren(node, context, false, true, true)
context.pushStringPart(`</${tag.value!.content}>`)
}
} else {
// 无 tag 情况
processChildren(node, context, true, true, true)
}
}
三、原理剖析:SSR 编译的两阶段逻辑
1. Transform 阶段(ssrTransformTransitionGroup)
在 AST 转换阶段,Vue 编译器会遍历模板中的每一个节点。
对于 <TransitionGroup>,这一步主要做的是信息提取:
核心流程
javascript
const tag = findProp(node, 'tag') // 找出 <TransitionGroup tag="ul"> 的 tag 属性
const otherProps = node.props.filter(p => p !== tag) // 过滤掉 tag
const { props, directives } = buildProps(...) // 构建 SSR props
逻辑说明
-
findProp:查找<TransitionGroup>上的 tag 属性; -
buildProps:构建 SSR 需要的 props;- 这里传入的
true /* ssr (skip event listeners) */表示跳过事件监听,因为 SSR 不绑定事件;
- 这里传入的
-
createCallExpression:生成_ssrRenderAttrs()调用表达式,用于在后续拼接字符串时插入属性字符串; -
最终结果存入一个
WeakMap缓存,以便第二阶段使用。
生成的中间数据结构
yaml
{
tag: AttributeNode | DirectiveNode,
propsExp: CallExpression | null,
scopeId: string | null
}
2. Process 阶段(ssrProcessTransitionGroup)
这一阶段真正执行"渲染字符串输出"。
动态标签(:tag="expr")逻辑
scss
if (tag.type === NodeTypes.DIRECTIVE) {
context.pushStringPart(`<`)
context.pushStringPart(tag.exp!)
if (propsExp) context.pushStringPart(propsExp)
if (scopeId) context.pushStringPart(` ${scopeId}`)
context.pushStringPart(`>`)
processChildren(node, context, false, true, true)
context.pushStringPart(`</`)
context.pushStringPart(tag.exp!)
context.pushStringPart(`>`)
}
在这里,
tag.exp!是一个动态表达式(例如"listTag"),SSR 输出会变成类似:
bash<${listTag} ...attrs>...</${listTag}>
静态标签(tag="ul")逻辑
javascript
context.pushStringPart(`<${tag.value!.content}`)
if (propsExp) context.pushStringPart(propsExp)
if (scopeId) context.pushStringPart(` ${scopeId}`)
context.pushStringPart(`>`)
processChildren(node, context, false, true, true)
context.pushStringPart(`</${tag.value!.content}>`)
输出类似:
ini<ul class="fade-list"> ... </ul>
特殊参数说明
processChildren(node, context, false, true, true) 的最后两个布尔参数非常关键:
flattenFragments = true:表示将所有子节点展开为单层 fragment;ignoreComments = true:TransitionGroup 会在运行时过滤掉注释节点,因此 SSR 也必须同步。
四、对比与特性分析
| 特性项 | 普通组件 | Transition | TransitionGroup |
|---|---|---|---|
| 输出结构 | 固定 DOM 或 Fragment | 包裹子元素 | 动态 tag 可配置 |
| Props 处理 | 正常 | 正常 | 跳过事件监听 |
| 子节点渲染 | 保持层级 | 单层 | 强制展平 (flatten) |
| 注释节点 | 保留 | 保留 | 过滤掉注释节点 |
| SSR 输出 | 静态 | 动态包裹 | 动态拼接标签结构 |
这一表格揭示了为什么 Vue 需要专门的 SSR transform 逻辑来处理它:
TransitionGroup 同时具备"容器"与"动态结构"的特性。
五、拓展理解:SSR Transform 的模式化设计
Vue 的 SSR Transform 系统普遍遵循一个模板化的模式:
| 阶段 | 函数命名 | 作用 |
|---|---|---|
| Transform 阶段 | ssrTransformXxx |
只负责收集静态信息,记录到 WeakMap |
| Process 阶段 | ssrProcessXxx |
读取缓存信息,生成最终字符串输出 |
同类文件包括:
ssrTransformComponentssrTransformTeleportssrTransformSlotOutlet
这种设计具有以下优势:
- 逻辑解耦:Transform 与输出生成分离;
- 性能优化:避免重复属性分析;
- 递归安全:支持嵌套结构(如 TransitionGroup 内再嵌套组件);
- Hydration 一致性:保证 SSR 输出与客户端渲染结构对齐。
六、潜在问题与实现注意事项
-
WeakMap 生命周期
编译阶段缓存仅存于内存,若存在并行编译(如 Vite 多线程 SSR),需避免交叉访问。
-
属性拼接安全性
由于 SSR 拼接字符串直接输出 HTML,必须保证
tag.exp安全,防止 XSS 注入。Vue 内部对该表达式有 AST 级验证。 -
作用域 ID 拼接细节
注意
context.pushStringPart(${scopeId})前的空格,若省略,可能导致属性粘连错误,如:
ini<ulclass="fade"> // ❌ 错误输出 -
无 tag 情况
若开发者未指定 tag,Vue 会退回 fragment 模式(即不包裹任何标签),
此时调用
processChildren(..., true, true, true)。
七、总结与启示
ssrTransformTransitionGroup 是 Vue 3 SSR 架构中一个小而精妙的片段。
它的实现体现出 Vue 对编译阶段与运行时一致性的极高要求:
- 利用 两阶段编译策略 解决动态结构问题;
- 在 AST Transform 层 提前抽取信息,避免生成期重复运算;
- 通过 processChildren 的参数控制,完美模拟了运行时行为。
这种精细化设计让 Vue 的 SSR 输出既高效又精准,保证了前后端渲染结构一致性。
结尾说明:
本文部分内容借助 AI 辅助生成,并由作者整理审核。