Vue SSR 组件转换源码深度解析:ssrTransformComponent.ts
一、概念:Vue SSR 转换的目的
在 Vue 3 的编译体系中,模板编译会分为 普通渲染(client render) 与 服务器端渲染(SSR render) 两种模式。
客户端模式下生成的代码依赖 h()、createVNode() 等运行时函数,而 SSR 模式下的编译产物则需要能直接输出字符串片段,用于服务端生成 HTML。
ssrTransformComponent 模块正是用于在 编译阶段 处理 <Component /> 标签在 SSR 模式下的代码生成逻辑。
它的目标是让 Vue 组件在服务端编译时能生成对应的 _push(...) 片段,以供后续的 ssrRenderComponent() 调用。
二、原理:两阶段 SSR 转换机制
源码的注释开头明确指出:
// ssr component transform is done in two phases:
Vue 的 SSR 编译对组件分两步处理:
-
第一阶段(transform phase) :
- 利用
buildSlots分析组件的插槽结构; - 创建未完成的插槽函数(
WIPSlotEntry),并保存在WeakMap中; - 为组件生成 SSR Codegen Node 占位符。
- 利用
-
第二阶段(process phase) :
- 遍历第一阶段记录的
WIPSlotEntry; - 填充每个 slot 的函数体(即
_push输出或 VNode 分支); - 输出最终可执行的
_ssrRenderComponent(...)或_ssrRenderVNode(...)调用。
- 遍历第一阶段记录的
这两阶段分别对应源码中的:
-
ssrTransformComponent(转换器) -
ssrProcessComponent(处理器)
三、对比:SSR 与 Client 渲染差异
| 项目 | Client 渲染 | SSR 渲染 |
|---|---|---|
| 渲染目标 | VNode 树 | HTML 字符串 |
| 调用接口 |
createVNode() / renderComponentRoot()
|
_push() / ssrRenderComponent()
|
| 插槽处理 | renderSlot() |
_ssrRenderSlot() |
| 动态组件 | resolveDynamicComponent() |
ssrRenderVNode() |
| 转换阶段 | 单次编译 | 两阶段转换(分析 + 生成) |
由此可见,SSR 编译需要在编译期模拟组件行为,以提前生成完整的 HTML 输出逻辑。
四、实践:源码逐段讲解
4.1 数据结构与全局变量
const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>()
const componentTypeMap = new WeakMap<ComponentNode, string | symbol | CallExpression>()
-
wipMap:存储当前组件未完成的插槽函数信息; -
componentTypeMap:记录组件对应的类型(静态字符串、动态调用、内置组件符号)。
WIPSlotEntry 的定义如下:
interface WIPSlotEntry {
type: typeof WIP_SLOT
fn: FunctionExpression
children: TemplateChildNode[]
vnodeBranch: ReturnStatement
}
表示一个“正在构建中的”插槽函数,等待第二阶段补全。
4.2 SSR 转换主函数
export const ssrTransformComponent: NodeTransform = (node, context) => {
if (node.type !== NodeTypes.ELEMENT || node.tagType !== ElementTypes.COMPONENT) return
- 仅在节点为“组件类型元素”时触发。
- 通过
resolveComponentType()判断组件类型(静态、动态或内置组件)。
const component = resolveComponentType(node, context, true /* ssr */)
此时若检测到内置组件(如 <Suspense>、<Teleport>、<Transition>),会直接交由对应的专用转换函数处理。
4.3 构建 vnode fallback 分支
SSR 渲染中,部分插槽可能退回到普通 VNode 渲染。
因此,这里克隆当前节点,通过 buildSlots() 构建备用的 VNode 渲染分支:
const vnodeBranches: ReturnStatement[] = []
const clonedNode = clone(node)
buildSlots 会返回多个 slot,每个 slot 对应一个 ReturnStatement,记录备用渲染逻辑。
4.4 构建 SSR 属性与插槽函数
const { props, directives } = buildProps(node, context, undefined, true, isDynamicComponent)
const propsExp = buildSSRProps(props, directives, context)
- SSR 模式下的属性生成需过滤掉无关事件;
-
buildSSRProps()会转换属性为字符串拼接形式。
接着定义一个 SlotFnBuilder:
const buildSSRSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) => {
const fn = createFunctionExpression([...], undefined, true, true, loc)
wipEntries.push({ fn, children, vnodeBranch })
return fn
}
这一步只是构造函数签名(参数为 _push, _parent, _scopeId 等),函数体在第二阶段填充。
4.5 生成最终 SSR 调用节点
静态组件使用:
node.ssrCodegenNode = createCallExpression(
context.helper(SSR_RENDER_COMPONENT),
[component, propsExp, slots, `_parent`]
)
动态组件(通过 resolveDynamicComponent)使用:
node.ssrCodegenNode = createCallExpression(
context.helper(SSR_RENDER_VNODE),
[
`_push`,
createCallExpression(context.helper(CREATE_VNODE), [component, propsExp, slots]),
`_parent`
]
)
区别在于 SSR 是否需要额外创建 VNode 实例来决定渲染逻辑。
4.6 第二阶段:ssrProcessComponent
此函数在第二轮遍历中被调用,用于真正生成可执行代码。
填充 Slot 函数体
fn.body = createIfStatement(
createSimpleExpression(`_push`, false),
processChildrenAsStatement(wipEntries[i], context, false, true),
vnodeBranch
)
即生成如下伪代码:
if (_push) {
// SSR 输出逻辑
} else {
// VNode fallback 逻辑
}
最终输出
若组件为静态:
context.pushStatement(createCallExpression(`_push`, [node.ssrCodegenNode]))
若动态组件:
context.pushStatement(node.ssrCodegenNode)
4.7 子转换与辅助函数
createVNodeSlotBranch() 与 subTransform() 实现了子上下文的 VNode 模式编译,以保证插槽作用域正确继承。
而 clone() 则为深度克隆 AST 节点的递归实现:
function clone(v: any): any {
if (isArray(v)) return v.map(clone)
else if (isPlainObject(v)) { ... }
else return v
}
五、拓展:可结合 SSR 插槽的运行机制理解
-
ssrRenderSlot()在运行时调用这些生成的函数; -
_push()代表向最终 HTML 字符串流写入; - SSR 插槽的作用域与客户端一致,但行为不同:SSR 中是同步展开,Client 中是延迟渲染。
六、潜在问题与优化方向
-
WeakMap 引用的生命周期问题
若编译器复用上下文过久,wipMap未清理可能导致内存占用上升。 -
vnodeFallback 的冗余生成
在多数情况下,SSR 插槽不会回退至 VNode 模式,可优化以按需生成。 -
Dynamic Component 的边界场景
SSR 渲染时,若动态组件解析失败(如外部依赖未加载),ssrRenderVNode可能输出空标签。
七、总结
本文分析了 Vue SSR 组件转换源码的核心逻辑,完整展示了:
- SSR 编译的双阶段设计;
- 插槽函数的延迟补全机制;
- 静态与动态组件的差异处理;
- 子上下文继承与 VNode fallback 机制。
这份源码展示了 Vue 3 编译体系在“声明式模板 → 渲染指令代码”间的严谨抽象,是理解 Vue SSR 的核心部分。
本文部分内容借助 AI 辅助生成,并由作者整理审核。