Jw
首页
技术
英语
首页
技术
英语
  • 01-权衡的艺术

    • 1.1 命令式与声明式
    • 1.2 性能与可维护性的权衡
    • 1.3 虚拟DOM的性能到底如何
    • 1.4 运行时和编译时
  • 02-框架设计的核心要素

    • 2.1 提升用户的开发体验
    • 2.2 控制框架代码的体积
    • 2.3 框架要做到良好的 Tree-Shaking
    • 2.4 框架应该输出怎样的构建产物
    • 2.5 特性开关
    • 2.6 错误处理
    • 2.7 良好的 TypeScript 类型支持
  • 03-Vue.js 3 的设计思路

    • 3.1 声明式地描述UI
    • 3.2 初识渲染器
    • 3.3 组件的本质
    • 3.4 模板的工作原理
    • 3.5 Vue.js 是各个模块组成的有机整体
  • 04-响应系统的作用与实现

    • 4.1 响应式数据与副作用函数
    • 4.2 响应式数据的基本实现
    • 4.3 设计一个完善的响应系统
    • 4.4 分支切换与 cleanup
    • 4.5 嵌套的effect与effect栈
    • 4.6 避免无限递归循环
    • 4.7 调度执行
    • 4.8 计算属性 computed 与 lazy
    • 4.9 watch 的实现原理
    • 4.10 立即执行的 watch 与回调执行时机
    • 4.11 过期的副作用
  • 05-非原始值的响应式方案

    • 5.1 理解 Proxy 和 Reflect
    • 5.2 JavaScript 对象及 Proxy 的工作原理
    • 5.3 如何代理 Object
    • 5.4 合理地触发响应
    • 5.5 浅响应与深响应
    • 5.6 只读和浅只读
    • 5.7 代理数组
    • 5.7.1 数组的索引与 length
    • 5.7.2 遍历数组
    • 5.7.3 数组的查找方法
    • 5.7.4 隐式修改数组长度的原型方法
    • 5.8 代理 Set 和 Map
    • 5.8.1 如何代理Set和Map
    • 5.8.2 建立响应联系
    • 5.8.3 避免污染原始数据
    • 5.8.4 处理forEach
    • 5.8.5 迭代器方法
    • 5.8.6 values与keys方法
  • 06-原始值的响应式方案

    • 6.1 引入 ref 的概念
    • 6.2 响应丢失问题
    • 6.3 自动脱ref
  • 07-渲染器的设计

    • 7.1 渲染器与响应系统的结合
    • 7.2 渲染器的基本概念
    • 7.3 自定义渲染器
  • 08-挂载与更新

    • 8.1 挂载子节点和元素的属性
    • 8.2 HTML Attributes 与 DOM Properties
    • 8.3 正确地设置元素属性
    • 8.4 class的处理
    • 8.5 卸载操作
    • 8.6 区分vnode类型
    • 8.7 事件的处理
    • 8.8 事件冒泡与更新时机问题
    • 8.9 更新子节点
    • 8.10 文本节点和注释节点
    • 8.11 Fragment
  • 09-简单 Diff 算法

    • 9.1 减少 DOM 操作的性能开销
    • 9.2 DOM 复用与 key 的作用
    • 9.3 找到需要移动的元素
    • 9.4 如何移动元素
    • 9.5 添加新元素
    • 9.6 移除不存在的元素
  • 10-双端 Diff 算法

    • 10.1 双端比较的原理
    • 10.3 非理想状况的处理方式
    • 10.4 添加新元素
    • 10.5 移除不存在的元素
  • 11-快速 Diff 算法

    • 11.1 相同的前置元素和后置元素
    • 11.2 判断是否需要进行 DOM 移动操作
    • 11.3 如何移动元素
  • 12-组件的实现原理

    • 12.1 渲染组件
    • 12.2 组件状态与自更新
    • 12.3 组件实例与组件的生命周期
    • 12.4 props与组件的被动更新
    • 12.5 setup 函数的作用与实现
    • 12.6 组件事件与 emit 的实现
    • 12.7 插槽的工作原理与实现
    • 12.8 注册生命周期
  • 13-异步组件与函数式组件

    • 13.1 异步组件要解决的问题
    • 13.2.1 封装defineAsyncComponent函数
    • 13.2.2 超时与 Error 组件
    • 13.2.3 延迟与 Loading 组件
    • 13.2.4 重试机制
    • 13.3 函数式组件
  • 14-内建组件和模块

    • 14.1.1 组件的激活与失活
    • 14.1.2 include 和 exclude
    • 14.1.3 缓存管理
    • 14.2.1 Teleport 组件要解决的问题
    • 14.2.2 实现 Teleport 组件
    • 14.3.1 原生 DOM 的过渡
    • 14.3.2 实现 Transition 组件
  • 15-编译器核心技术概览

    • 15.1 模板 DSL 的编译器
    • 15.2 parser 的实现原理与状态机
    • 15.3 构造AST
    • 15.4 AST 的转换与插件化架构
    • 15.4.1 节点的访问
    • 15.4.2 转换上下文与节点操作
    • 15.4.3 进入与退出
    • 15.5 将模板 AST 转为 JavaScript AST
    • 15.6 代码生成
  • 16-解析器

    • 16.1 文本模式及其对解析器的影响
    • 16.2 递归下降算法构造模板 AST
    • 16.3 状态机的开启与停止
    • 16.4 解析标签节点
    • 16.5 解析属性
    • 16.6.1 解析文本
    • 16.6.2 解码命名字符引用
    • 16.6.3 解码数字字符引用
    • 16.7 解析插值与注释
  • 17-编译优化

    • 17.1.1 传统Diff算法问题
    • 17.1.2 Block 与 PatchFlags
    • 17.1.3 收集动态节点
    • 17.1.4 渲染器的运行时支持
    • 17.2.1 带有 v-if 指令的节点
    • 17.2.2 带有 v-for 指令的节点
    • 17.2.3 Fragment 的稳定性
    • 17.3 静态提升
    • 17.4 预字符串化
    • 17.5 缓存内联事件处理函数
    • 17.6 v-once
  • 18-同构渲染

    • 18.1 CSR、SSR 以及同构渲染
    • 18.2 将虚拟 DOM 渲染为 HTML 字符串
    • 18.3 将组件渲染为 HTML 字符串
    • 18.4 客户端激活的原理
    • 18.5.1 组件的生命周期
    • 18.5.2 使用跨平台的 API
    • 18.5.3 只在某一端引入模块
    • 18.5.4 避免交叉请求引起的状态污染
    • 18.5.5 ClientOnly 组件

18.2 将虚拟 DOM 渲染为 HTML 字符串

同构渲染在服务端不需要真实 DOM,只需把标签类型的 vnode 递归拼接成 HTML 字符串;难点全在边界处理:void element、属性合法性、HTML 转义。

一、最小实现:拼接标签

给一个标签类型的 vnode:

const ElementVNode = {
  type: 'div',
  props: { id: 'foo' },
  children: [
    { type: 'p', children: 'hello' }
  ]
}

不考虑任何边界条件时,直接按「开始标签 → 属性 → 子节点 → 结束标签」拼接:

function renderElementVNode(vnode) {
  const { type: tag, props, children } = vnode
  // 开始标签的头部
  let ret = `<${tag}`
  // 处理标签属性
  if (props) {
    for (const k in props) {
      // 以 key="value" 的形式拼接字符串
      ret += ` ${k}="${props[k]}"`
    }
  }
  ret += `>`

  // 处理子节点
  if (typeof children === 'string') {
    // 文本子节点,直接拼接
    ret += children
  } else if (Array.isArray(children)) {
    // 一组子节点,递归渲染
    children.forEach(child => {
      ret += renderElementVNode(child)
    })
  }

  ret += `</${tag}>`
  return ret
}

这版实现只演示核心原理,不满足生产要求,缺陷有四类:自闭合标签、属性名称合法性与属性值转义、子节点类型多样(Fragment / 组件 / 函数式组件 / 文本)、文本子节点也需要转义。

二、处理 void element(自闭合标签)

像 area、br、img、input 这类标签在 HTML 中不允许有子节点,也没有结束标签,统称 void element:渲染时自闭合、直接返回,不再处理 children。

const VOID_TAGS = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'.split(',')

// 判断是否是 void element
const isVoidElement = VOID_TAGS.includes(tag)

// 是 void element 就自闭合
ret += isVoidElement ? `/>` : `>`
// 没有 children,无需继续处理
if (isVoidElement) return ret

三、严谨地处理属性

属性不能只做 key="value" 的拼接,需要分三类处理。

1. 忽略不该渲染的属性

vnode 的 props 里往往混着仅供运行时使用的字段,服务端渲染必须忽略:

  • key:只服务于虚拟 DOM 的 Diff 算法;
  • ref:只用于实现 template ref;
  • 以 on + 非小写字母开头的属性(onClick 等):事件,服务端渲染不考虑事件绑定。
const shouldIgnoreProp = ['key', 'ref']

// 事件或应忽略的属性,跳过
if (shouldIgnoreProp.includes(key) || /^on[^a-z]/.test(key)) continue

2. boolean attribute

checked、disabled 这类 boolean attribute 的渲染规则与普通属性不同:

属性值渲染结果
false什么都不渲染
其他值(含 true)只渲染属性名,如 disabled

3. 属性名称的安全校验

属性名称必须由一个或多个「非下述字符」组成,否则不安全且不合法:

  • 控制字符(control character):码点 [0x01, 0x1f] 与 [0x7f, 0x9f];
  • U+0020(SPACE)、U+0022(")、U+0027(')、U+003E(>)、U+002F(/)、U+003D(=);
  • noncharacters:Unicode 永久保留的码点,范围 [0xFDD0, 0xFDEF],以及 0xFFFE、0xFFFF、0x1FFFE、0x1FFFF、0x2FFFE、0x2FFFF … 等。

模板编译器在编译阶段已经处理过 noncharacters 与控制字符,所以运行时只需小范围校验。

// 属性名称是否合法且安全
const isSSRSafeAttrName = key => !/[>/="'\u0009\u000a\u000c\u0020]/.test(key)

4. 属性的统一渲染

class / style 在 vnode 中可能用数组、对象等结构表示(如 class: ['foo', 'bar']),序列化成字符串表示后再走通用逻辑即可。

const isBooleanAttr = key =>
  (`itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly` +
   `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,` +
   `loop,open,required,reversed,scoped,seamless,` +
   `checked,muted,multiple,selected`).split(',').includes(key)

function renderDynamicAttr(key, value) {
  if (isBooleanAttr(key)) {
    // boolean attribute:false 不渲染,否则只渲染属性名
    return value === false ? `` : ` ${key}`
  } else if (isSSRSafeAttrName(key)) {
    // 安全属性:属性值必须做 HTML 转义
    return value === '' ? ` ${key}` : ` ${key}="${escapeHtml(value)}"`
  } else {
    // 不安全属性:跳过并告警
    console.warn(
      `[@vue/server-renderer] Skipped rendering unsafe attribute name: ${key}`
    )
    return ``
  }
}

function renderAttrs(props) {
  let ret = ''
  for (const key in props) {
    // 忽略事件与运行时专用属性
    if (shouldIgnoreProp.includes(key) || /^on[^a-z]/.test(key)) continue
    ret += renderDynamicAttr(key, props[key])
  }
  return ret
}

四、HTML 转义

转义就是把特殊字符换成对应的 HTML 实体,这是防御 XSS 攻击的关键。

场景需转义字符实体
普通文本内容&&amp;
普通文本内容<&lt;
普通文本内容>&gt;
属性值"&quot;
属性值'&#39;

属性值除了前三个,还要额外转义 " 与 ',否则可能被拼出新的属性。

const escapeRE = /["'&<>]/

function escapeHtml(string) {
  const str = '' + string
  const match = escapeRE.exec(str)
  if (!match) return str

  let html = ''
  let escaped
  let index
  let lastIndex = 0
  for (index = match.index; index < str.length; index++) {
    switch (str.charCodeAt(index)) {
      case 34: escaped = '&quot;'; break // "
      case 38: escaped = '&amp;'; break  // &
      case 39: escaped = '&#39;'; break  // '
      case 60: escaped = '&lt;'; break   // <
      case 62: escaped = '&gt;'; break   // >
      default: continue
    }
    if (lastIndex !== index) {
      html += str.substring(lastIndex, index)
    }
    lastIndex = index + 1
    html += escaped
  }
  return lastIndex !== index ? html + str.substring(lastIndex, index) : html
}

五、整合后的 renderElementVNode

function renderElementVNode(vnode) {
  const { type: tag, props, children } = vnode
  const isVoidElement = VOID_TAGS.includes(tag)

  let ret = `<${tag}`
  if (props) {
    // 属性统一交给 renderAttrs 严谨处理
    ret += renderAttrs(props)
  }

  ret += isVoidElement ? `/>` : `>`
  if (isVoidElement) return ret

  if (typeof children === 'string') {
    // 文本子节点同样需要转义
    ret += escapeHtml(children)
  } else if (Array.isArray(children)) {
    children.forEach(child => {
      ret += renderElementVNode(child)
    })
  }

  ret += `</${tag}>`
  return ret
}

该函数只处理标签类型的 vnode,子节点里一旦出现组件类型就无法继续,需要由统一的分发函数接手。

相关

  • 18.3 将组件渲染为 HTML 字符串:向上补齐一层,让组件类型的 vnode 也能被渲染为字符串
最近更新: 2026/10/12 00:05
Contributors: liujw
Prev
18.1 CSR、SSR 以及同构渲染
Next
18.3 将组件渲染为 HTML 字符串