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 |