一、概念
在 Vue 的 SSR(服务端渲染)编译阶段中,v-model 指令的处理逻辑与客户端渲染存在显著差异。
客户端的 v-model 依赖运行时双向绑定机制,而 SSR 需要在编译时就生成静态字符串输出,因此必须提供一个对应的 SSR 版本 transform :ssrTransformModel。
它的任务是:
- 将
v-model指令在服务端编译阶段转化为适用于服务端的渲染表达式; - 自动为不同类型的表单控件(
input、textarea、select等)注入正确的 SSR 绑定逻辑; - 确保在模板被渲染成字符串时,
v-model的值反映在 DOM 属性中(例如选中状态或输入值)。
二、原理
ssrTransformModel 是一个 DirectiveTransform,即用于指令编译的转换函数。它通过匹配 v-model 指令节点,根据节点类型(HTML 元素类型)生成对应的 SSR 代码片段。
核心思路如下:
-
判断节点类型
- 普通元素(
input、textarea、select) → 服务端静态输出; - 组件节点(
<MyInput v-model="x" />) → 委托给transformModel()。
- 普通元素(
-
针对不同元素的处理逻辑
input[type=text]:输出value属性;input[type=radio]:根据v-model值是否匹配当前选项生成checked;input[type=checkbox]:根据数组/布尔判断生成checked;textarea:将内部内容替换为插值;select:递归处理所有option,为选中项添加selected属性。
-
错误与校验
-
检查不合法用法,如:
v-model与value同时存在;v-model用于<input type="file">;v-model用于非表单元素。
-
三、源码与逐行注释
1. 引入与类型声明
python
import {
DOMErrorCodes,
type DirectiveTransform,
ElementTypes,
type ExpressionNode,
NodeTypes,
type PlainElementNode,
type TemplateChildNode,
createCallExpression,
createConditionalExpression,
createDOMCompilerError,
createInterpolation,
createObjectProperty,
createSimpleExpression,
findProp,
hasDynamicKeyVBind,
transformModel,
} from '@vue/compiler-dom'
import {
SSR_INCLUDE_BOOLEAN_ATTR,
SSR_LOOSE_CONTAIN,
SSR_LOOSE_EQUAL,
SSR_RENDER_DYNAMIC_MODEL,
} from '../runtimeHelpers'
import type { DirectiveTransformResult } from 'packages/compiler-core/src/transform'
说明:
- 前半部分导入 AST 处理相关工具函数(如
createCallExpression、createConditionalExpression等)。- 后半部分导入 SSR 渲染辅助函数(如
SSR_LOOSE_EQUAL、SSR_RENDER_DYNAMIC_MODEL等),这些是 SSR 运行时在字符串拼接中使用的 helper。
2. 核心 transform 函数定义
javascript
export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
const model = dir.exp!
说明:
dir:表示v-model指令节点;node:当前绑定的元素节点;context:编译上下文,用于错误报告、helper 注册等;model:v-model的表达式节点(如foo.bar)。
3. 检查重复绑定 value
scss
function checkDuplicatedValue() {
const value = findProp(node, 'value')
if (value) {
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,
value.loc,
),
)
}
}
逻辑说明:
SSR 不需要再显式设置
value,因为v-model已自动生成它;若模板中多写一个value,会报错提示用户。
4. 递归处理 <select> 子节点
ini
const processSelectChildren = (children: TemplateChildNode[]) => {
children.forEach(child => {
if (child.type === NodeTypes.ELEMENT) {
processOption(child as PlainElementNode)
} else if (child.type === NodeTypes.FOR) {
processSelectChildren(child.children)
} else if (child.type === NodeTypes.IF) {
child.branches.forEach(b => processSelectChildren(b.children))
}
})
}
逻辑说明:
递归遍历
<select>的所有层级子节点,包括被v-for或v-if包裹的选项,确保每个<option>都能正确标注selected状态。
5. 处理 <option> 元素
scss
function processOption(plainNode: PlainElementNode) {
if (plainNode.tag === 'option') {
if (plainNode.props.findIndex(p => p.name === 'selected') === -1) {
const value = findValueBinding(plainNode)
plainNode.ssrCodegenNode!.elements.push(
createConditionalExpression(
createCallExpression(context.helper(SSR_INCLUDE_BOOLEAN_ATTR), [ createConditionalExpression( createCallExpression(`Array.isArray`, [model]),
createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [ model, value, ]),
createCallExpression(context.helper(SSR_LOOSE_EQUAL), [ model, value, ]),
),
]),
createSimpleExpression(' selected', true),
createSimpleExpression('', true),
false,
),
)
}
} else if (plainNode.tag === 'optgroup') {
processSelectChildren(plainNode.children)
}
}
逐步说明:
若
<option>没有显式selected,则判断它是否应被选中;判断逻辑:
- 如果
v-model是数组,则调用SSR_LOOSE_CONTAIN(model, value);- 否则用
SSR_LOOSE_EQUAL(model, value);若判断为真,则拼接字符串
" selected"到最终 SSR 输出。
6. 主逻辑分支(根据元素类型)
ini
if (node.tagType === ElementTypes.ELEMENT) {
const res: DirectiveTransformResult = { props: [] }
const defaultProps = [
createObjectProperty(`value`, model),
]
创建 transform 结果对象
res,用于存放编译后生成的 props(最终转为 SSR 属性字符串)。
6.1 输入框 <input>
ini
if (node.tag === 'input') {
const type = findProp(node, 'type')
if (type) {
const value = findValueBinding(node)
(1) 动态类型输入框
ini
if (type.type === NodeTypes.DIRECTIVE) {
res.ssrTagParts = [
createCallExpression(context.helper(SSR_RENDER_DYNAMIC_MODEL), [
type.exp!,
model,
value,
]),
]
用
SSR_RENDER_DYNAMIC_MODEL处理,例如:<input :type="inputType" v-model="x">。SSR 阶段会生成动态
type分支逻辑。
(2) 静态类型输入框
根据 type 值不同:
- radio
css
case 'radio':
res.props = [
createObjectProperty(
`checked`,
createCallExpression(context.helper(SSR_LOOSE_EQUAL), [model, value]),
),
]
- checkbox
css
case 'checkbox':
const trueValueBinding = findProp(node, 'true-value')
...
res.props = [
createObjectProperty(
`checked`,
createConditionalExpression(
createCallExpression(`Array.isArray`, [model]),
createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [model, value]),
model,
),
),
]
- file
python
case 'file':
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
dir.loc,
),
)
- default(text/password等)
ini
default:
checkDuplicatedValue()
res.props = defaultProps
6.2 文本域 <textarea>
ini
} else if (node.tag === 'textarea') {
checkDuplicatedValue()
node.children = [createInterpolation(model, model.loc)]
替换内部内容为插值表达式,使得 SSR 输出
<textarea>内容</textarea>。
6.3 下拉框 <select>
ini
} else if (node.tag === 'select') {
processSelectChildren(node.children)
调用前文定义的递归函数,为
<option>自动添加selected。
6.4 非法用法处理
python
} else {
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
dir.loc,
),
)
}
例如在
<div>或<span>上使用v-model。
6.5 返回结果
kotlin
return res
} else {
// component v-model
return transformModel(dir, node, context)
}
对组件节点交由通用的
transformModel处理。
7. 辅助函数:提取 value 绑定
javascript
function findValueBinding(node: PlainElementNode): ExpressionNode {
const valueBinding = findProp(node, 'value')
return valueBinding
? valueBinding.type === NodeTypes.DIRECTIVE
? valueBinding.exp!
: createSimpleExpression(valueBinding.value!.content, true)
: createSimpleExpression(`null`, false)
}
若节点有
v-bind:value或静态value属性,返回其表达式;否则返回一个空表达式null。
四、对比分析
| 类型 | 客户端编译 (transformModel) |
SSR 编译 (ssrTransformModel) |
|---|---|---|
| 输出形式 | 动态绑定 + 事件 | 静态字符串生成 |
| 目标 | 运行时 DOM 更新 | 初始 HTML 渲染 |
| 处理时机 | 运行时 | 编译时 |
| 表单类型支持 | 同步运行时 DOM | 预渲染选中与值 |
SSR 版本的核心区别是:它必须在没有 DOM 的环境下模拟"选中/输入"状态。
五、实践应用
假设模板如下:
ini
<select v-model="lang">
<option value="en">English</option>
<option value="jp">Japanese</option>
</select>
SSR 编译后会生成:
vbnet
<select>
<option value="en" selected>English</option>
<option value="jp">Japanese</option>
</select>
如果
lang === 'en',则selected会在服务器端直接输出。客户端 hydration 阶段无需再额外处理选中逻辑。
六、拓展:与 v-bind、v-if 的协同
在 SSR 编译中,v-model 与 v-bind、v-if、v-for 可交织存在。
ssrTransformModel 特意支持递归遍历 v-if 和 v-for 子节点,确保所有动态渲染的选项都被正确判断和输出。
七、潜在问题与设计思考
- 动态类型的复杂性
<input :type="inputType" v-model="x">在 SSR 中必须生成多分支渲染逻辑,否则无法确定checked/value的生成规则。 - 数组绑定的兼容性
通过Array.isArray()判断是否是多选绑定,这种动态检测在 SSR 环境中需谨慎处理性能开销。 - hydration 对齐问题
SSR 输出的初始状态必须与客户端初始data完全一致,否则会出现 mismatch 警告。
八、总结
ssrTransformModel 是 Vue SSR 编译体系中最关键的指令处理逻辑之一。
它在编译阶段将 v-model 的双向绑定语义转化为静态属性字符串,从而实现初始状态可还原、无运行时依赖的 HTML 输出。
这段代码充分体现了 Vue 在 编译期与运行期分层设计 的思想:
编译期静态生成 + 运行时动态绑定 = 高性能与高一致性并存。
本文部分内容借助 AI 辅助生成,并由作者整理审核。