跳到主要内容

5.1-服务端渲染

Create by fall on: 2022-09-28 Recently revised in: 2022-09-28

服务端渲染

SSR:Server-Side Rendering

默认情况下 Vue 在浏览器中运行,生成和操作 DOM,SSR 会在服务端直接生成 HTML 字符串,作为服务端响应返回给浏览器,最后在浏览器端将静态的 HTML 激活,然后进行交互。

对 Vue 来说,应用的大部分代码同时运行在服务端客户端。

有关服务端渲染的内容可以查看:《大前端知识点》 中关于 SSR 的内容。

使用 Vue 创建

import {createSSRApp} from 'vue'
import {renderToString} from 'vue/server-renderer'
const app = createSSRApp({
data:()=>({count:1}),
template:`<button @click="count++">{{ count }}</button>`
})
renderToString(app).then((html) => {
console.log(html)
})

然后通过 node 运行该内容即可。

renderToString() 接收一个 Vue 应用实例作为参数,返回一个 Promise,当 Promise resolve 时得到应用渲染的 HTML。

客户端激活

渲染到浏览器中的内容并没有交互,HTML 内容完全是静态的。

为了使客户端的应用可交互,Vue 需要执行一个激活步骤。在激活过程中,Vue 会创建一个与服务端完全相同的应用实例,然后将每个组件与它应该控制的 DOM 节点相匹配,并添加 DOM 事件监听器。

为了在激活模式下挂载应用,我们应该使用 createSSRApp() 而不是 createApp()

应用的创建逻辑拆分到单独的 app.js 中:

可以复用的逻辑

// app.js (在服务器和客户端之间共享)
import { createSSRApp } from 'vue'
export function createApp() {
return createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
}
// 这个文件及其依赖项在服务器和客户端之间共享,就是说,都可以使用

服务端和客户端单独的逻辑

// 服务端 server.js
import { createApp } from './app.js'
server.get('/', (req, res) => {
const app = createApp()
renderToString(app).then(html => {
// ... // html 相关的内容
})
})
// 客户端 client.js
import { createApp } from './app.js'
createApp().mount('#app')

为了在浏览器中加载客户端文件,我们还需要:

server.js 中添加 server.use(express.static('.')) 来托管客户端文件。

<script type="module" src="/client.js"></script> 添加到返回的 HTML 外壳以加载客户端入口文件。

通过在 HTML 外壳中添加 Import Map 以支持在浏览器中使用 import * from 'vue'

构建

  • 对于 SSR 应用来说,我们要构建两次,一次是服务端构建,一次是客户端构建。
  • 在服务器请求处理函数中,确保返回的 HTML 包含正确的客户端资源链接和最优的资源加载提示 (如 prefetch 和 preload)。我们可能还需要在 SSR 和 SSG 模式之间切换,甚至在同一个应用中混合使用这两种模式。
  • 以一种通用的方式管理路由、数据获取和状态存储。

强烈建议你使用一种更通用的、更集成化的解决方案,帮你抽象掉那些复杂的东西。

Nuxt

Quasar

SSR特性

因为没有用户交互和 DOM 更新,所以响应性在服务端是不必要的。为了更好的性能,默认情况下响应性在 SSR 期间是禁用的。

每个SSR应用,只初始化一次,因此在处理全局状态的过程中,如果多个用户进行访问服务端的全局变量,会造成冲突,把一个用户的请求泄露给另一个用户

同一个应用模块会在多个服务器请求之间被复用,而我们的单例状态对象也一样。如果我们用单个用户特定的数据对共享的单例状态进行修改,那么这个状态可能会意外地泄露给另一个用户的请求。我们把这种情况称为跨请求状态污染。因此应用模块通常只在服务器启动时初始化一次。

推荐的解决方案是在每个请求中为整个应用创建一个全新的实例,包括 router 和全局 store。然后,我们使用应用层级的 provide 方法来提供共享状态,并将其注入到需要它的组件中,而不是直接在组件中将其导入。

// app.js (在服务端和客户端间共享)
import { createSSRApp } from 'vue'
import { createStore } from './store.js'
// 每次请求时调用
export function createApp() {
const app = createSSRApp(/* ... */)
// 对每个请求都创建新的 store 实例
const store = createStore(/* ... */)
// 提供应用级别的 store
app.provide('store', store)
// 也为激活过程暴露出 store
return { app, store }
}

因为没有任何动态更新,所以像 onMounted 或者 onUpdated 这样的生命周期钩子不会在 SSR 期间被调用,而只会在客户端运行。

有副作用的 setInterval 之类的定时器需要在 onMounted 中使用,否则定时器将永远存在。

不同的平台有不同的 API,但是你可以使用例如 node-fetch 在服务端和客户端使用相同的 fetch API。对于浏览器特有的 API,通常的方法是在仅客户端特有的生命周期钩子中惰性地访问它们,例如 onMounted

参考文章

文章名称地址
服务端渲染 (SSR)https://cn.vuejs.org/guide/scaling-up/ssr.html