# SSR
近些年来,SSR/SSG 由于其良好的首屏展现速度和SEO友好性逐渐成为主流的技术规范,但过去 Mpx 对 SSR 的支持不完善,使用 Mpx 开发的跨端页面一直无法享受到 SSR 带来的性能提升,在 2.9 版本中,我们对 Mpx 输出 web 流程进行了大量适配改造,解决了 SSR 中常见的内存泄漏、跨请求状态污染和数据预请求等问题,完整支持了基于 Vue 和 Pinia 的 SSR 技术方案。
# 配置使用 SSR
在 Vue SSR 项目中,我们一般需要提供 server entry 和 client entry 两个文件作为 webpack 的构建入口,然后通过 webpack 构建出 server bundle 和 client bundle。在用户访问页面时,在服务端使用 server bundle 渲染出 HTML 字符串,作为静态页面发送到客户端,然后在客户端使用 client bundle 通过水合(hydration)对静态页面进行激活,实现可交互效果,下图展示了 Vue SSR 的大致流程。
Mpx SSR 核心基于 Vue SSR 实现,大致流程思路与 Vue 一致,不过为了保持与小程序代码的兼容性,在配置使用上有一些改动差异,下面我们详细展开介绍:
# 构建server/client bundle
SSR项目中,我们需要分别构建出 server bundle 和 client bundle,对于不同环境的产物构建,我们需要进行不同的配置。
在 Vue 中,我们需要提供 entry-server.js
和 entry-client.js
两个文件分别作为 server 和 client 的构建入口,与 Vue 不同,在 Mpx 中我们通过编译处理与运行时增强生命周期实现了使用 app.mpx
作为统一构建入口,无需区分 server 和 client。
# 服务端构建配置
服务端配置中除了将 entry 制定为 app.mpx
及其它基础配置外,最重要的是安装 vue-server-renderer
包中提供的 server-plugin
插件,该插件能够构建产出 vue-ssr-server-bundle.json
文件供 renderer
后续消费。
// webpack.server.config.js
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(baseConfig, {
// 将 entry 指向项目的 app.mpx 文件
entry: './app.mpx',
// ...
plugins: [
// 产出 `vue-ssr-server-bundle.json`
new VueSSRServerPlugin()
]
})
更加详细的配置说明可参考 Vue SSR的服务端配置 (opens new window)
# 客户端构建配置
类似服务端构建配置,在客户端构建中我们需要使用 vue-server-renderer
包中 client-plugin
插件来帮助我们生成客户端环境的资源清单 vue-ssr-client-manifest.json
,并供 renderer
后续消费。
// webpack.client.config.js
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge(baseConfig, {
// 将 entry 指向项目的 app.mpx 文件
entry: './app.mpx',
// ...
plugins: [
// 产出 `vue-ssr-client-manifest.json`。
new VueSSRClientPlugin()
]
})
更加详细的配置说明可参考 Vue SSR的客户端配置 (opens new window)
# 准备页面模版
SSR 渲染中,我们创建 renderer
需要一个页面模板,简单的示例如下:
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
与 CSR 渲染模版不同,SSR 渲染模版中需要提供一个特殊的 <!--vue-ssr-outlet-->
注释,标记 SSR 渲染产物的插入位置,如使用 @mpxjs/cli
脚手架创建 SSR 项目,该模版已经内置于脚手架中。
# 集成启动 SSR 服务
当我们准备好页面模版和双端构建产物后,我们就可以创建 renderer
并与 Node 服务进行集成,启动 SSR 服务,下面以 express
为例:
//server.js
const app = require('express')()
const { createBundleRenderer } = require('vue-server-renderer')
// 通过 vue-server-renderer/server-plugin 生成的文件
const serverBundle = require('../dist/server/vue-ssr-server-bundle.json')
// 通过 vue-server-renderer/client-plugin 生成的文件
const clientManifest = require('../dist/client/vue-ssr-client-manifest.json')
// 页面模版文件
const template = require('fs').readFileSync('../src/index.template.html', 'utf-8')
// 创建 renderer 渲染器
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template,
clientManifest,
});
// 注册启动 SSR 服务
app.get('*', (req, res) => {
const context = { url: req.url }
renderer.renderToString(context, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(html);
})
})
app.listen(8080)
# SSR 生命周期
在 Mpx 2.9 的版本中,我们提供了三个全新用于 SSR 的生命周期,分别是onAppInit
、serverPrefetch
和 onSSRAppCreated
,以统一服务端与客户端的构建入口,下面展开介绍:
# onAppInit
该生命周期仅可在 App 中使用
在 SSR 中用户每发出一个请求,我们都会为其生成一个新的应用实例,onAppInit
生命周期会在应用创建 new Vue(...)
前被调用,其执行的返回结果会被合并到创建应用的 options
中
很常见的使用场景在于返回新的全局状态管理实例,Mpx 中提供了 @mpxjs/pinia
作为全局状态管理工具,我们可以在 onAppInit
中返回全新的 pinia
示例避免产生跨请求状态污染,示例如下:
// app.mpx
import mpx, { createApp } from '@mpxjs/core'
import { createPinia } from '@mpxjs/pinia'
createApp({
// ...
onAppInit () {
const pinia = createPinia()
return {
pinia
}
}
})
SSR 中仅支持使用
@mpxjs/pinia
作为状态管理工具,@mpxjs/store
暂不支持
# serverPrefetch
该生命周期可在 App/Page/Component 中使用,只在服务端渲染时执行
当我们需要在 SSR 使用数据预拉取时,可以使用这个生命周期进行,使用方法与 Vue 一致, 示例如下:
选项式 API:
import { createPage } from '@mpxjs/core'
import useStore from '../store/index'
createPage({
//...
serverPrefetch () {
const query = this.$route.query
const store = useStore(this.$pinia)
// return the promise from the action, fetch data and update state
return store.fetchData(query)
}
})
组合式 API:
import { onServerPrefetch, getCurrentInstance, createPage } from '@mpxjs/core'
import useStore from '../store/index'
createPage({
setup () {
const store = userStore()
onServerPrefetch(() => {
const query = getCurrentInstance().proxy.$route.query
// return the promise from the action, fetch data and update state
return store.fetchData(query)
})
}
})
关于数据预拉取更详细的说明可以查看这里 (opens new window)。
# onSSRAppCreated
该生命周期仅可在 App 中使用,只在服务端渲染时执行
在 Vue SSR 项目中,我们会在 entry-server.js
中导出一个工厂函数,在该函数中实现创建应用实例、路由匹配和状态同步等逻辑,并返回应用实例 app
。
在 Mpx SSR 中,我们将这部分逻辑整合在 onSSRAppCreated
中执行,该生命周期执行时用户可以从参数中获取应用实例 app
、路由实例 router
、数据管理实例 pinia
和 SSR 上下文 context
,在完成必要的操作后,该生命周期需要返回一个 resolve(app)
的 promise。
通常我们会在 onSSRAppCreated
中进行路由路径设置和数据预拉取后的状态同步工作,示例如下:
// app.mpx
createApp({
// ...,
onSSRAppCreated ({ pinia, router, app, context }) {
return new Promise((resolve, reject) => {
// 设置服务端路由路径
router.push(context.url)
router.onReady(() => {
// 应用完成渲染时执行
context.rendered = () => {
// 将服务端渲染后得到的 pinia.state 同步到 context.state 中,
// context.state 会被自动序列化并通过 `window.__INITIAL_STATE__`
// 注入到 HTML 中,并在客户端运行时再读取同步
context.state = pinia.state.value
}
// 返回应用程序实例
resolve(app)
}, reject)
})
}
})
上述示例代码等价于 Vue 中的 entry-server.js
// entry-server.js
import { createApp } from './app'
export default context => {
return new Promise((resolve, reject) => {
const { app, router, store } = createApp()
router.push(context.url)
router.onReady(() => {
// This `rendered` hook is called when the app has finished rendering
context.rendered = () => {
// After the app is rendered, our store is now
// filled with the state from our components.
// When we attach the state to the context, and the `template` option
// is used for the renderer, the state will automatically be
// serialized and injected into the HTML as `window.__INITIAL_STATE__`.
context.state = store.state
}
resolve(app)
}, reject)
})
}
如用户没有配置 onSSRAppCreated,框架内部会执行兜底逻辑,以保障 SSR 的正常运行。
# 其他注意事项
Mpx SSR 渲染支持 i18n 的功能,但为了防止内存泄漏,当前 i18n 实例不会随着每次请求而重新创建,这是由于 Vue2.x 版本插件机制的设计缺陷所造成的,因此在使用 i18n 进行 SSR 时可能会产生跨请求状态污染的问题,这个问题会在未来 Mpx 输出 web 切换为 Vue3 后完全解决。
在服务端渲染阶段,对于 global 全局对象访问修改,如__mpx, __mpxRouter, __mpxPinia 都可能导致全局状态污染,所以在服务端渲染阶段请尽量避免进行相关操作;对于存在全局访问修改的方法,如 getApp(), getCurrentPages() 等在服务端渲染中被调用时,会产生相关报错提示。
由于服务器无法收到 URL 中的 hash 信息,使用 SSR 时需要通过修改
mpx.config.webRouteConfig
将路由模式设置成history
模式。