注意事项#
服务端的响应性
Vue 的响应式系统主要用于追踪数据的变化,并在数据变化时自动更新 DOM。在客户端应用中,用户的交互、数据更新都会触发这种响应式机制,从而使页面保持最新状态。但在服务器端渲染(SSR)的过程中,应用仅仅是一次性生成 HTML 字符串,随后发送给浏览器;这期间根本没有用户交互,也没有实际的 DOM 更新操作。
如果在服务器端继续启用 Vue 的响应式系统,会产生一些不必要的性能开销。
为了提高 SSR 的性能和减少不必要的资源消耗,Vue 默认在服务端渲染期间禁用了响应式转换。这样,在服务端渲染时,数据仅作为静态的状态来使用,不会再被包裹为响应式对象,而是直接用于生成最终的 HTML 字符串。
例如:
const app = Vue.createSSRApp({ data() { return { count: 1 } }, template: `<div>{{ count }}</div>`,})最终生成的结构:
<div>1</div>组件生命周期钩子
由于服务端渲染只发生一次,且渲染完后不会有数据更新,也没有真实的 DOM,因此大部分与“动态交互”或“DOM 操作”相关的生命周期钩子(onMounted 或 onUpdated)不会在 SSR 期间被调用,而只会在客户端运行。
例如:
<template> <div>App页面</div></template>
<script setup>console.log('created')onMounted(() => { console.log('onMounted')})</script>这就带来一个问题:如果你在组件的根作用域(比如在 setup() 或 <script setup> 中直接写一些副作用代码)执行了像 setInterval 这样的定时器逻辑,那么在 SSR 期间,这段代码也会执行,并且因为 SSR 渲染结束后不会调用任何“卸载”钩子(比如 onBeforeUnmount 或 onUnmounted),导致定时器无法被清理,从而可能引起内存泄漏。
<template> <div>{{ count }}</div></template>
<script setup>const count = ref(0)// 该定时器代码由于实在根作用域下,因此在服务器端也会执行// 但是清除计时器的操作实在钩子方法里面,而钩子方法仅在客户端才会执行const timer = setInterval(() => { count.value++ console.log('计时器运行中', count.value)}, 1000)
// 客户端卸载时清除定时器onUnmounted(() => { clearInterval(timer)})</script>最佳做法是将需要产生副作用的代码放到 onMounted 钩子中。因为 onMounted 只会在浏览器中挂载真实 DOM 后被调用,而在 SSR 中根本不会执行。
<template> <div>{{ count }}</div></template>
<script setup>const count = ref(0)let timer = null// 现在启动计时器和清除计时器都只在客户端才会执行onMounted(() => { timer = setInterval(() => { count.value++ console.log('计时器运行中', count.value) }, 1000)})
onUnmounted(() => { clearInterval(timer)})</script>访问平台特有API
通用代码不能访问平台特有的 API,如果你的代码直接使用了浏览器特有的全局变量,比如 window 或 document,他们会在 Node.js 运行时报错,反过来也一样。例如:
// 错误示例:直接在通用代码中访问浏览器特有的 APIif (window.innerWidth > 800) { // 执行某些操作}更好的方式是将对平台特有 API 的访问封装在特定的生命周期钩子中,确保这些代码仅在客户端执行。例如,可以将对 window 的访问放在 onMounted 钩子中:
import { onMounted } from 'vue'
onMounted(() => { if (window.innerWidth > 800) { // 执行某些操作 }})这样 window 的访问仅在客户端挂载后执行,避免了在服务器端渲染期间的错误。
对于在服务器和客户端之间共享,但使用了不同的平台 API 的任务,建议将平台特定的实现封装在一个通用的 API 中,或者使用能够在不同平台上运行的库。
举个例子,现在需要发送请求,平时都是使用 fetch,但是 fetch 是属于浏览器环境特有的 API,SSR 的时候会报错。更好的解决方案是使用 node-fetch(第三方库,封装了通用代码,浏览器环境和Node环境都可以使用)
跨请求状态污染
在 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 }}像 Pinia 这样的状态管理库在设计时就考虑到了这一点。
export function createRouter() { // 该方法就是返回一个新的router实例 return _createRouter({ history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(), routes, })}
// createApp方法,每一次请求过来,会创建一系列新的实例export function createApp() { const app = createSSRApp(App) const router = createRouter() const pinia = createPinia()
// ...
return { app, router, pinia }}水合不匹配
Hydration completed but contains mismatches
简单来讲就是服务器端渲染出来的 html 结构和客户端激活时得到的 html 结构不一致导致的。
最常见的原因有以下几种:
-
结构不符合规范
<p><div>123</div></p>服务器端遇到上面的结构,会直接渲染成对应的字符串。但是在浏览器环境存在纠错机制
<p></p><div>123</div><p></p> -
数据包含随机值
- 如果是在 Nuxt 中可以使用 ClientOnly 组件来解决
- 如果是原生 SSR 环境,那么可以将产生随机数的逻辑放到 mounted 钩子方法里面
- 也可以使用一些第三方库,例如 seedrandom
-
服务端和客户端的时区不一致
- 解决方案就是将和时区相关的处理放到客户端去操作
-EOF-