Skip to content

SSR 渲染器:renderToString 的实现

renderToString 是 SSR 的核心函数,将 Vue 组件树转换为 HTML 字符串

理解它的实现原理,能帮你更好地优化 SSR 性能。 本章将分析其实现原理。

API 设计

javascript
import { renderToString } from '@vue/server-renderer'

const html = await renderToString(app, {
  // SSR 上下文
  user: currentUser
})

函数签名:

typescript
async function renderToString(
  input: App | VNode,
  context?: SSRContext
): Promise<string>

interface SSRContext {
  modules?: Set<string>   // 收集使用的模块
  teleports?: Record<string, string>  // Teleport 内容
  [key: string]: any      // 自定义数据
}

渲染流程

┌─────────────────────────────────────────┐
│          renderToString 流程            │
├─────────────────────────────────────────┤
│                                         │
│  Vue App                                │
│      │                                  │
│      ▼ createVNode                      │
│  Root VNode                             │
│      │                                  │
│      ▼ renderComponentVNode            │
│  ┌──────────────────────────────────┐  │
│  │         递归渲染                  │  │
│  │  Component → VNode → Children    │  │
│  └──────────────────────────────────┘  │
│      │                                  │
│      ▼ push to buffer                   │
│  SSRBuffer ──────────▶ HTML String     │
│                                         │
└─────────────────────────────────────────┘

缓冲区机制

直接拼接字符串性能较差,使用缓冲区优化:

javascript
type SSRBuffer = SSRBufferItem[]
type SSRBufferItem = string | SSRBuffer | Promise<SSRBuffer>

function createBuffer() {
  let appendable = true
  const buffer = []
  
  return {
    getBuffer() {
      return buffer
    },
    
    push(item) {
      // 优化:合并相邻字符串
      if (appendable && typeof item === 'string') {
        const lastIndex = buffer.length - 1
        if (typeof buffer[lastIndex] === 'string') {
          buffer[lastIndex] += item
          return
        }
      }
      buffer.push(item)
      appendable = typeof item === 'string'
    }
  }
}

关键优化:相邻字符串自动合并,减少数组操作。

核心实现

javascript
async function renderToString(input, context = {}) {
  // 创建根 VNode
  const vnode = isApp(input)
    ? createVNode(input._component, input._props)
    : input
  
  // 设置应用上下文
  if (isApp(input)) {
    vnode.appContext = input._context
  }
  
  // 创建缓冲区
  const buffer = createBuffer()
  
  // 递归渲染
  await renderVNode(buffer.push, vnode, context)
  
  // 展平缓冲区为字符串
  return unrollBuffer(buffer.getBuffer())
}

async function unrollBuffer(buffer) {
  let result = ''
  
  for (const item of buffer) {
    if (typeof item === 'string') {
      result += item
    } else if (Array.isArray(item)) {
      result += await unrollBuffer(item)
    } else {
      // Promise
      result += await unrollBuffer(await item)
    }
  }
  
  return result
}

VNode 渲染分发

javascript
async function renderVNode(push, vnode, context) {
  const { type, shapeFlag } = vnode
  
  switch (type) {
    case Text:
      push(escapeHtml(vnode.children))
      break
      
    case Comment:
      push(`<!--${vnode.children || ''}-->`)
      break
      
    case Static:
      push(vnode.children)
      break
      
    case Fragment:
      await renderChildren(push, vnode.children, context)
      break
      
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        await renderElement(push, vnode, context)
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        await renderComponent(push, vnode, context)
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
        await renderTeleport(push, vnode, context)
      } else if (shapeFlag & ShapeFlags.SUSPENSE) {
        await renderSuspense(push, vnode, context)
      }
  }
}

元素渲染

javascript
async function renderElement(push, vnode, context) {
  const { type: tag, props, children, shapeFlag } = vnode
  
  // 开始标签
  push(`<${tag}`)
  
  // 属性
  if (props) {
    for (const key in props) {
      push(renderAttr(key, props[key]))
    }
  }
  
  // 自闭合标签
  if (isVoidTag(tag)) {
    push('/>')
    return
  }
  
  push('>')
  
  // 子内容
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    push(escapeHtml(children))
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    await renderChildren(push, children, context)
  }
  
  // 结束标签
  push(`</${tag}>`)
}

组件渲染

元素渲染处理的是原生 HTML 标签(<div><span> 等),而组件渲染则复杂得多——需要创建组件实例、执行 setup 函数、获取渲染结果。

首先要问的是:组件渲染与元素渲染的核心区别是什么?

  • 元素:直接输出 HTML 标签和属性
  • 组件:需要实例化 → 执行 setup → 调用 render → 递归渲染子树
javascript
async function renderComponent(push, vnode, context) {
  const { type: Component } = vnode
  
  // 创建组件实例
  const instance = createComponentInstance(vnode)
  
  // 执行 setup
  const setupResult = Component.setup?.(
    instance.props,
    { attrs: instance.attrs, slots: instance.slots, emit: instance.emit }
  )
  
  // 获取渲染函数
  let render = Component.render
  if (typeof setupResult === 'function') {
    render = setupResult
  } else if (setupResult) {
    instance.setupState = setupResult
  }
  
  // SSR 优化:使用 ssrRender(如果有)
  if (Component.ssrRender) {
    Component.ssrRender(instance.proxy, push, instance)
  } else {
    // 回退到普通渲染
    const subTree = render.call(instance.proxy)
    await renderVNode(push, subTree, context)
  }
}

异步组件处理

javascript
async function renderComponent(push, vnode, context) {
  const { type: Component } = vnode
  
  // 异步组件
  if (Component.__asyncLoader) {
    const resolvedComp = await Component.__asyncLoader()
    vnode.type = resolvedComp
    return renderComponent(push, vnode, context)
  }
  
  // ...
}

Suspense 渲染

javascript
async function renderSuspense(push, vnode, context) {
  const { default: defaultSlot, fallback } = vnode.children
  
  try {
    // 尝试渲染默认内容
    await renderSlot(push, defaultSlot, context)
  } catch (error) {
    if (error instanceof Promise) {
      // 等待异步内容
      await error
      await renderSlot(push, defaultSlot, context)
    } else if (fallback) {
      // 渲染 fallback
      await renderSlot(push, fallback, context)
    }
  }
}

本章小结

本章分析了 renderToString 的实现:

  • 缓冲区机制:优化字符串拼接性能
  • VNode 分发:根据类型调用不同渲染函数
  • 元素渲染:标签、属性、子内容
  • 组件渲染:实例化、执行 setup、调用 render
  • 异步处理:异步组件和 Suspense

下一章将分析客户端激活(Hydration)的实现。

SSR 渲染器:renderToString 的实现 has loaded