Skip to content

7.3 SSR 之 renderToString:服务器端如何将 vnode 渲染成 HTML 字符串?

服务器端渲染(SSR)是现代 Web 应用的重要技术,它能够提升首屏加载速度、改善 SEO 效果,并提供更好的用户体验。Vue 3 的 SSR 实现位于 @vue/server-renderer 包中,本节将深入分析其核心机制,揭示 Vue 如何在服务器端将虚拟 DOM 转换为 HTML 字符串。

SSR 架构概览

核心文件结构

Vue 3 的服务端渲染主要由以下核心文件组成:

server-renderer/src/
├── renderToString.ts     # 字符串渲染入口
├── renderToStream.ts     # 流式渲染实现
├── render.ts            # 核心渲染逻辑
├── internal.ts          # 内部辅助函数
└── helpers/             # 渲染辅助函数
    ├── ssrCompile.ts    # 服务端模板编译
    ├── ssrRenderComponent.ts  # 组件渲染
    ├── ssrRenderAttrs.ts     # 属性渲染
    ├── ssrRenderSuspense.ts  # Suspense 处理
    └── ...

SSR 上下文

typescript
export type SSRContext = {
  [key: string]: any
  teleports?: Record<string, string>        // Teleport 内容映射
  /**
   * @internal
   */
  __teleportBuffers?: Record<string, SSRBuffer>  // Teleport 缓冲区
  /**
   * @internal
   */
  __watcherHandles?: (() => void)[]         // 监听器清理函数
}

renderToString 核心流程

渲染入口

renderToString 是 SSR 的主要入口函数:

typescript
export async function renderToString(
  input: App | VNode,
  context: SSRContext = {},
): Promise<string> {
  if (isVNode(input)) {
    // 原始 vnode,包装成应用
    return renderToString(createApp({ render: () => input }), context)
  }

  // 渲染应用
  const vnode = createVNode(input._component, input._props)
  vnode.appContext = input._context
  
  // 提供 SSR 上下文给组件树
  input.provide(ssrContextKey, context)
  
  // 渲染组件 VNode
  const buffer = await renderComponentVNode(vnode)

  // 展开缓冲区为字符串
  const result = await unrollBuffer(buffer as SSRBuffer)

  // 解析 Teleport 内容
  await resolveTeleports(context)

  // 清理监听器
  if (context.__watcherHandles) {
    for (const unwatch of context.__watcherHandles) {
      unwatch()
    }
  }

  return result
}

缓冲区机制

SSR 使用缓冲区(Buffer)机制来处理异步渲染:

typescript
export type SSRBuffer = SSRBufferItem[] & { hasAsync?: boolean }
export type SSRBufferItem = string | SSRBuffer | Promise<SSRBuffer>
export type PushFn = (item: SSRBufferItem) => void

// 创建缓冲区
export function createBuffer() {
  let appendable = false
  const buffer: SSRBuffer = []
  
  return {
    getBuffer(): SSRBuffer {
      return buffer
    },
    push(item: SSRBufferItem): void {
      const isStringItem = isString(item)
      
      // 字符串合并优化
      if (appendable && isStringItem) {
        buffer[buffer.length - 1] += item as string
        return
      }
      
      buffer.push(item)
      appendable = isStringItem
      
      // 标记异步状态
      if (isPromise(item) || (isArray(item) && item.hasAsync)) {
        buffer.hasAsync = true
      }
    },
  }
}

缓冲区展开

缓冲区展开是将嵌套的异步缓冲区转换为最终 HTML 字符串的过程:

typescript
function nestedUnrollBuffer(
  buffer: SSRBuffer,
  parentRet: string,
  startIndex: number,
): Promise<string> | string {
  if (!buffer.hasAsync) {
    // 同步缓冲区,直接展开
    return parentRet + unrollBufferSync(buffer)
  }

  let ret = parentRet
  for (let i = startIndex; i < buffer.length; i += 1) {
    const item = buffer[i]
    
    if (isString(item)) {
      ret += item
      continue
    }

    if (isPromise(item)) {
      // 等待 Promise 解析
      return item.then(nestedItem => {
        buffer[i] = nestedItem
        return nestedUnrollBuffer(buffer, ret, i)
      })
    }

    // 递归处理嵌套缓冲区
    const result = nestedUnrollBuffer(item, ret, 0)
    if (isPromise(result)) {
      return result.then(nestedItem => {
        buffer[i] = nestedItem
        return nestedUnrollBuffer(buffer, '', i)
      })
    }

    ret = result
  }

  return ret
}

export function unrollBuffer(buffer: SSRBuffer): Promise<string> | string {
  return nestedUnrollBuffer(buffer, '', 0)
}

组件渲染机制

组件实例化

服务端组件渲染的核心是 renderComponentVNode 函数:

typescript
export function renderComponentVNode(
  vnode: VNode,
  parentComponent: ComponentInternalInstance | null = null,
  slotScopeId?: string,
): SSRBuffer | Promise<SSRBuffer> {
  // 创建组件实例
  const instance = (vnode.component = createComponentInstance(
    vnode,
    parentComponent,
    null,
  ))
  
  if (__DEV__) pushWarningContext(vnode)
  
  // 设置组件(SSR 模式)
  const res = setupComponent(instance, true /* isSSR */)
  
  if (__DEV__) popWarningContext()
  
  const hasAsyncSetup = isPromise(res)
  let prefetches = instance.sp /* LifecycleHooks.SERVER_PREFETCH */
  
  // 处理异步 setup 和 serverPrefetch
  if (hasAsyncSetup || prefetches) {
    const p: Promise<unknown> = Promise.resolve(res as Promise<void>)
      .then(() => {
        if (hasAsyncSetup) prefetches = instance.sp
        if (prefetches) {
          return Promise.all(
            prefetches.map(prefetch => prefetch.call(instance.proxy)),
          )
        }
      })
      .catch(NOOP)
    
    return p.then(() => renderComponentSubTree(instance, slotScopeId))
  } else {
    return renderComponentSubTree(instance, slotScopeId)
  }
}

组件子树渲染

typescript
function renderComponentSubTree(
  instance: ComponentInternalInstance,
  slotScopeId?: string,
): SSRBuffer | Promise<SSRBuffer> {
  if (__DEV__) pushWarningContext(instance.vnode)
  
  const comp = instance.type as Component
  const { getBuffer, push } = createBuffer()
  
  if (isFunction(comp)) {
    // 函数式组件
    let root = renderComponentRoot(instance)
    
    // 处理作用域 ID 透传
    if (!(comp as FunctionalComponent).props) {
      for (const key in instance.attrs) {
        if (key.startsWith(`data-v-`)) {
          ;(root.props || (root.props = {}))[key] = ``
        }
      }
    }
    
    renderVNode(push, (instance.subTree = root), instance, slotScopeId)
  } else {
    // 普通组件
    
    // 编译模板(如果需要)
    if (
      (!instance.render || instance.render === NOOP) &&
      !instance.ssrRender &&
      !comp.ssrRender &&
      isString(comp.template)
    ) {
      comp.ssrRender = ssrCompile(comp.template, instance)
    }

    const ssrRender = instance.ssrRender || comp.ssrRender
    
    if (ssrRender) {
      // 优化的 SSR 渲染
      let attrs = instance.inheritAttrs !== false ? instance.attrs : undefined
      let hasCloned = false

      // 收集作用域 ID
      let cur = instance
      while (true) {
        const scopeId = cur.vnode.scopeId
        if (scopeId) {
          if (!hasCloned) {
            attrs = { ...attrs }
            hasCloned = true
          }
          attrs![scopeId] = ''
        }
        
        const parent = cur.parent
        if (parent && parent.subTree && parent.subTree === cur.vnode) {
          cur = parent
        } else {
          break
        }
      }

      // 处理插槽作用域 ID
      if (slotScopeId) {
        if (!hasCloned) attrs = { ...attrs }
        const slotScopeIdList = slotScopeId.trim().split(' ')
        for (let i = 0; i < slotScopeIdList.length; i++) {
          attrs![slotScopeIdList[i]] = ''
        }
      }

      // 执行 SSR 渲染函数
      const prev = setCurrentRenderingInstance(instance)
      try {
        ssrRender(
          instance.proxy,
          push,
          instance,
          attrs,
          // 编译器优化的绑定
          instance.props,
          instance.setupState,
          instance.data,
          instance.ctx,
        )
      } finally {
        setCurrentRenderingInstance(prev)
      }
    } else if (instance.render && instance.render !== NOOP) {
      // 使用 render 函数
      renderVNode(
        push,
        (instance.subTree = renderComponentRoot(instance)),
        instance,
        slotScopeId,
      )
    } else {
      // 缺少模板或渲染函数
      const componentName = comp.name || comp.__file || `<Anonymous>`
      warn(`Component ${componentName} is missing template or render function.`)
      push(`<!---->`)
    }
  }
  
  if (__DEV__) popWarningContext()
  return getBuffer()
}

VNode 渲染逻辑

核心渲染函数

renderVNode 是处理各种类型 VNode 的核心函数:

typescript
export function renderVNode(
  push: PushFn,
  vnode: VNode,
  parentComponent: ComponentInternalInstance,
  slotScopeId?: string,
): void {
  const { type, shapeFlag, children, dirs, props } = vnode
  
  // 应用 SSR 指令
  if (dirs) {
    vnode.props = applySSRDirectives(vnode, props, dirs)
  }

  switch (type) {
    case Text:
      push(escapeHtml(children as string))
      break
      
    case Comment:
      push(
        children
          ? `<!--${escapeHtmlComment(children as string)}-->`
          : `<!---->`,
      )
      break
      
    case Static:
      push(children as string)
      break
      
    case Fragment:
      if (vnode.slotScopeIds) {
        slotScopeId =
          (slotScopeId ? slotScopeId + ' ' : '') + vnode.slotScopeIds.join(' ')
      }
      push(`<!--[-->`) // 开始标记
      renderVNodeChildren(
        push,
        children as VNodeArrayChildren,
        parentComponent,
        slotScopeId,
      )
      push(`<!--]-->`) // 结束标记
      break
      
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        renderElementVNode(push, vnode, parentComponent, slotScopeId)
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        push(renderComponentVNode(vnode, parentComponent, slotScopeId))
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
        renderTeleportVNode(push, vnode, parentComponent, slotScopeId)
      } else if (shapeFlag & ShapeFlags.SUSPENSE) {
        renderVNode(push, vnode.ssContent!, parentComponent, slotScopeId)
      } else {
        warn(
          '[@vue/server-renderer] Invalid VNode type:',
          type,
          `(${typeof type})`,
        )
      }
  }
}

元素渲染

typescript
function renderElementVNode(
  push: PushFn,
  vnode: VNode,
  parentComponent: ComponentInternalInstance,
  slotScopeId?: string,
) {
  const tag = vnode.type as string
  let { props, children, shapeFlag, scopeId } = vnode
  let openTag = `<${tag}`

  // 渲染属性
  if (props) {
    openTag += ssrRenderAttrs(props, tag)
  }

  // 添加作用域 ID
  if (scopeId) {
    openTag += ` ${scopeId}`
  }
  
  // 继承父组件链的作用域 ID
  let curParent: ComponentInternalInstance | null = parentComponent
  let curVnode = vnode
  while (curParent && curVnode === curParent.subTree) {
    curVnode = curParent.vnode
    if (curVnode.scopeId) {
      openTag += ` ${curVnode.scopeId}`
    }
    curParent = curParent.parent
  }
  
  if (slotScopeId) {
    openTag += ` ${slotScopeId}`
  }

  push(openTag + `>`)
  
  if (!isVoidTag(tag)) {
    let hasChildrenOverride = false
    
    // 处理特殊属性
    if (props) {
      if (props.innerHTML) {
        hasChildrenOverride = true
        push(props.innerHTML)
      } else if (props.textContent) {
        hasChildrenOverride = true
        push(escapeHtml(props.textContent))
      } else if (tag === 'textarea' && props.value) {
        hasChildrenOverride = true
        push(escapeHtml(props.value))
      }
    }
    
    if (!hasChildrenOverride) {
      if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
        push(escapeHtml(children as string))
      } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        renderVNodeChildren(
          push,
          children as VNodeArrayChildren,
          parentComponent,
          slotScopeId,
        )
      }
    }
    
    push(`</${tag}>`)
  }
}

异步组件与 Suspense

异步组件处理

在 SSR 中,异步组件会被等待解析后再渲染:

typescript
// 异步组件示例
const AsyncComponent = {
  async setup() {
    // 异步数据获取
    const data = await fetchData()
    return () => h('div', data)
  },
}

// 在 renderComponentVNode 中的处理
if (hasAsyncSetup || prefetches) {
  const p: Promise<unknown> = Promise.resolve(res as Promise<void>)
    .then(() => {
      if (hasAsyncSetup) prefetches = instance.sp
      if (prefetches) {
        // 执行 serverPrefetch 钩子
        return Promise.all(
          prefetches.map(prefetch => prefetch.call(instance.proxy)),
        )
      }
    })
    .catch(NOOP)
  
  return p.then(() => renderComponentSubTree(instance, slotScopeId))
}

Suspense 在 SSR 中的实现

typescript
export async function ssrRenderSuspense(
  push: PushFn,
  { default: renderContent }: Record<string, (() => void) | undefined>,
): Promise<void> {
  if (renderContent) {
    renderContent()  // 直接渲染默认内容
  } else {
    push(`<!---->`)   // 空注释
  }
}

SSR 中的 Suspense 行为与客户端不同:

  • 服务端:等待所有异步组件解析完成,直接渲染最终内容
  • 客户端:先显示 fallback,异步组件解析后替换内容

错误处理

typescript
// 测试用例展示错误处理
const RejectingAsync = {
  setup() {
    return new Promise((_, reject) => reject('error'))
  },
}

// 渲染结果:组件渲染失败时输出空注释
expect(await renderToString(createApp(RejectingAsync))).toBe(`<!---->`)

流式渲染实现

流式渲染原理

流式渲染允许在组件渲染完成时立即发送 HTML 片段,而不需要等待整个页面渲染完成:

typescript
export function renderToSimpleStream<T extends SimpleReadable>(
  input: App | VNode,
  context: SSRContext,
  stream: T,
): T {
  if (isVNode(input)) {
    return renderToSimpleStream(
      createApp({ render: () => input }),
      context,
      stream,
    )
  }

  const vnode = createVNode(input._component, input._props)
  vnode.appContext = input._context
  input.provide(ssrContextKey, context)

  Promise.resolve(renderComponentVNode(vnode))
    .then(buffer => unrollBuffer(buffer, stream))  // 流式展开缓冲区
    .then(() => resolveTeleports(context))
    .then(() => {
      if (context.__watcherHandles) {
        for (const unwatch of context.__watcherHandles) {
          unwatch()
        }
      }
    })
    .then(() => stream.push(null))  // 结束流
    .catch(error => {
      stream.destroy(error)         // 错误处理
    })

  return stream
}

流式缓冲区展开

typescript
async function unrollBuffer(
  buffer: SSRBuffer,
  stream: SimpleReadable,
): Promise<void> {
  if (buffer.hasAsync) {
    // 异步缓冲区:逐个等待并推送
    for (let i = 0; i < buffer.length; i++) {
      let item = buffer[i]
      if (isPromise(item)) {
        item = await item
      }
      if (isString(item)) {
        stream.push(item)  // 立即推送字符串
      } else {
        await unrollBuffer(item, stream)  // 递归处理嵌套缓冲区
      }
    }
  } else {
    // 同步缓冲区:直接展开
    unrollBufferSync(buffer, stream)
  }
}

function unrollBufferSync(buffer: SSRBuffer, stream: SimpleReadable) {
  for (let i = 0; i < buffer.length; i++) {
    let item = buffer[i]
    if (isString(item)) {
      stream.push(item)
    } else {
      unrollBufferSync(item as SSRBuffer, stream)
    }
  }
}

多种流式 API

Vue 3 提供了多种流式渲染 API 以适应不同环境:

Node.js Streams

typescript
export function renderToNodeStream(
  input: App | VNode,
  context: SSRContext = {},
): Readable {
  const stream: Readable = __CJS__
    ? new (require('node:stream').Readable)({ read() {} })
    : null

  if (!stream) {
    throw new Error(
      `ESM build of renderToStream() does not support renderToNodeStream(). ` +
        `Use pipeToNodeWritable() with an existing Node.js Writable stream ` +
        `instance instead.`,
    )
  }

  return renderToSimpleStream(input, context, stream)
}

export function pipeToNodeWritable(
  input: App | VNode,
  context: SSRContext | undefined = {},
  writable: Writable,
): void {
  renderToSimpleStream(input, context, {
    push(content) {
      if (content != null) {
        writable.write(content)
      } else {
        writable.end()
      }
    },
    destroy(err) {
      writable.destroy(err)
    },
  })
}

Web Streams

typescript
export function renderToWebStream(
  input: App | VNode,
  context: SSRContext = {},
): ReadableStream {
  if (typeof ReadableStream !== 'function') {
    throw new Error(
      `ReadableStream constructor is not available in the global scope.`
    )
  }

  const encoder = new TextEncoder()
  let cancelled = false

  return new ReadableStream({
    start(controller) {
      renderToSimpleStream(input, context, {
        push(content) {
          if (cancelled) return
          if (content != null) {
            controller.enqueue(encoder.encode(content))
          } else {
            controller.close()
          }
        },
        destroy(err) {
          controller.error(err)
        },
      })
    },
    cancel() {
      cancelled = true
    },
  })
}

export function pipeToWebWritable(
  input: App | VNode,
  context: SSRContext | undefined = {},
  writable: WritableStream,
): void {
  const writer = writable.getWriter()
  const encoder = new TextEncoder()

  // CloudFlare Workers 兼容性处理
  let hasReady = false
  try {
    hasReady = isPromise(writer.ready)
  } catch (e: any) {}

  renderToSimpleStream(input, context, {
    async push(content) {
      if (hasReady) {
        await writer.ready
      }
      if (content != null) {
        return writer.write(encoder.encode(content))
      } else {
        return writer.close()
      }
    },
    destroy(err) {
      console.log(err)
      writer.close()
    },
  })
}

服务端模板编译

编译缓存机制

typescript
const compileCache: Record<string, SSRRenderFunction> = Object.create(null)

export function ssrCompile(
  template: string,
  instance: ComponentInternalInstance,
): SSRRenderFunction {
  // ESM 构建不支持运行时编译
  if (!__CJS__) {
    throw new Error(
      `On-the-fly template compilation is not supported in the ESM build of ` +
        `@vue/server-renderer. All templates must be pre-compiled into ` +
        `render functions.`,
    )
  }

  const Component = instance.type as ComponentOptions
  const { isCustomElement, compilerOptions } = instance.appContext.config
  const { delimiters, compilerOptions: componentCompilerOptions } = Component

  // 合并编译选项
  const finalCompilerOptions: CompilerOptions = extend(
    extend(
      {
        isCustomElement,
        delimiters,
      },
      compilerOptions,
    ),
    componentCompilerOptions,
  )

  finalCompilerOptions.isCustomElement =
    finalCompilerOptions.isCustomElement || NO
  finalCompilerOptions.isNativeTag = finalCompilerOptions.isNativeTag || NO

  // 生成缓存键
  const cacheKey = JSON.stringify(
    {
      template,
      compilerOptions: finalCompilerOptions,
    },
    (key, value) => {
      return isFunction(value) ? value.toString() : value
    },
  )

  // 检查缓存
  const cached = compileCache[cacheKey]
  if (cached) {
    return cached
  }

  // 错误处理
  finalCompilerOptions.onError = (err: CompilerError) => {
    if (__DEV__) {
      const message = `[@vue/server-renderer] Template compilation error: ${err.message}`
      const codeFrame =
        err.loc &&
        generateCodeFrame(
          template as string,
          err.loc.start.offset,
          err.loc.end.offset,
        )
      warn(codeFrame ? `${message}\n${codeFrame}` : message)
    } else {
      throw err
    }
  }

  // 编译模板
  const { code } = compile(template, finalCompilerOptions)
  
  // 创建模拟的 require 函数
  const requireMap = {
    vue: Vue,
    'vue/server-renderer': helpers,
  }
  const fakeRequire = (id: 'vue' | 'vue/server-renderer') => requireMap[id]
  
  // 缓存并返回编译结果
  return (compileCache[cacheKey] = Function('require', code)(fakeRequire))
}

Teleport 处理

Teleport 渲染

typescript
function renderTeleportVNode(
  push: PushFn,
  vnode: VNode,
  parentComponent: ComponentInternalInstance,
  slotScopeId?: string,
) {
  const target = vnode.props && vnode.props.to
  const disabled = vnode.props && vnode.props.disabled
  
  if (!target) {
    if (!disabled) {
      warn(`[@vue/server-renderer] Teleport is missing target prop.`)
    }
    return []
  }
  
  if (!isString(target)) {
    warn(
      `[@vue/server-renderer] Teleport target must be a query selector string.`,
    )
    return []
  }
  
  ssrRenderTeleport(
    push,
    push => {
      renderVNodeChildren(
        push,
        vnode.children as VNodeArrayChildren,
        parentComponent,
        slotScopeId,
      )
    },
    target,
    disabled || disabled === '',
    parentComponent,
  )
}

Teleport 解析

typescript
export async function resolveTeleports(context: SSRContext): Promise<void> {
  if (context.__teleportBuffers) {
    context.teleports = context.teleports || {}
    
    for (const key in context.__teleportBuffers) {
      // 注意:这里可以顺序等待,因为 Promise 是并行创建的
      context.teleports[key] = await unrollBuffer(
        await Promise.all([context.__teleportBuffers[key]]),
      )
    }
  }
}

性能优化策略

1. 渲染缓存

组件级缓存

typescript
// 组件缓存示例
const CachedComponent = {
  name: 'CachedComponent',
  serverCacheKey: (props) => props.id,
  render() {
    return h('div', `Cached content for ${this.id}`)
  }
}

模板编译缓存

typescript
// ssrCompile.ts 中的缓存机制
const compileCache: Record<string, SSRRenderFunction> = Object.create(null)

// 基于模板和编译选项生成缓存键
const cacheKey = JSON.stringify({
  template,
  compilerOptions: finalCompilerOptions,
})

const cached = compileCache[cacheKey]
if (cached) {
  return cached  // 直接返回缓存的渲染函数
}

2. 流式输出优化

减少 TTFB(Time To First Byte)

typescript
// 流式渲染立即开始输出
Promise.resolve(renderComponentVNode(vnode))
  .then(buffer => unrollBuffer(buffer, stream))  // 边渲染边输出
  .then(() => stream.push(null))  // 完成时关闭流

异步组件优化

typescript
// 异步组件并行处理
if (prefetches) {
  return Promise.all(
    prefetches.map(prefetch => prefetch.call(instance.proxy)),
  )
}

3. 字符串合并优化

typescript
// createBuffer 中的字符串合并
push(item: SSRBufferItem): void {
  const isStringItem = isString(item)
  
  // 连续的字符串直接合并,减少数组长度
  if (appendable && isStringItem) {
    buffer[buffer.length - 1] += item as string
    return
  }
  
  buffer.push(item)
  appendable = isStringItem
}

4. 作用域 ID 优化

typescript
// 延迟克隆属性对象
let attrs = instance.inheritAttrs !== false ? instance.attrs : undefined
let hasCloned = false

// 只在需要时才克隆
if (scopeId) {
  if (!hasCloned) {
    attrs = { ...attrs }
    hasCloned = true
  }
  attrs![scopeId] = ''
}

错误处理与调试

开发模式增强

typescript
if (__DEV__) {
  // 警告上下文管理
  pushWarningContext(vnode)
  // ... 渲染逻辑
  popWarningContext()
  
  // 详细错误信息
  const message = `[@vue/server-renderer] Template compilation error: ${err.message}`
  const codeFrame = err.loc && generateCodeFrame(
    template as string,
    err.loc.start.offset,
    err.loc.end.offset,
  )
  warn(codeFrame ? `${message}\n${codeFrame}` : message)
}

生产模式优化

typescript
if (__DEV__) {
  // 开发模式的详细错误处理
} else {
  throw err  // 生产模式直接抛出错误
}

与客户端渲染的差异

1. 生命周期差异

生命周期钩子客户端服务端
beforeCreate
created
beforeMount
mounted
serverPrefetch

2. 响应式系统

  • 服务端:响应式数据在渲染时是静态的,不会触发更新
  • 客户端:响应式数据变化会触发重新渲染

3. 事件处理

  • 服务端:事件监听器不会被注册,只渲染静态 HTML
  • 客户端:事件监听器正常工作

4. DOM 操作

  • 服务端:无法访问 DOM API
  • 客户端:可以正常使用 DOM API

最佳实践

1. 组件设计

typescript
// ✅ 好的 SSR 组件设计
const SSRFriendlyComponent = {
  async serverPrefetch() {
    // 在服务端预取数据
    this.data = await fetchData()
  },
  
  data() {
    return {
      data: null
    }
  },
  
  async mounted() {
    // 客户端激活时的逻辑
    if (!this.data) {
      this.data = await fetchData()
    }
  }
}

2. 异步数据处理

typescript
// ✅ 使用 serverPrefetch 预取数据
export default {
  async serverPrefetch() {
    // 服务端数据预取
    const data = await this.$store.dispatch('fetchData')
    return data
  },
  
  async created() {
    // 客户端数据获取
    if (process.client && !this.$store.state.data) {
      await this.$store.dispatch('fetchData')
    }
  }
}

3. 条件渲染

typescript
// ✅ 使用 ClientOnly 组件包装客户端特定内容
const App = {
  render() {
    return h('div', [
      h('h1', 'Universal Content'),
      h(ClientOnly, () => h('div', 'Client Only Content'))
    ])
  }
}

4. 性能监控

typescript
// ✅ 添加性能监控
const startTime = Date.now()
const html = await renderToString(app, context)
const renderTime = Date.now() - startTime

console.log(`SSR render time: ${renderTime}ms`)

总结

Vue 3 的 SSR 实现展现了现代前端框架在服务端渲染方面的技术深度:

  1. 高效的渲染机制:通过缓冲区系统和流式渲染实现高性能输出
  2. 完善的异步处理:支持异步组件、Suspense 和 serverPrefetch
  3. 灵活的流式 API:支持 Node.js Streams 和 Web Streams
  4. 智能的优化策略:编译缓存、字符串合并、作用域 ID 优化
  5. 强大的错误处理:开发模式的详细错误信息和生产模式的性能优化

SSR 的核心价值在于:

  • 更快的首屏加载:服务端直接输出 HTML
  • 更好的 SEO:搜索引擎可以直接索引内容
  • 更佳的用户体验:减少白屏时间
  • 渐进式增强:支持 JavaScript 禁用的环境

Vue 3 的 SSR 实现不仅保持了框架的易用性,还在性能和功能方面达到了工业级标准,为构建现代 Web 应用提供了强有力的技术支撑。


微信公众号二维码