Vue SSR实战小练
思想
- 开发环境:
- 在webpack dev与pro的前端打包构建基础上, 添加webpack server compiler的服务,其为单独创建的node服务,用于渲染html代码并返回给客户端。(其他JS...则仍旧交由webpack dev server来构建)
- 所以在获取html后要再自行将dev客户端渲染的js加入到html中
- 在生产环境则不需如此:
- 由dev与server打包好的文件,将其组合
- 客户端则访问node服务来获取文件
基本用法
- 安装:
npm install vue vue-server-renderer -save
| 注意问题 | |
| vue-server-renderer与vue的版本必须一致 | |
| vue-server-renderer依赖node原生模块,只能在Node.js中使用 |
- 渲染一个Vue实例
`` // ①创建Vue实例 const Vue = require('vue') const app = new Vue({ template:...` }) // ②创建一个renderer const renderer = require('vue-server-renderer').createRenderer()
// ③将Vue实例渲染为真实html renderer.renderToString(app, (err, html) => { if (err) throw err console.log(html) // =>
// *其中在2.5+版本,如果没有传入回调函数,则返回一个Promise对象: renderer.renderToString(app).then(html => { console.log(html) }).catch(err => { console.error(err) })
3. 在Node服务器中使用
const Vue = require('vue') const server = require('express')() const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
const app = new Vue({
data: {
url: req.url
},
template: <div>访问的 URL 是: {{ url }}</div>
})
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>)
})
})
server.listen(8080)
4. Renderer页面模板:
- 当我们在将Vue应用程序渲染时,renderer只会从应用程序中生成HTML,为了简化一些程序,可以在创建renderer时提供一个页面模板,我们可以将模板放在特有的文件中,如`index.template.html`,其中`<!--vue-ssr-outlet-->`时将应用程序的HTML注入的地方。
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
// 配置文件中,读取与传输文件到renderer
const renderer = createRenderer({
template: require('fs').readFileSync('./index.template.html', 'utf-8')
})
renderer.renderToString(app, (err, html) => {
console.log(html)
})
```
- 页面模板还支持简单的插值,我们可以通过传入一个上下文对象ctx作为rendererToString函数的第二个参数,来提供插值数据:(这个ctx可以自定义,也可以与VueApp共享其组件实例数据)
```
<html>
<head>
<!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) -->
<title>{{ title }}</title>
<!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) -->
{{{ meta }}}
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
// Renderer
const context = {
title: 'hello',
meta: `
<meta ...>
<meta ...>
`
}
renderer.renderToString(app, context, (err, html) => {
// 页面 title 将会是 "Hello"
// meta 标签也会注入
})
```
编写通用代码
- 组件声明周期钩子函数:由于没有了动态更新,所有的生命周期钩子函数中,只有
beforeCreate与created会在SSR的过程中被调用。也就是说其他任何的生命周期钩子函数中的代码(mounted...)将只会在客户端执行。 --> 即,我们需要进行服务端渲染的代码只能放再beforeCreate与created中。 - 访问特定平台API:通用代码不能接受特定平台的API,比如在代码中使用了
window,document这种仅浏览器可用的全局变量,NodeJS执行时将会抛出错误。 - 自定义指令:大多数自定义指令是直接操作DOM的,因此会在SSR中导致错误,可使用两种方法解决:
- ①使用组件作为抽象机制,并运行在虚拟DOM层级,即使用Vue的render函数进行组件渲染
- ②如果有一个自定义指令,但不是很容易被替换成组件,则可在创建服务器renderer时使用directives选项提供的服务器端版本server-side-version
源码结构
避免单例模式:一般在编写客户端代码时,习惯于直接每次在新的上下文代码中仅行取值,但NodeJS服务器是一个长期运行的进程。当我们的代码进入该进程时,他将进行一次取值并留存在内存中。这意味着如果我们创建一个单例对象,他将会在每个传入的请求中共享它。在
基本用法 3中,可以看到,在服务端为每个请求创建一个新的根Vue实例,但实质上,这个Vue实例将会被存入内存成为共享的。因此容易导致交叉请求状态的污染。- 因此,我们应该直接创建一个应用程序的实例,而是暴露一个可以重复执行的工厂函数来为每隔请求创建新的应用程序实例:
此时服务器代码变为:// app.js const Vue = require('vue') module.exports = function createApp (ctx) { return new Vue({ data: { url: ctx.url }, template: `<div>您访问的url为:{{ url }}</div>` }) }同样的规则也适用于router、store与event Bus实例。都需要暴露一个可重复使用的工厂函数来为其创建新的实例,并从根实例中注入。// server.js const createApp = require('./app') server.get('*', (req, res) => { const ctx = { url: req.url } const app = createApp(ctx) // 将Vue实例渲染成html renderer.renderToString(app, (err, html) => { res.end(html) }) })
- 因此,我们应该直接创建一个应用程序的实例,而是暴露一个可以重复执行的工厂函数来为每隔请求创建新的应用程序实例:
构建步骤:我们需要使用
webpack来打包VueApp- ①通常VueApp是由webpack与vue-loader构建的,并许多webpack特定功能无法直接在NodeJS中运行(例如各种loader)
- ②尽管NodeJS最新版本支持ES2015特性,但我们还是需要对客户端代码进行转译以适应老版本浏览器

使用了webpack的源码结构:
- 项目基本目录结构:
src ├── components │ ├── Foo.vue │ ├── Bar.vue │ └── Baz.vue ├── App.vue ├── app.js # 通用 entry(universal entry) ├── entry-client.js # 仅运行于浏览器 └── entry-server.js # 仅运行于服务器 app.js:这是我们程序的通用入口(见编写通用代码)。在纯客户端App中,我们将在此文件中创建Vue实例,并挂载到DOM上,但对于服务器端渲染(SSR)来说,责任将转移到纯客户端entry文件,而app.js则简单的使用export导出一个createApp函数:import Vue from 'vue' import App from './App.vue' // 导出工厂函数用于创建新的app、router与store实例 export function createApp () { const app = new Vue({ render: h => h(App) }) return { app } }entry-client.js:客户端入口,只需要创建App,并将其挂载到DOM上就行:import { createApp } from './app' //客户端特定引导逻辑... //创建实例并挂载 const { app } = createApp() app.$mount('#app')
- 项目基本目录结构:
路由与代码分割
- 使用
vue-router的路由:在服务器代码中我们使用了*处理任意url访问,这允许我们将访问的url传递到Vue中,对客户端与服务器使用相同的路由配置。为此建议直接使用官方的vue-router,同样,在创建router时,也要与创建vue实例一样导出一个create函数: ``` // 1、router.js import Vue from 'vue' import Router from 'vue-router'
Vue.use(Router)
export function createRouter () { return new Router({ mode: 'history', routes: [ // ... ] }) }
// 2、然后在app.js中更新 import Vue from 'vue' import App from './App.vue' import { createRouter } from './router'
export function createApp () { // 创建 router 实例 const router = createRouter()
const app = new Vue({ // 注入 router 到根 Vue 实例 router, render: h => h(App) })
// 返回 app 和 router return { app, router } }
// 3、接下来需要在entry-server.js中实现服务器端路由逻辑 import { createApp } from './app'
export default context => { // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise, // 以便服务器能够等待所有的内容在渲染前, // 就已经准备就绪。 return new Promise((resolve, reject) => { const { app, router } = createApp()
// 设置服务器端 router 的位置
router.push(context.url)
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// 匹配不到的路由,执行 reject 函数,并返回 404
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// Promise 应该 resolve 应用程序实例,以便它可以渲染
resolve(app)
}, reject)
}) }
假设服务器bundle已经完成构建,服务器用法看起来如下
// server.js const createApp = require('/path/to/built-server-bundle.js') server.get('*', (req, res) => { const ctx = { url: req.url } createApp(ctx).then(app, (err, html) => { if (err) { if (err.code === 404) res.status(404).end('Page not found') else res.status(500).end('Internal Server Error') } else { res.end(html) } }) })
2. 代码分割:应用程序的代码分割或惰性加载,有助于减少浏览器在初始环境中下载的资源体积,可以改善大体积bundle的可交互时间。Vue提供异步组件作为第一类的概念,与webpack所支持的动态导入结合:
// 将其改为 import Foo from './Foo.vue'
//here: const Foo = () => import('./Foo.vue')
*值得注意的是,需要在挂载app之前调用`router.onReady`因为路由器必须要提前一步解析路由配置中的异步组件,才能正确调用可能存在的路由钩子。*
在客户端中进行更新:
// entry-client.js import { createApp } from './app' const { app, router } = createApp()
router.onReady(() => { app.$mount('#app') })
// 异步路由组件路由配置router.js import Vue from 'vue' import Router from 'vue-router'
Vue.use(Router)
export function createRouter () { return new Router({ mode: 'history', routes: [ { path: '/', component: () => import('./components/Home.vue') }, { path: '/item/:id', component: () => import('./components/Item.vue') } ] }) }
---
## 数据预读取与状态
1. 数据预存取容器(Data Store):在SSR期间,我们本质上是在渲染我们App的快照,所以如果App依赖于一些异步数据,那么在开始渲染过程之前,**需要先预取与解析好这些数据**。同时,在客户端(client),在挂载app(mount)之前,需要获取到与服务器相同的数据,否则客户端app会因为使用与服务端app不同状态导致混合失败。
- 为了解决这个问题,我们获取的数据**需要位于视图组件之外的地方**,即放置在专门的数据预取存储去容器。首先在服务端,我们可以在渲染之前预取数据,将数据填充到store中。此外我们将在HTML中序列化与内联预置状态。这样在挂载到客户端app之前,可以直接从store获取到内联预置状态。为此,我们可以直接使用官方状态管理库Vuex
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 假定我们有一个可以返回 Promise 的
// 通用 API(请忽略此 API 具体实现细节)
import { fetchItem } from './api'
export function createStore () {
return new Vuex.Store({
state: {
items: {}
},
actions: {
fetchItem ({ commit }, id) {
// `store.dispatch()` 会返回 Promise,
// 以便我们能够知道数据在何时更新
return fetchItem(id).then(item => {
commit('setItem', { id, item })
})
}
},
mutations: {
setItem (state, { id, item }) {
Vue.set(state.items, id, item)
}
}
})
}
// 然后修改app.js
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
import { sync } from 'vuex-router-sync'
export function createApp () {
// 创建 router 和 store 实例
const router = createRouter()
const store = createStore()
// 同步路由状态(route state)到 store
sync(store, router)
// 创建应用程序实例,将 router 和 store 注入
const app = new Vue({
router,
store,
render: h => h(App)
})
// 暴露 app, router 和 store。
return { app, router, store }
}
```
带有逻辑配置的组件(Logic Collocation with Components):那么我们需要在哪里放置「dispatch数据预取action」的代码呢?
需要通过访问路由来决定获取哪部分数据(这也决定了哪些组件需要渲染)。事实上,给定路由所需的数据也是在该路由上渲染组件时所需要的数据。所以在路由组件中放置数据预取逻辑,十分自然。我们将在路由组件上暴露出一个自定义静态函数
asyncData。由于此函数会在组件实例化之前调用,++所以他无法访问this++,需要将store与路由信息作为参数传递进去。<!-- Item.vue --> <template> <div>{{ item.title }}</div> </template> <script> export default { asyncData ({ store, route }) { // 触发 action 后,会返回 Promise return store.dispatch('fetchItem', route.params.id) }, computed: { // 从 store 的 state 对象中的获取 item。 item () { return this.$store.state.items[this.$route.params.id] } } } </script>
服务端数据预取:在
entry-server.js中,我们可以通过路由获取与router.getMatchedComponents()相匹配的组件,如果组件暴露出asyncData,我们就掉用这个方法。然后需要将解析完成的状态,附加在渲染上下文render context中 ``` // 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(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// 对所有匹配的路由组件调用 `asyncData()`
Promise.all(matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute
})
}
})).then(() => {
// 在所有预取钩子(preFetch hook) resolve 后,
// 我们的 store 现在已经填充入渲染应用程序所需的状态。
// 当我们将状态附加到上下文,
// 并且 `template` 选项用于 renderer 时,
// 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
context.state = store.state
resolve(app)
}).catch(reject)
}, reject)
}) }
当使用 `template` 时,`context.state` 将作为 `window.__INITIAL_STATE__` 状态,自动嵌入到最终的 HTML 中。而在客户端,在挂载到应用程序之前,store 就应该获取到状态:
// entry-client.js
const { app, router, store } = createApp()
if (window.INITIAL_STATE) { store.replaceState(window.INITIAL_STATE) }
4. 客户端数据预取:在客户端数据与区有两种不同的方式:
- 1. 在路由导航之前解析数据:使用此策略,应用程序会等待视图所需数据全部解析之后,再传入数据并处理当前视图。好处在于,可以直接在数据准备就绪时,传入视图渲染完整内容,但是如果数据预取需要很长时间,用户在当前视图会感受到"明显卡顿"。因此,如果使用此策略,建议提供一个数据加载指示器 (data loading indicator)。我们可以通过检查匹配的组件,并在全局路由钩子函数中执行 `asyncData` 函数,来在客户端实现此策略。注意,在初始路由准备就绪之后,我们应该注册此钩子,这样我们就不必再次获取服务器提取的数据。
// entry-client.js
// ...忽略无关代码
router.onReady(() => {
// 添加路由钩子函数,用于处理 asyncData.
// 在初始路由 resolve 后执行,
// 以便我们不会二次预取(double-fetch)已有的数据。
// 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。
router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to)
const prevMatched = router.getMatchedComponents(from)
// 我们只关心非预渲染的组件
// 所以我们对比它们,找出两个匹配列表的差异组件
let diffed = false
const activated = matched.filter((c, i) => {
return diffed || (diffed = (prevMatched[i] !== c))
})
if (!activated.length) {
return next()
}
// 这里如果有加载指示器 (loading indicator),就触发
Promise.all(activated.map(c => {
if (c.asyncData) {
return c.asyncData({ store, route: to })
}
})).then(() => {
// 停止加载指示器(loading indicator)
next()
}).catch(next)
})
app.$mount('#app')
})
```
- 2. 匹配要渲染的视图后,再获取数据:此策略将客户端数据预取逻辑,放在视图组件的 `beforeMount` 函数中。当路由导航被触发时,可以立即切换视图,因此应用程序具有更快的响应速度。然而,传入视图在渲染时不会有完整的可用数据。因此,对于使用此策略的每个视图组件,都需要具有条件加载状态。这可以通过纯客户端 (client-only) 的全局 mixin 来实现:
```
Vue.mixin({
beforeMount () {
const { asyncData } = this.$options
if (asyncData) {
// 将获取数据操作分配给 promise
// 以便在组件中,我们可以在数据准备就绪后
// 通过运行 `this.dataPromise.then(...)` 来执行其他任务
this.dataPromise = asyncData({
store: this.$store,
route: this.$route
})
}
}
})
```
这两种策略是根本上不同的用户体验决策,应该根据你创建的应用程序的实际使用场景进行挑选。但是无论你选择哪种策略,当路由组件重用(同一路由,但是 params 或 query 已更改,例如,从 user/1 到 user/2)时,也应该调用 asyncData 函数。我们也可以通过纯客户端 (client-only) 的全局 mixin 来处理这个问题:
```
Vue.mixin({
beforeRouteUpdate (to, from, next) {
const { asyncData } = this.$options
if (asyncData) {
asyncData({
store: this.$store,
route: to
}).then(next).catch(next)
} else {
next()
}
}
})
```
- Store代码拆分:在大型应用程序中,我们的 Vuex store 可能会分为多个模块。当然,也可以将这些模块代码,分割到相应的路由组件 chunk 中。假设我们有以下 store 模块:
我们可以在路由组件的 asyncData 钩子函数中,使用 store.registerModule 惰性注册(lazy-register)这个模块: ``` // 在路由组件内// store/modules/foo.js export default { namespaced: true, // 重要信息:state 必须是一个函数, // 因此可以创建多个实例化该模块 state: () => ({ count: 0 }), actions: { inc: ({ commit }) => commit('inc') }, mutations: { inc: state => state.count++ } }{{ fooCount }}
由于模块现在是路由组件的依赖,所以它将被 webpack 移动到路由组件的异步 chunk 中。
---
## 客户端激活:
所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。
- 在 entry-client.js 中,我们用下面这行挂载(mount)应用程序:
// 这里假定 App.vue template 根元素的 id="app"
app.$mount('#app')
由于服务器已经渲染好了 HTML,我们显然无需将其丢弃再重新创建所有的 DOM 元素。相反,我们需要"激活"这些静态的 HTML,然后使他们成为动态的(能够响应后续的数据变化)。
如果你检查服务器渲染的输出结果,你会注意到应用程序的根元素上添加了一个特殊的属性:
`data-server-rendered` 特殊属性,让客户端 Vue 知道这部分 HTML 是由 Vue 在服务端渲染的,并且应该以激活模式进行挂载。注意,这里并没有添加 `id="app"`,而是添加 `data-server-rendered` 属性:你需要自行添加 ID 或其他能够选取到应用程序根元素的选择器,否则应用程序将无法正常激活。
注意,在没有 `data-server-rendered` 属性的元素上,还可以向 `$mount` 函数的 `hydrating` 参数位置传入 `true`,来强制使用激活模式(hydration):
// 强制使用应用程序的激活模式 app.$mount('#app', true)
*在开发模式下,Vue 将推断客户端生成的虚拟 DOM 树 (virtual DOM tree),是否与从服务器渲染的 DOM 结构 (DOM structure) 匹配。如果无法匹配,它将退出混合模式,丢弃现有的 DOM 并从头开始渲染。在生产模式下,此检测会被跳过,以避免性能损耗。*
- **一些需要注意的坑:**
使用「SSR + 客户端混合」时,需要了解的一件事是,浏览器可能会更改的一些特殊的 HTML 结构。例如,当你在 Vue 模板中写入:
| hi |
浏览器会在 `<table>` 内部自动注入 `<tbody>`,然而,由于 Vue 生成的虚拟 DOM (virtual DOM) 不包含 `<tbody>`,所以会导致无法匹配。为能够正确匹配,请确保在模板中写入有效的 HTML。
---
## Bundle Renderer 指引
1. **使用基本 SSR的问题**:到目前为止,我们假设打包的服务器端代码,将由服务器通过 `require` 直接使用:
const createApp = require('/path/to/built-server-bundle.js')
这是理所应当的,然而在每次编辑过应用程序源代码之后,都必须停止并重启服务。这在开发过程中会影响开发效率。此外,Node.js 本身不支持 `source map`。
2. **传入 BundleRenderer**:`vue-server-renderer` 提供一个名为 `createBundleRenderer` 的 API,用于处理此问题,通过使用 `webpack` 的自定义插件,`server bundle` 将生成为可传递到 `bundle renderer` 的特殊 JSON 文件。所创建的 `bundle renderer`,用法和普通 `renderer` 相同,但是 `bundle renderer` 提供以下优点:
- 内置的 source map 支持(在 webpack 配置中使用 `devtool: 'source-map'`)
- 在开发环境甚至部署过程中热重载(通过读取更新后的 `bundle`,然后重新创建 `renderer` 实例)
- 关键 CSS(critical CSS) 注入(在使用 `*.vue` 文件时):自动内联在渲染过程中用到的组件所需的CSS。
- 使用 `clientManifest` 进行资源注入:自动推断出最佳的预加载(preload)和预取(prefetch)指令,以及初始渲染所需的代码分割 chunk。
以下就是创建和使用 bundle renderer 的方法:(需要先在webpack中配置,以生成bundle renderer所需要的构建工件)
const { createBundleRenderer } = require('vue-server-renderer')
const renderer = createBundleRenderer(serverBundle, { runInNewContext: false, // 推荐 template, // (可选)页面模板 clientManifest // (可选)客户端构建 manifest })
// 在服务器处理函数中…… server.get('*', (req, res) => { const context = { url: req.url } // 这里无需传入一个应用程序,因为在执行 bundle 时已经自动创建过。 // 现在我们的服务器与应用程序已经解耦! renderer.renderToString(context, (err, html) => { // 处理异常…… res.end(html) }) })
`bundle renderer` 在调用 `renderToString` 时,它将自动执行「由 bundle 创建的应用程序实例」所导出的函数(传入上下文作为参数),然后渲染它。
注意,推荐将 `runInNewContext` 选项设置为 `false` 或 `'once'`。
---
## 构建配置(webpack配置bundle render)
服务器端渲染 (SSR) 项目的配置大体上与纯客户端项目类似,但是我们建议将配置分为三个文件:`base`, `client` 和 `server`。基本配置 (base config) 包含在两个环境共享的配置,例如,输出路径 (output path),别名 (alias) 和 loader。服务器配置 (server config) 和客户端配置 (client config),可以通过使用 `webpack-merge` 来简单地扩展基本配置。
- **服务器配置 (Server Config)**:服务器配置,是用于生成传递给 `createBundleRenderer` 的 `server bundle`。它应该是这样的:
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: '/path/to/entry-server.js',
// 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
// 并且还会在编译 Vue 组件时,
// 告知 vue-loader 输送面向服务器代码(server-oriented code)。
target: 'node',
// 对 bundle renderer 提供 source map 支持 devtool: 'source-map',
// 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports) output: { libraryTarget: 'commonjs2' },
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化应用程序依赖模块。可以使服务器构建速度更快,
// 并生成较小的 bundle 文件。
externals: nodeExternals({
// 不要外置化 webpack 需要处理的依赖模块。
// 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
// 你还应该将修改 global(例如 polyfill)的依赖模块列入白名单
whitelist: /.css$/
}),
// 这是将服务器的整个输出
// 构建为单个 JSON 文件的插件。
// 默认文件名为 vue-ssr-server-bundle.json
plugins: [
new VueSSRServerPlugin()
在生成 `vue-ssr-server-bundle.json` 之后,只需将文件路径传递给 `createBundleRenderer`:
const { createBundleRenderer } = require('vue-server-renderer') const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', { // ……renderer 的其他选项 }) ```
- 扩展说明 (Externals Caveats):
请注意,在
externals选项中,我们将 CSS 文件列入白名单。这是因为从依赖模块导入的 CSS 还应该由webpack处理。如果你导入依赖于webpack的任何其他类型的文件(例如*.vue,*.sass),那么你也应该将它们添加到白名单中。 如果你使用runInNewContext: 'once'或runInNewContext: true,那么你还应该将修改global的polyfill列入白名单,例如babel-polyfill。这是因为当使用新的上下文模式时,server bundle中的代码具有自己的global对象。由于在使用 Node 7.6+ 时,在服务器并不真正需要它,所以实际上只需在客户端entry导入它。 - 客户端配置 (Client Config):客户端配置 (client config) 和基本配置 (base config) 大体上相同。显然你需要把
entry指向你的客户端入口文件。除此之外,如果你使用CommonsChunkPlugin,请确保仅在客户端配置 (client config) 中使用,因为服务器包需要单独的入口chunk。 - 生成 clientManifest:除了
server bundle之外,我们还可以生成客户端构建清单 (client build manifest)。使用客户端清单 (client manifest) 和服务器 bundle(server bundle),renderer现在具有了服务器和客户端的构建信息,因此它可以自动推断和注入资源预加载 / 数据预取指令(preload / prefetch directive),以及 css 链接 / script 标签到所渲染的 HTML。有双重好处:- 在生成的文件名中有哈希时,可以取代
html-webpack-plugin来注入正确的资源 URL。 - 在通过
webpack的按需代码分割特性渲染bundle时,我们可以确保对chunk进行最优化的资源预加载/数据预取,并且还可以将所需的异步chunk智能地注入为<script>标签,以避免客户端的瀑布式请求 (waterfall request),以及改善可交互时间 (TTI - time-to-interactive)。
- 在生成的文件名中有哈希时,可以取代
SEOMixin:
- 同样也要服务端客户端一起设置
- ① 在server.js中会设置默认title/...
- ② 在具体组件内调用对应Mixin方法,eg:title


