SSR 解决了什么问题,有做过 SSR 吗?

1. 什么是 SSR

SSR(Server-Side Rendering)意为服务端渲染,指由服务侧完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程。

打开页面查看源码,浏览器拿到的是全部的 DOM 结构。

Web 发展的 3 个阶段

  1. 服务端渲染(SSR):服务端直接吐出完整 HTML,浏览器拿到的内容包括首屏及完整结构。
  2. 单页应用(SPA):浏览器拿到的仅有宿主元素 #app,其余内容由 JS 运行时渲染。
  3. SSR + SPA 同构:后端渲染出完整的首屏 DOM 结构返回,前端拿到内容包括首屏及完整 SPA 结构,应用激活后依然按照 SPA 方式运行。

2. SSR 解决了什么

SSR 主要解决了以下两种问题:

  1. SEO 问题:SPA 首屏 HTML 内容为空,搜索引擎爬虫无法抓取到关键内容,影响收录。SSR 直接吐出完整 HTML,对爬虫友好。
  2. 首屏白屏问题:SPA 需要先下载 JS 再执行渲染,首屏出现明显白屏。SSR 直接返回首屏内容,TTFB 到首屏可见的时间更短。

SSR 的缺点

  • 开发条件受限:浏览器端特有的代码(window / document)只能放在 beforeMount / mounted 等生命周期里;一些外部库可能需要特殊处理。
  • 更多的服务端负载:在服务端渲染时需要占用 CPU 资源,如果访问量大且复杂,可能会占用大量 CPU。
  • 部署 / 运维复杂:需要同时维护 Node 服务和静态服务。

选型前要慎重考虑的问题

  • 是否真的需要 SEO?
  • 首屏可见时间对业务的关键程度?
  • 团队是否具备 Node 端开发和运维能力?

3. 如何实现 SSR

对于同构开发,我们依然使用 webpack 打包,但要解决两个问题:服务端首屏渲染 和 客户端激活。

这里需要生成一个服务器 bundle 文件用于服务端首屏渲染,和一个客户端 bundle 文件用于客户端激活。

代码结构

除了两个不同入口(客户端入口和服务端入口),其他结构和之前的 vue 应用完全相同:

src
├── router
│   └── index.js          # 路由声明
├── store
│   └── index.js          # 全局状态
├── main.js               # 用于创建 vue 实例
├── entry-client.js       # 客户端入口,用于静态内容"激活"
└── entry-server.js       # 服务端入口,用于首屏内容渲染

主文件

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'

// 导出 Vue 实例工厂函数,为每次请求创建独立实例
// 上下文用于给 vue 实例传递参数
export function createApp(context) {
  const router = createRouter()
  const store = createStore()
  const app = new Vue({
    router,
    context,
    render: h => h(App)
  })
  return { app, router, store }
}

服务端入口

它的任务是创建 Vue 实例并根据传入的 url 指定首屏:

import { createApp } from './main'

// 返回一个函数,接收请求上下文,返回创建的 vue 实例
export default context => {
  return new Promise((resolve, reject) => {
    const { app, router } = createApp(context)
    // 跳转到首屏的地址
    router.push(context.url)
    // 路由就绪,返回结果
    router.onReady(() => {
      resolve(app)
    }, reject)
  })
}

客户端入口

客户端入口只需创建 vue 实例并执行挂载,这一步称为"激活":

import { createApp } from './main'

const { app, router } = createApp()
router.onReady(() => {
  app.$mount('#app')
})

注意:服务器端渲染的是应用程序的"快照"。如果应用依赖于异步数据,那么在开始渲染之前,需要先预取和解析好这些数据。

来源整理自:vue3js.cn 面试官系列