Vue SSR 源码解析:ssrTransformModel 深度剖析

一、概念

在 Vue 的 SSR(服务端渲染)编译阶段中,v-model 指令的处理逻辑与客户端渲染存在显著差异。

客户端的 v-model 依赖运行时双向绑定机制,而 SSR 需要在编译时就生成静态字符串输出,因此必须提供一个对应的 SSR 版本 transformssrTransformModel

它的任务是:

  • v-model 指令在服务端编译阶段转化为适用于服务端的渲染表达式;
  • 自动为不同类型的表单控件(inputtextareaselect 等)注入正确的 SSR 绑定逻辑;
  • 确保在模板被渲染成字符串时,v-model 的值反映在 DOM 属性中(例如选中状态或输入值)。

二、原理

ssrTransformModel 是一个 DirectiveTransform,即用于指令编译的转换函数。它通过匹配 v-model 指令节点,根据节点类型(HTML 元素类型)生成对应的 SSR 代码片段。

核心思路如下:

  1. 判断节点类型

    • 普通元素(inputtextareaselect) → 服务端静态输出;
    • 组件节点(<MyInput v-model="x" />) → 委托给 transformModel()
  2. 针对不同元素的处理逻辑

    • input[type=text]:输出 value 属性;
    • input[type=radio]:根据 v-model 值是否匹配当前选项生成 checked
    • input[type=checkbox]:根据数组/布尔判断生成 checked
    • textarea:将内部内容替换为插值;
    • select:递归处理所有 option,为选中项添加 selected 属性。
  3. 错误与校验

    • 检查不合法用法,如:

      • v-modelvalue 同时存在;
      • 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 处理相关工具函数(如 createCallExpressioncreateConditionalExpression 等)。
  • 后半部分导入 SSR 渲染辅助函数(如 SSR_LOOSE_EQUALSSR_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 注册等;
  • modelv-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-forv-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)
    }
  }

逐步说明:

  1. <option> 没有显式 selected,则判断它是否应被选中;

  2. 判断逻辑:

    • 如果 v-model 是数组,则调用 SSR_LOOSE_CONTAIN(model, value)
    • 否则用 SSR_LOOSE_EQUAL(model, value)
  3. 若判断为真,则拼接字符串 " 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-bindv-if 的协同

在 SSR 编译中,v-modelv-bindv-ifv-for 可交织存在。
ssrTransformModel 特意支持递归遍历 v-ifv-for 子节点,确保所有动态渲染的选项都被正确判断和输出。


七、潜在问题与设计思考

  1. 动态类型的复杂性
    <input :type="inputType" v-model="x"> 在 SSR 中必须生成多分支渲染逻辑,否则无法确定 checked/value 的生成规则。
  2. 数组绑定的兼容性
    通过 Array.isArray() 判断是否是多选绑定,这种动态检测在 SSR 环境中需谨慎处理性能开销。
  3. hydration 对齐问题
    SSR 输出的初始状态必须与客户端初始 data 完全一致,否则会出现 mismatch 警告。

八、总结

ssrTransformModel 是 Vue SSR 编译体系中最关键的指令处理逻辑之一。

它在编译阶段将 v-model 的双向绑定语义转化为静态属性字符串,从而实现初始状态可还原、无运行时依赖的 HTML 输出

这段代码充分体现了 Vue 在 编译期与运行期分层设计 的思想:

编译期静态生成 + 运行时动态绑定 = 高性能与高一致性并存。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

相关推荐
恋猫de小郭20 分钟前
Flutter 状态管理基准测评,一个很有趣的观点
android·前端·flutter
雪芽蓝域zzs25 分钟前
前端编辑组件wangEditor
前端
风骏时光牛马1 小时前
稳定性治理及疑难问题深度剖析与实战复盘
前端
JeffongTan9 小时前
在LWC中镶嵌VF Page获取用户IP
前端·javascript·salesforce
陆枫Larry11 小时前
Astro 是什么
前端
是立不是利13 小时前
写给设计师的 CSS 博客美学指南
前端·css
默_笙14 小时前
🏝 Docker 就是"房地产开发":从施工图纸到小区物业的容器化指南
前端·javascript
计算机魔术师14 小时前
Rohan Paul 谈用时间跨度衡量智能体能力,并引用 OpenAI 自动化研究实习生里程碑
前端
kyriewen14 小时前
我手写了个极简版 React Router——才搞懂 v6 为什么砍了这么多 API
前端·javascript·程序员