什么是服务器端渲染 (SSR)?
Vue.js 是构建客户端应用程序的框架。然而能够将组件在服务器端渲染为 HTML 字符串,将它们间接发送到浏览器,最初将这些动态标记"激活"为客户端上齐全可交互的应用程序。
为什么应用服务器端渲染 (SSR)?
- 更好的 SEO,因为搜索引擎爬虫抓取工具能够间接查看齐全渲染的页面。
- 更快的响应工夫,无需期待 JavaScript 加载
衡量?
- 开发条件所限
- 波及构建设置和部署的更多要求
- 更多的服务器端负载
根本应用
装置
npm install vue vue-server-renderer --S
渲染
const Vue = require('vue')const { createRenderer } = require('vue-server-renderer')const app = new Vue({ template: `<div>Hello World</div>`})createRenderer().renderToString(app, (err, html) => { console.log(html);})
集成服务器
装置 Express
npm install express --save
const Vue = require('vue')const { createRenderer } = require('vue-server-renderer')const express = require('express')const server = express()const app = new Vue({ template: `<div>Hello 小王</div>`})server.get('/', (req, res) => { createRenderer().renderToString(app, (err, html) => { res.end(html) })})server.listen('3000')
应用页面模板
在下面的案例中咱们会发现中文显示乱码,起因是因为编译后的 HTML 文件短少编码等阐明
在这个示例中,咱们必须用一个额定的 HTML 页面包裹容器,来包裹生成的 HTML 标记,为了简化这些,你能够间接在创立 renderer 时提供一个页面模板。咱们会将页面模板放在特有的文件中,例如 index.template.html
<!DOCTYPE html><html lang="en"> <head> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta charset="utf-8" /> <!-- 应用双花括号(double-mustache)进行 HTML 本义插值(HTML-escaped interpolation) --> <title>{{ title }}</title> <!-- 应用三花括号(triple-mustache)进行 HTML 不本义插值(non-HTML-escaped interpolation) --> </head> <body> <!--vue-ssr-outlet--> </body></html>
留神 <!--vue-ssr-outlet-->
正文,这里将是应用程序 HTML 标记注入的中央。
模板插值:咱们能够通过传入一个"渲染上下文对象",作为 renderToString 函数的第二个参数,来提供插值数据
const Vue = require('vue')const fs = require('fs')const { createRenderer } = require('vue-server-renderer')const express = require('express')const server = express()const app = new Vue({ template: `<div>Hello 小王</div>`})const context = { title: 'hello'}const renderer = createRenderer({ template: fs.readFileSync('./index.template.html', 'utf-8')})server.get('/', (req, res) => { renderer.renderToString(app, context ,(err, html) => { res.end(html) })})server.listen('3000')
通用模板
应用 webpack 的源码构造
咱们须要应用 webpack 来打包咱们的 Vue 应用程序,因为
- 通常 Vue 应用程序是由 webpack 和 vue-loader 构建,并且许多 webpack 特定性能不能间接在 Node.js 中运行
- 只管 Node.js 最新版本可能齐全反对 ES2015 个性,咱们还是须要转译客户端代码以适应老版浏览器。
所以对于客户端应用程序和服务器应用程序,咱们都要应用 webpack 打包,服务器须要「服务器 bundle」而后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合动态标记。
一个根本的我的项目
build # webpack 构建文件├── setup-dev-server.js # develop 模式下 render 文件├── webpack.base.config.js # 通用打包配置文件├── webpack.client.config.js # 客户端打包配置文件└── webpack.server.config.js # 服务端打包配置文件src├── pages│ ├── Foo.vue│ ├── Bar.vue│ └── Baz.vue├── App.vue├── app.js # 通用 entry(universal entry)├── entry-client.js # 仅运行于浏览器└── entry-server.js # 仅运行于服务器└── server.js # 启动文件
入口文件
- app.js:是咱们应用程序的「通用 entry」。在纯客户端应用程序中,咱们将在此文件中创立根 Vue 实例,并间接挂载到 DOM。然而,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。app.js 简略地应用 export 导出一个 createApp 函数:
import Vue from 'vue'import App from './App.vue'// 导出一个工厂函数,用于创立新的// 应用程序、router 和 store 实例export function createApp () { const app = new Vue({ // 根实例简略的渲染应用程序组件。 render: h => h(App) }) return { app }}
- entry-client.js:客户端 entry 只需创立应用程序,并且将其挂载到 DOM 中
import { createApp } from './app'// 客户端特定疏导逻辑……const { app } = createApp()// 这里假设 App.vue 模板中根元素具备 `id="app"`app.$mount('#app')
- entry-server.js:服务器 entry 应用 default export 导出函数,并在每次渲染中反复调用此函数。此时,除了创立和返回应用程序实例之外,它不会做太多事件。然而稍后咱们将在此执行服务器端路由匹配 (server-side route matching) 和数据预取逻辑 (data pre-fetching logic)。
import { createApp } from './app'export default context => { const { app } = createApp() return app}
webpack 构建配置
- webpack.base.config.js:通用 webpack 打包配置文件,定义打包模式和进口文件门路,假猪 loader 打包各类文件,应用了 vueLoaderPlugin
const path = require('path')const vueLoaderPlugin = require('vue-loader/lib/plugin')const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')const resolve = file => path.resolve(__dirname, file)const isProd = process.env.NODE_ENV === 'production'module.exports = { mode: isProd ? 'production' : 'development', output: { path: resolve('../dist'), publicPath: '/dist', filename: '[name].[chunkhash].js' }, resolve: { alias: { '@': resolve('../src') }, extensions: ['.js','.vue','.json'] }, devtool: 'source-map', module: { rules: [ { test: /\.(png|jpg|gif)$/i, use: [ { loader: 'url-loader', options: { limit: 8192, }, } ] }, // 解决字体资源 { test: /\.(woff|woff2|eot|ttf|otf)$/, use: [ 'file-loader', ], }, // 解决 .vue 资源 { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.css$/, use: [ 'vue-style-loader', 'css-loader' ] }, ] }, plugins: [ new vueLoaderPlugin(), new FriendlyErrorsWebpackPlugin() ]}
- webpack.client.config.js:客户端 webpack 打包配置文件,定义客户端打包入口,ES6 的语法转换,应用了 VueSSRClientPlugin
/** * 客户端打包配置 */const { merge } = require('webpack-merge');const baseConfig = require('./webpack.base.config.js');const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');const { CleanWebpackPlugin } = require('clean-webpack-plugin')module.exports = merge(baseConfig, { entry: { app: './src/entry-client.js', }, module: { rules: [ // ES6 转 ES5 { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], cacheDirectory: true, plugins: ['@babel/plugin-transform-runtime'], }, }, }, ], }, // 重要信息:这将 webpack 运行时拆散到一个疏导 chunk 中, // 以便能够在之后正确注入异步 chunk。 optimization: { splitChunks: { name: 'manifest', minChunks: Infinity, }, }, plugins: [ new CleanWebpackPlugin(), // 此插件在输入目录中生成 `vue-ssr-client-manifest.json`。 new VueSSRClientPlugin(), ],});
- webpack.server.config.js:服务端 webpack 打包配置文件,定义服务端打包入口,告知
vue-loader
输送面向服务器代码,应用了 VueSSRClientPlugin
/** * 服务端打包配置 */const { merge } = require('webpack-merge');const nodeExternals = require('webpack-node-externals');const baseConfig = require('./webpack.base.config.js');const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');module.exports = merge(baseConfig, { // 将 entry 指向应用程序的 server entry 文件 entry: './src/entry-server.js', // 这容许 webpack 以 Node 实用形式解决模块加载 // 并且还会在编译 Vue 组件时, // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。 target: 'node', output: { filename: 'server-bundle.js', // 此处告知 server bundle 应用 Node 格调导出模块(Node-style exports) libraryTarget: 'commonjs2', }, // 不打包 node_modules 第三方包,而是保留 require 形式间接加载 externals: [ nodeExternals({ // 白名单中的资源仍然失常打包 allowlist: [/\.css$/], }), ], plugins: [ // 这是将服务器的整个输入构建为单个 JSON 文件的插件。 // 默认文件名为 `vue-ssr-server-bundle.json` new VueSSRServerPlugin(), ],});
启动利用
装置开发依赖
包 | 阐明 |
---|---|
webpack webpack-cli | webpack 外围包 |
webpack-merge | webpack 配置信息合并工具 |
webpack-node-externals | 排除 webpack 中的 Node 模块 |
friendly-errors-webpack-plugin | 敌对的 webpack 谬误提醒 |
@babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader | Babel 相干工具 |
vue-loader vue-template-compiler | 解决 .vue 资源 |
file-loader css-loader url-loader | 解决资源文件 |
cross-env | 通过 npm scripts 设置跨平台环境变量 |
批改启动文件 server.js
const express = require('express')const { createBundleRenderer } = require('vue-server-renderer')const server = express()server.use('/dist', express.static('./dist'))const template = require('fs').readFileSync('./index.template.html', 'utf-8');const serverBundle = require('./dist/vue-ssr-server-bundle.json')const clientManifest = require('./dist/vue-ssr-client-manifest.json')renderer = createBundleRenderer(serverBundle, { template, clientManifest })const render = async (req, res) => { try { const html = await renderer.renderToString({ title: '', meta: ` <meta name="description" content="vue srr demo"> `, url: req.url, }) res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) } catch (err) { res.status(500).end('Internal Server Error.') }}server.get('*', render)server.listen(8080)
执行费打包命令
npm run build:clientnpm run build:server
打包胜利后我即可在 dist 文件下看到打包后的后果,运行 server.js 文件能够查看页面
production 与 develop 环境拆散
下面尽管咱们曾经能够胜利的运行,但其中还存在一些问题
- 每次写完代码,都要从新打包构建
- 重新启动 Web 服务
所以上面咱们来实现我的项目中的开发模式构建,也就是咱们心愿可能实现:
- 写完代码,主动构建
- 主动重启 Web 服务
- 主动刷新页面内容
思路
node 命令中应用 cross-env 携带 NODE_ENV 变量,辨别执行的环境
生产模式
- npm run build 构建
- node server.js 启动
开发模式:
- 监督代码变动,热更新
在 package.json 文件中退出命令脚本
"scripts": { "build": "npm run build:client && npm run build:server", "start": "cross-env NODE_ENV=production node server.js", "dev": "node server.js" },
装置扩大包
包 | 阐明 |
---|---|
chokidar | 监听本地文件的改变 |
webpack-dev-middlewar | 中间件 |
webpack-hot-middleware | 热更新 |
批改启动脚本,生产环境下间接应用打包好的文件,开发环境下须要期待 renderer 函数的生成
const express = require('express')const setupDevServer = require('./build/setup-dev-server')const { createBundleRenderer } = require('vue-server-renderer')const server = express()server.use('/dist', express.static('./dist'))const isProd = process.env.NODE_ENV === 'production'let onReady, rendererif (isProd) { // 生产模式,间接基于已构建好的包创立渲染器 const template = require('fs').readFileSync('./index.template.html', 'utf-8'); const serverBundle = require('./dist/vue-ssr-server-bundle.json') const clientManifest = require('./dist/vue-ssr-client-manifest.json') renderer = createBundleRenderer(serverBundle, { template, clientManifest })} else { // 开发模式 打包构建(客户端 + 服务端) -> 创立渲染器 onReady = setupDevServer(server, (serverBundle, template, clientManifest) => { renderer = createBundleRenderer(serverBundle, { template, clientManifest }) })}const render = async (req, res) => { try { if (!isProd) { await onReady } /** * 这里无需传入一个应用程序,因为在执行 bundle 时曾经主动创立过。 * bundle renderer 在调用 renderToString 时,它将主动执行「由 bundle 创立的应用程序实例」所导出的函数(传入上下文作为参数),而后渲染它。 */ const html = await renderer.renderToString({ title: '', meta: ` <meta name="description" content="vue srr demo"> `, }) res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) } catch (err) { res.status(500).end('Internal Server Error.') }}server.get('*', render)server.listen(8080)
开发环境下 setup-dev-server.js 生成 renderer 函数
const fs = require('fs')const path = require('path')const chokidar = require('chokidar')const webpack = require('webpack')const devMiddleware = require('webpack-dev-middleware')const hotMiddleware = require('webpack-hot-middleware')const serverConfig = require('./webpack.server.config')const clientConfig = require('./webpack.client.config')const resolve = file => path.resolve(__dirname, file)const templatePath = path.resolve(__dirname, '../index.template.html')module.exports = (server, callback) => { let ready, template, serverBundle, clientManifest const onReady = new Promise(r => ready = r) const update = () => { if (template && serverBundle && clientManifest) { ready() callback(serverBundle, template, clientManifest) } } // 监督构建 template template = fs.readFileSync(templatePath, 'utf-8') update() chokidar.watch(templatePath).on('change', () => { template = fs.readFileSync(templatePath, 'utf-8') update() }) // 监督构建 serverBundle const serverCompiler = webpack(serverConfig) const serverDevMiddleware = devMiddleware(serverCompiler) serverCompiler.hooks.done.tap('server', () => { serverBundle = JSON.parse(serverDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8')) update() }) // 监督构建 clientManifest clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin()) clientConfig.entry.app = [ 'webpack-hot-middleware/client?quiet=true&reload=true', // 和服务端交互解决热更新一个客户端脚本 clientConfig.entry.app ] const clientCompiler = webpack(clientConfig) const clientDevMiddleware = devMiddleware(clientCompiler, { publicPath: clientConfig.output.publicPath, }) clientCompiler.hooks.done.tap('client', () => { clientManifest = JSON.parse(clientDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8')) update() }) // clientDevMiddleware 挂载到 Express 服务中,提供对其外部内存中数据的拜访 server.use(clientDevMiddleware) server.use(hotMiddleware(clientCompiler, { log: false // 敞开它自身的日志输入 })) return onReady}
路由治理
装置 vue-router
创立 router.js 文件
import Vue from 'vue'import VueRouter from 'vue-router'import Home from '@/pages/Home'Vue.use(VueRouter)export const createRouter = () => { const router = new VueRouter({ mode: 'history', // 兼容前后端 routes: [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', component: () => import('@/pages/About') }, { path: '/posts', name: 'post-list', component: () => import('@/pages/Posts') }, { path: '*', name: 'error', component: () => import('@/pages/404') } ] }) return router}
更新 app.js
/** * 通用入口 */ import Vue from 'vue' import App from './App.vue' import { createRouter } from './router' Vue.use(VueMeta) // 导出一个工厂函数,用于创立新的 // 应用程序、router 和 store 实例 export function createApp () { const router = createRouter() const app = new Vue({ router, render: h => h(App) }) return { app, router } }
在 entry-server.js 中实现服务器端路由逻辑
/** * 服务端 */ import { createApp } from './app' export default async context => { const { app, router } = createApp() router.push(context.url) await new Promise(router.onReady.bind(router)) return app }
在 entry-client.js 中实现客户器端路由逻辑
/** * 客户端 */ import { createApp } from './app' // 客户端特定疏导逻辑…… const { app, router } = createApp() // 这里假设 App.vue 模板中根元素具备 `id="app"` router.onReady(() => { app.$mount('#app') })
批改 App.vue 文件
<template> <div id="app"> <h1>{{ message }}</h1> <div><input type="" v-model="message"></div> <button @click="onClick">onClick</button> <ul> <li> <router-link to="/">Home</router-link> </li> <li> <router-link to="/about">About</router-link> </li> <li> <router-link to="/posts">Posts</router-link> </li> </ul> <!-- 路由进口 --> <router-view/> </div></template><script>export default { name: 'App', data () { return { message: 'vue-ssr' } }, methods: { onClick () { console.log('Hello World!') } }}</script>
启动胜利,拜访页面咱们会发现除了 app 主资源外,其它的资源也被下载下来了,但咱们的路由配置是动静引入,也就是当咱们拜访时才应该加载,而这里却是一上来就加载了。
起因是在页面的头部中的带有 preload 和 prefetch 的 link 标签。
咱们冀望客户端 JavaScript 脚本尽快加载尽早的接管服务端渲染的内容,让其领有动静交互能力,然而
如果你把 script 标签放到这里的话,浏览器会去下载它,而后执行外面的代码,这个过程会阻塞页面的
渲染。
所以看到真正的 script 标签是在页面的底部的。而这里只是通知浏览器能够去预加载这个资源。然而不
要执行外面的代码,也不要影响网页的失常渲染。直到遇到真正的 script 标签加载该资源的时候才会去
执行外面的代码,这个时候可能曾经预加载好了,间接应用就能够了,如果没有加载好,也不会造成重
复加载,所以不必放心这个问题。
而 prefetch 资源是加载下一个页面可能用到的资源,浏览器会在闲暇的时候对其进行加载,所以它并
不肯定会把资源加载进去,而 preload 肯定会预加载。所以你能够看到当咱们去拜访 about 页面的时
候,它的资源是通过 prefetch 预取过去的,进步了客户端页面导航的响应速度。
治理页面 Head
页面中的 body 是动静渲染进去的,然而页面的 head 是写死的,应用 vue-meta
Vue Meta 是一个反对 SSR 的第三方 Vue.js 插件,可让你轻松的实现不同页面的 head 内容治理。
<template>...</template><script> export default { metaInfo: { title: 'My Example App', titleTemplate: '%s - Yay!', htmlAttrs: { lang: 'en', amp: true } } }</script>
装置 npm i vue-meta -S
在通用入口 app.js 中通过插件的形式将 vue-meta 注册到 Vue 中。
import VueMeta from 'vue-meta' Vue.use(VueMeta) Vue.mixin({ metaInfo: { titleTemplate: '%s - vue-ssr' } })
而后在服务端渲染 entry-server.js 文件中适配 vue-meta:
/** * 服务端 */ import { createApp } from './app' export default async context => { const { app, router } = createApp() const meta = app.$meta() router.push(context.url) context.meta = meta await new Promise(router.onReady.bind(router)) return app }
最初在模板页面 index.template.html 中注入 meta 信息
<head> {{{ meta.inject().meta.text() }}} {{{ meta.inject().title.text() }}}</head>
数据预取
假如需要为渲染文章列表
- 服务端渲染:在服务端的状况下,这个需要很简略,无非是发送申请拿到数据渲染
客户端渲染:在客户端会有以下问题
- 只反对 beforeCreate 和 created 生命周期
- 不会期待 beforeCreate 和 created 生命周期中的异步操作
- 不反对响应式数据,也就是说拿到数据也无奈动静渲染到页面
官网文档给出的解决办法的外围思路就是把在服务端渲染期间获取的数据存储到 Vuex 容器中,
而后把容器中的数据同步到客户端,这样就放弃了前后端渲染的数据状态同步,防止了客户端从新渲染
的问题。
装置 Vuex :npm i vuex
创立 Vuex 容器 store/index.js
import Vue from 'vue'import Vuex from 'vuex'import axios from 'axios'Vue.use(Vuex)export const createStore = () => { return new Vuex.Store({ state: () => ({ posts: [] }), mutations: { setPosts (state, data) { state.posts = data } }, actions: { // 在服务端渲染期间务必让 action 返回一个 Promise async getPosts ({ commit }) { // return new Promise() const { data } = await axios.get('https://cnodejs.org/api/v1/topics') commit('setPosts', data.data) } } })}
在通用利用入口中将 Vuex 容器挂载到 Vue 根实例
/** * 通用入口 */ import Vue from 'vue' import App from './App.vue' import { createRouter } from './router' import { createStore } from './store' import VueMeta from 'vue-meta' Vue.use(VueMeta) Vue.mixin({ metaInfo: { titleTemplate: '%s - vue-ssr' } }) // 导出一个工厂函数,用于创立新的 // 应用程序、router 和 store 实例 export function createApp () { const router = createRouter() const store = createStore() const app = new Vue({ router, store, render: h => h(App) }) return { app, router, store } }
在服务端渲染利用入口中将容器状态序列化到页面中,从而防止
两个端状态不统一导致客户端从新渲染的问题。
- 将容器中的 state 转为 JSON 格局字符串
- 生成代码:
window.__INITIAL__STATE = store
语句插入模板页面中 - 客户端通过
window.__INITIAL__STATE
获取该数据
entry-server.js
context.rendered = () => { // Renderer 会把 context.state 数据对象内联到页面模板中 // 最终发送给客户端的页面中会蕴含一段脚本:window.__INITIAL_STATE__ = context.state // 客户端就要把页面中的 window.__INITIAL_STATE__ 拿进去填充到客户端 store 容器中 context.state = store.state }
entry-client.js 在客户端渲染入口中把服务端传递过去的状态数据填充到客户端 Vuex 容器中
if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) }
页面缓存
尽管 Vue 的服务器端渲染 (SSR) 相当疾速,然而因为创立组件实例和虚构 DOM 节点的开销,无奈与纯基于字符串拼接的模板的性能相当。在 SSR 性能至关重要的状况下,明智地利用缓存策略,能够极大改善响应工夫并缩小服务器负载。
页面级别缓存
能够利用名为 micro-caching 的缓存策略,来大幅度提高利用程序处理高流量的能力。但并非所有页面都适宜利用 micro-caching 缓存策略,咱们能够将资源分为三类:
- 动态资源:如 js 、 css 、 images 等
- 用户特定的动静资源:不同的用户拜访雷同的资源会失去不同的内容。
- 用户无关的动静资源:任何用户拜访该资源都会失去雷同的内容,但该内容可能在任意工夫产生变
化,如博客文章
装置依赖
npm i lru-cache -S
server.js
const express = require('express')const setupDevServer = require('./build/setup-dev-server')const { createBundleRenderer } = require('vue-server-renderer')const LRU = require('lru-cache')const server = express()server.use('/dist', express.static('./dist'))const cache = new LRU({ max: 100, maxAge: 10000 // Important: entries expires after 1 second.})const isCacheable = req =>{ console.log(req.url) if (req.url === '/posts') { return true }}const isProd = process.env.NODE_ENV === 'production'let onReady, rendererif (isProd) { // 生产模式,间接基于已构建好的包创立渲染器 const template = require('fs').readFileSync('./index.template.html', 'utf-8'); const serverBundle = require('./dist/vue-ssr-server-bundle.json') const clientManifest = require('./dist/vue-ssr-client-manifest.json') renderer = createBundleRenderer(serverBundle, { template, clientManifest })} else { // 开发模式 打包构建(客户端 + 服务端) -> 创立渲染器 onReady = setupDevServer(server, (serverBundle, template, clientManifest) => { renderer = createBundleRenderer(serverBundle, { template, clientManifest }) })}const render = async (req, res) => { try { const cacheable = isCacheable(req) if (cacheable) { const html = cache.get(req.url) if (html) { return res.end(html) } } if (!isProd) { await onReady } /** * 这里无需传入一个应用程序,因为在执行 bundle 时曾经主动创立过。 * bundle renderer 在调用 renderToString 时,它将主动执行「由 bundle 创立的应用程序实例」所导出的函数(传入上下文作为参数),而后渲染它。 */ const html = await renderer.renderToString({ title: '', meta: ` <meta name="description" content="vue srr demo"> `, url: req.url, }) res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) if (cacheable) { cache.set(req.url, html) } } catch (err) { res.status(500).end('Internal Server Error.') }}server.get('*', render)server.listen(8080)
组件级别缓存
vue-server-renderer 内置反对组件级别缓存。要启用组件级别缓存,须要在创立 renderer 时提供具体缓存实现形式。
const LRU = require('lru-cache')const renderer = createRenderer({ cache: LRU({ max: 10000, maxAge: ... })})
而后,你能够通过实现 serverCacheKey 函数来缓存组件。
export default { name: 'item', // 必填选项 props: ['item'], serverCacheKey: props => props.item.id, render (h) { return h('div', this.item.id) }}