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 辅助生成,并由作者整理审核。

相关推荐
excel2 小时前
Vue SSR 运行时辅助工具注册机制源码详解
前端
excel2 小时前
Vue SSR 源码解析:ssrProcessIf 条件渲染的服务端转换逻辑
前端
excel2 小时前
深度解析:Vue 3 中 ssrTransformTransitionGroup 的实现原理与机制
前端
晚秋大魔王2 小时前
基于python的jlink单片机自动化批量烧录工具
前端·python·单片机
星尘库2 小时前
抖音自动化-实现给特定用户发私信
前端·javascript·自动化
excel2 小时前
深入理解 Vue SSR 中的 v-for 编译逻辑:ssrProcessFor 源码解析
前端
excel2 小时前
Vue SSR 编译器核心逻辑解析:ssrInjectFallthroughAttrs
前端
excel2 小时前
深度解析:Vue SSR 编译器中的 ssrTransformElement 与 ssrProcessElement
前端
excel2 小时前
Vue SSR 源码解读:ssrTransformTransition 与 ssrProcessTransition 的实现逻辑
前端