一、背景与概念
在 Vue 3 的服务端渲染(SSR)体系中,模板编译器的职责是将 .vue 模板转化为服务端可执行的渲染函数代码。与客户端渲染(CSR)不同,SSR 输出的不是虚拟 DOM,而是完整 HTML 字符串,因此编译产物的结构和生成逻辑完全不同。
这篇文章将分析 Vue 源码中 ssrCodegenTransform.ts 文件的实现,它是SSR 编译阶段的核心转换器,主要负责:
- 将模板 AST 转换成 JS AST;
- 插入 SSR 特定的 helper 调用;
- 组织字符串拼接逻辑;
- 管理作用域与 CSS 变量。
二、核心函数:ssrCodegenTransform
📜 源码片段
javascript
export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions): void {
const context = createSSRTransformContext(ast, options)
if (options.ssrCssVars) {
const cssContext = createTransformContext(createRoot([]), options)
const varsExp = processExpression(
createSimpleExpression(options.ssrCssVars, false),
cssContext,
)
context.body.push(
createCompoundExpression([`const _cssVars = { style: `, varsExp, `}`]),
)
Array.from(cssContext.helpers.keys()).forEach(helper => {
ast.helpers.add(helper)
})
}
const isFragment =
ast.children.length > 1 && ast.children.some(c => !isText(c))
processChildren(ast, context, isFragment)
ast.codegenNode = createBlockStatement(context.body)
ast.ssrHelpers = Array.from(
new Set([
...Array.from(ast.helpers).filter(h => h in ssrHelpers),
...context.helpers,
]),
)
ast.helpers = new Set(Array.from(ast.helpers).filter(h => !(h in ssrHelpers)))
}
🧩 逐行解析
-
创建上下文:
iniconst context = createSSRTransformContext(ast, options)创建一个 SSR 专用的转换上下文,用于存储编译状态(包括 helper、body、错误回调等)。
-
注入 CSS 变量:
scssif (options.ssrCssVars) { ... }当模板中使用了 SFC
<style>中定义的 CSS 变量时,需要生成_cssVars常量,确保 SSR 渲染时能正确解析。 -
判断是否为 Fragment:
iniconst isFragment = ast.children.length > 1 && ast.children.some(c => !isText(c))若根节点包含多个子节点(或非纯文本节点),需将其包裹为
<!--[-->与<!--]-->片段标记。 -
核心递归处理:
scssprocessChildren(ast, context, isFragment)将 AST 中的所有子节点转换为 JS 表达式或字符串片段。
-
生成最终代码块:
iniast.codegenNode = createBlockStatement(context.body)以 BlockStatement(JS 语法树节点)形式输出整个 SSR 渲染函数体。
-
区分 SSR 与 Vue 内置 helper:
iniast.ssrHelpers = ...将属于 SSR 渲染器的 helper(如
_push,_interpolate)从普通 Vue helper 中分离,供@vue/server-renderer使用。
三、上下文系统:createSSRTransformContext
📜 源码片段
typescript
function createSSRTransformContext(
root: RootNode,
options: CompilerOptions,
helpers: Set<symbol> = new Set(),
withSlotScopeId = false,
): SSRTransformContext {
const body: BlockStatement['body'] = []
let currentString: TemplateLiteral | null = null
return {
root,
options,
body,
helpers,
withSlotScopeId,
onError: options.onError || (e => { throw e }),
helper<T extends symbol>(name: T): T {
helpers.add(name)
return name
},
pushStringPart(part) {
if (!currentString) {
const currentCall = createCallExpression(`_push`)
body.push(currentCall)
currentString = createTemplateLiteral([])
currentCall.arguments.push(currentString)
}
const bufferedElements = currentString.elements
const lastItem = bufferedElements[bufferedElements.length - 1]
if (isString(part) && isString(lastItem)) {
bufferedElements[bufferedElements.length - 1] += part
} else {
bufferedElements.push(part)
}
},
pushStatement(statement) {
currentString = null
body.push(statement)
},
}
}
🧠 原理讲解
SSRTransformContext 是整个编译流程的"状态容器"。
它的职责相当于一个"编译游标":
- 跟踪生成的语句(
body); - 合并字符串模板;
- 注册需要导入的 helper;
- 捕获编译错误。
⚙️ 字符串缓冲机制
SSR 渲染主要是拼接字符串,因此引入了 pushStringPart() 方法,将所有连续的字符串合并在一个模板字面量(TemplateLiteral)中,减少 _push 调用开销。
四、核心节点遍历:processChildren
📜 源码片段
javascript
export function processChildren(
parent: Container,
context: SSRTransformContext,
asFragment = false,
disableNestedFragments = false,
disableComment = false,
): void {
if (asFragment) context.pushStringPart(`<!--[-->`)
const { children } = parent
for (let i = 0; i < children.length; i++) {
const child = children[i]
switch (child.type) {
case NodeTypes.ELEMENT:
switch (child.tagType) {
case ElementTypes.ELEMENT:
ssrProcessElement(child, context)
break
case ElementTypes.COMPONENT:
ssrProcessComponent(child, context, parent)
break
case ElementTypes.SLOT:
ssrProcessSlotOutlet(child, context)
break
}
break
case NodeTypes.TEXT:
context.pushStringPart(escapeHtml(child.content))
break
case NodeTypes.INTERPOLATION:
context.pushStringPart(
createCallExpression(context.helper(SSR_INTERPOLATE), [child.content]),
)
break
case NodeTypes.IF:
ssrProcessIf(child, context, disableNestedFragments, disableComment)
break
case NodeTypes.FOR:
ssrProcessFor(child, context, disableNestedFragments)
break
case NodeTypes.COMMENT:
if (!disableComment) {
context.pushStringPart(`<!--${child.content}-->`)
}
break
}
}
if (asFragment) context.pushStringPart(`<!--]-->`)
}
🧩 核心逻辑分解
-
Fragment 包裹:
使用注释节点标记多节点片段,便于客户端 hydration 对齐。
-
节点类型分派:
- 元素节点 :委托给
ssrProcessElement()。 - 组件节点 :调用
ssrProcessComponent()。 - 插槽节点 :使用
ssrProcessSlotOutlet()。 - 文本节点:HTML 转义后直接拼接。
- 插值表达式 :调用
_interpolatehelper。 - 条件 / 循环节点 :分别交由
ssrProcessIf与ssrProcessFor处理。
- 元素节点 :委托给
-
注释节点:
默认输出到最终 HTML 中,但可以通过
disableComment抑制。
五、子上下文机制:createChildContext
scss
function createChildContext(
parent: SSRTransformContext,
withSlotScopeId = parent.withSlotScopeId,
): SSRTransformContext {
return createSSRTransformContext(
parent.root,
parent.options,
parent.helpers,
withSlotScopeId,
)
}
每个子作用域(如插槽或循环体)都会派生一个新的 context,但共享相同的 helpers 集合。
这样既能独立管理作用域,又能复用 helper 注册,保证生成代码一致。
六、实践与拓展
✅ 实践意义
理解 ssrCodegenTransform 有助于:
- 编写自定义 SSR 指令;
- 调试模板编译输出;
- 改进服务端渲染性能;
- 探索
@vue/compiler-ssr的扩展。
🧩 拓展方向
- 字符串合并优化:可尝试 AST 层级的批量合并;
- 流式 SSR :在
_push调用处加入流写入逻辑; - 定制 helper 注入:通过 context.helper() 动态扩展 SSR runtime。
⚠️ 潜在问题
- CSS 变量注入只在顶层执行,若嵌套作用域需手动传递;
- 片段注释标记若被破坏,会导致客户端 hydration 错位;
- 对于复杂组件(如异步组件、Teleport)还需额外的 transform 支持。
七、总结
ssrCodegenTransform 是 Vue 3 SSR 编译管线的关键环节,它将模板 AST 转化为服务端渲染可执行的 JS AST。其设计充分体现了 分层抽象 + 上下文状态隔离 + 字符串缓冲优化 的思想。
理解此模块能让开发者深入掌握 Vue SSR 的底层工作原理,也为二次开发和性能优化提供了坚实基础。
本文部分内容借助 AI 辅助生成,并由作者整理审核。