vue服务器端渲染指南研究

什么是服务器端渲染(SSR)?

Vue.js 是构建客户端应用程序的框架。默认状况下,能够在浏览器中输出 Vue 组件,进行生成 DOM 和操做 DOM。然而,也能够将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上彻底可交互的应用程序。javascript

服务器渲染的 Vue.js 应用程序也能够被认为是"同构"或"通用",由于应用程序的大部分代码均可以在服务器和客户端上运行。css

为何使用服务器端渲染(SSR)?

优点:html

1.更好的 SEO,因为搜索引擎爬虫抓取工具能够直接查看彻底渲染的页面。前端

2.更快的内容到达时间(time-to-content),特别是对于缓慢的网络状况或运行缓慢的设备。无需等待全部的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,因此你的用户将会更快速地看到完整渲染的页面。一般能够产生更好的用户体验,而且对于那些「内容到达时间(time-to-content)与转化率直接相关」的应用程序而言,服务器端渲染(SSR)相当重要。vue

使用服务器端渲染(SSR)时还须要有一些权衡之处:java

  • 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数(lifecycle hook)中使用;一些外部扩展库(external library)可能须要特殊处理,才能在服务器渲染应用程序中运行。webpack

  • 涉及构建设置和部署的更多要求。与能够部署在任何静态文件服务器上的彻底静态单页面应用程序(SPA)不一样,服务器渲染应用程序,须要处于 Node.js server 运行环境。ios

  • 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源(CPU-intensive - CPU 密集),所以若是你预料在高流量环境(high traffic)下使用,请准备相应的服务器负载,并明智地采用缓存策略。git

服务器端渲染 vs 预渲染(SSR vs Prerendering)

若是你调研服务器端渲染(SSR)只是用来改善少数营销页面(例如 //about/contact 等)的 SEO,那么你可能须要预渲染。无需使用 web 服务器实时动态编译 HTML,而是使用预渲染方式,在构建时(build time)简单地生成针对特定路由的静态 HTML 文件。优势是设置预渲染更简单,并能够将你的前端做为一个彻底静态的站点。。github

安装:

npm install vue vue-server-renderer --save

注意

  • 推荐使用 Node.js 版本 6+。
  • vue-server-renderer 和 vue 必须匹配版本。
  • vue-server-renderer 依赖一些 Node.js 原生模块,所以只能在 Node.js 中使用。咱们可能会提供一个更简单的构建,能够在未来在其余「JavaScript 运行时(runtime)」运行。

#渲染一个 Vue 实例

// 第 1 步:建立一个 Vue 实例 const Vue = require('vue') const app = new Vue({ template: `<div>Hello World</div>` }) // 第 2 步:建立一个 renderer const renderer = require('vue-server-renderer').createRenderer() // 第 3 步:将 Vue 实例渲染为 HTML renderer.renderToString(app, (err, html) => { if (err) throw err console.log(html) // => <div data-server-rendered="true">Hello World</div> }) // 在 2.5.0+,若是没有传入回调函数,则会返回 Promise: renderer.renderToString(app).then(html => { console.log(html) }).catch(err => { console.error(err) })

与服务器集成

npm install express --save
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)

使用一个页面模板

当你在渲染 Vue 应用程序时,renderer 只从应用程序生成 HTML 标记(markup)。在这个示例中,咱们必须用一个额外的 HTML 页面包裹容器,来包裹生成的 HTML 标记。

为了简化这些,你能够直接在建立 renderer 时提供一个页面模板。多数时候,咱们会将页面模板放在特有的文件中,例如 index.template.html

<!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body> <!--vue-ssr-outlet--> </body> </html> 

注意 <!--vue-ssr-outlet--> 注释 -- 这里将是应用程序 HTML 标记注入的地方。

而后,咱们能够读取和传输文件到 Vue renderer 中:

const renderer = createRenderer({ template: require('fs').readFileSync('./index.template.html', 'utf-8') }) renderer.renderToString(app, (err, html) => { console.log(html) // html 将是注入应用程序内容的完整页面 }) 

#模板插值

模板还支持简单插值。给定以下模板:

<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> 

咱们能够经过传入一个"渲染上下文对象",做为 renderToString 函数的第二个参数,来提供插值数据:

const context = { title: 'hello', meta: ` <meta ...> <meta ...> ` } renderer.renderToString(app, context, (err, html) => { // 页面 title 将会是 "Hello" // meta 标签也会注入 })

也能够与 Vue 应用程序实例共享 context 对象,容许模板插值中的组件动态地注册数据。

此外,模板支持一些高级特性,例如:

  • 在使用 *.vue 组件时,自动注入「关键的 CSS(critical CSS)」;
  • 在使用 clientManifest 时,自动注入「资源连接(asset links)和资源预加载提示(resource hints)」;
  • 在嵌入 Vuex 状态进行客户端融合(client-side hydration)时,自动注入以及 XSS 防护。

在以后的指南中介绍相关概念时,咱们将详细讨论这些。

编写通用代码

在进一步介绍以前,让咱们花点时间来讨论编写"通用"代码时的约束条件 - 即运行在服务器和客户端的代码。因为用例和平台 API 的差别,当运行在不一样环境中时,咱们的代码将不会彻底相同。因此这里咱们将会阐述你须要理解的关键事项。

服务器上的数据响应

在纯客户端应用程序(client-only app)中,每一个用户会在他们各自的浏览器中使用新的应用程序实例。对于服务器端渲染,咱们也但愿如此:每一个请求应该都是全新的、独立的应用程序实例,以便不会有交叉请求形成的状态污染(cross-request state pollution)。

由于实际的渲染过程须要肯定性,因此咱们也将在服务器上“预取”数据("pre-fetching" data) - 这意味着在咱们开始渲染时,咱们的应用程序就已经解析完成其状态。也就是说,将数据进行响应式的过程在服务器上是多余的,因此默认状况下禁用。禁用响应式数据,还能够避免将「数据」转换为「响应式对象」的性能开销

组件生命周期钩子函数

因为没有动态更新,全部的生命周期钩子函数中,只有 beforeCreate 和 created 会在服务器端渲染(SSR)过程当中被调用。这就是说任何其余生命周期钩子函数中的代码(例如 beforeMount 或 mounted),只会在客户端执行。

此外还须要注意的是,你应该避免在 beforeCreate 和 created 生命周期时产生全局反作用的代码,例如在其中使用 setInterval 设置 timer。在纯客户端(client-side only)的代码中,咱们能够设置一个 timer,而后在 beforeDestroy 或 destroyed 生命周期时将其销毁。可是,因为在 SSR 期间并不会调用销毁钩子函数,因此 timer 将永远保留下来。为了不这种状况,请将反作用代码移动到 beforeMount或 mounted 生命周期中。

访问特定平台(Platform-Specific) API

用代码不可接受特定平台的 API,所以若是你的代码中,直接使用了像 window 或 document,这种仅浏览器可用的全局变量,则会在 Node.js 中执行时抛出错误,反之也是如此。

对于共享于服务器和客户端,但用于不一样平台 API 的任务(task),建议将平台特定实现包含在通用 API 中,或者使用为你执行此操做的 library。例如,axios 是一个 HTTP 客户端,能够向服务器和客户端都暴露相同的 API。

对于仅浏览器可用的 API,一般方式是,在「纯客户端(client-only)」的生命周期钩子函数中惰性访问(lazily access)它们。

请注意,考虑到若是第三方 library 不是以上面的通用用法编写,则将其集成到服务器渲染的应用程序中,可能会很棘手。你可能要经过模拟(mock)一些全局变量来使其正常运行,但这只是 hack 的作法,而且可能会干扰到其余 library 的环境检测代码

自定义指令

大多数自定义指令直接操做 DOM,所以会在服务器端渲染(SSR)过程当中致使错误。有两种方法能够解决这个问题:

  1. 推荐使用组件做为抽象机制,并运行在「虚拟 DOM 层级(Virtual-DOM level)」(例如,使用渲染函数(render function))。

  2. 若是你有一个自定义指令,可是不是很容易替换为组件,则能够在建立服务器 renderer 时,使用 directives 选项所提供"服务器端版本(server-side version)"。

源码结构

#避免状态单例

当编写纯客户端(client-only)代码时,咱们习惯于每次在新的上下文中对代码进行取值。可是,Node.js 服务器是一个长期运行的进程。当咱们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着若是建立一个单例对象,它将在每一个传入的请求之间共享。

如基本示例所示,咱们为每一个请求建立一个新的根 Vue 实例。这与每一个用户在本身的浏览器中使用新应用程序的实例相似。若是咱们在多个请求之间使用一个共享的实例,很容易致使交叉请求状态污染(cross-request state pollution)。

所以,咱们不该该直接建立一个应用程序实例,而是应该暴露一个能够重复执行的工厂函数,为每一个请求建立新的应用程序实例:

// app.js const Vue = require('vue') module.exports = function createApp (context) { return new Vue({ data: { url: context.url }, template: `<div>访问的 URL 是: {{ url }}</div>` }) }

而且咱们的服务器代码如今变为:

// server.js const createApp = require('./app') server.get('*', (req, res) => { const context = { url: req.url } const app = createApp(context) renderer.renderToString(app, (err, html) => { // 处理错误…… res.end(html) }) })

一样的规则也适用于 router、store 和 event bus 实例。你不该该直接从模块导出并将其导入到应用程序中,而是须要在 createApp 中建立一个新的实例,并从根 Vue 实例注入。

在使用带有 { runInNewContext: true } 的 bundle renderer 时,能够消除此约束,可是因为须要为每一个请求建立一个新的 vm 上下文,所以伴随有一些显著性能开销。

 

介绍构建步骤

目前为止,咱们尚未讨论过如何将相同的 Vue 应用程序提供给客户端。为了作到这一点,咱们须要使用 webpack 来打包咱们的 Vue 应用程序。事实上,咱们可能须要在服务器上使用 webpack 打包 Vue 应用程序,由于:

  • 一般 Vue 应用程序是由 webpack 和 vue-loader 构建,而且许多 webpack 特定功能不能直接在 Node.js 中运行(例如经过 file-loader 导入文件,经过 css-loader 导入 CSS)。

  • 尽管 Node.js 最新版本可以彻底支持 ES2015 特性,咱们仍是须要转译客户端代码以适应老版浏览器。这也会涉及到构建步骤。

因此基本见解是,对于客户端应用程序和服务器应用程序,咱们都要使用 webpack 打包 - 服务器须要「服务器 bundle」而后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。

使用 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.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 }

 

路由和代码分割

#使用 vue-router 的路

 

可能已经注意到,咱们的服务器代码使用了一个 * 处理程序,它接受任意 URL。这容许咱们将访问的 URL 传递到咱们的 Vue 应用程序中,而后对客户端和服务器复用相同的路由配置!

为此,建议使用官方提供的 vue-router。咱们首先建立一个文件,在其中建立 router。注意,相似于 createApp,咱们也须要给每一个请求一个新的 router 实例,因此文件导出一个 createRouter 函数:

// router.js import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) export function createRouter () { return new Router({ mode: 'history', routes: [ // ... ] }) } 

而后更新 app.js:

// 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 } } 

如今咱们须要在 entry-server.js 中实现服务器端路由逻辑(server-side routing logic):

// 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 context = { url: req.url } createApp(context).then(app => { renderer.renderToString(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) } }) }) })

 

代码分割

 

用程序的代码分割或惰性加载,有助于减小浏览器在初始渲染中下载的资源体积,能够极大地改善大致积 bundle 的可交互时间(TTI - time-to-interactive)。这里的关键在于,对初始首屏而言,"只加载所需"。

Vue 提供异步组件做为第一类的概念,将其与 webpack 2 所支持的使用动态导入做为代码分割点相结合,你须要作的是:

// 这里进行修改…… import Foo from './Foo.vue' // 改成这样: const Foo = () => import('./Foo.vue') 

在 Vue 2.5 如下的版本中,服务端渲染时异步组件只能用在路由组件上。然而在 2.5+ 的版本中,得益于核心算法的升级,异步组件如今能够在应用中的任何地方使用。

须要注意的是,你仍然须要在挂载 app 以前调用 router.onReady,由于路由器必需要提早解析路由配置中的异步组件,才能正确地调用组件中可能存在的路由钩子。这一步咱们已经在咱们的服务器入口(server entry)中实现过了,如今咱们只须要更新客户端入口(client entry):

// 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') } ] }) }

数据预取和状态

#数据预取存储容器(Data Store)

 

在服务器端渲染(SSR)期间,咱们本质上是在渲染咱们应用程序的"快照",因此若是应用程序依赖于一些异步数据,那么在开始渲染过程以前,须要先预取和解析好这些数据。

另外一个须要关注的问题是在客户端,在挂载(mount)到客户端应用程序以前,须要获取到与服务器端应用程序彻底相同的数据 - 不然,客户端应用程序会由于使用与服务器端应用程序不一样的状态,而后致使混合失败。

为了解决这个问题,获取的数据须要位于视图组件以外,即放置在专门的数据预取存储容器(data store)或"状态容器(state container))"中。首先,在服务器端,咱们能够在渲染以前预取数据,并将数据填充到 store 中。此外,咱们将在 HTML 中序列化(serialize)和内联预置(inline)状态。这样,在挂载(mount)到客户端应用程序以前,能够直接从 store 获取到内联预置(inline)状态。

为此,咱们将使用官方状态管理库 Vuex。咱们先建立一个 store.js 文件,里面会模拟一些根据 id 获取 item 的逻辑:

// 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

// 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>

 

服务器端数据预取(Server Data Fetching)

在 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__) }

客户端数据预取(Client Data Fetching)

 

在客户端,处理数据预取有两种不一样方式:

  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') }) 
  1. 匹配要渲染的视图后,再获取数据:

此策略将客户端数据预取逻辑,放在视图组件的 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 代码拆分(Store Code Splitting)

在大型应用程序中,咱们的 Vuex store 可能会分为多个模块。固然,也能够将这些模块代码,分割到相应的路由组件 chunk 中。假设咱们有如下 store 模块:

// store/modules/foo.js export default { namespaced: true, // 重要信息:state 必须是一个函数, // 所以能够建立多个实例化该模块 state: () => ({ count: 0 }), actions: { inc: ({ commit }) => commit('inc') }, mutations: { inc: state => state.count++ } } 

咱们能够在路由组件的 asyncData 钩子函数中,使用 store.registerModule 惰性注册(lazy-register)这个模块:

// 在路由组件内
<template> <div>{{ fooCount }}</div> </template> <script> // 在这里导入模块,而不是在 `store/index.js` 中 import fooStoreModule from '../store/modules/foo' export default { asyncData ({ store }) { store.registerModule('foo', fooStoreModule) return store.dispatch('foo/inc') }, // 重要信息:当屡次访问路由时, // 避免在客户端重复注册模块。 destroyed () { this.$store.unregisterModule('foo') }, computed: { fooCount () { return this.$store.state.foo.count } } } </script> 

因为模块如今是路由组件的依赖,因此它将被 webpack 移动到路由组件的异步 chunk 中。

 

客户端激活(client-side hydration)

 

所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。

在 entry-client.js 中,咱们用下面这行挂载(mount)应用程序:

// 这里假定 App.vue template 根元素的 `id="app"` app.$mount('#app') 

因为服务器已经渲染好了 HTML,咱们显然无需将其丢弃再从新建立全部的 DOM 元素。相反,咱们须要"激活"这些静态的 HTML,而后使他们成为动态的(可以响应后续的数据变化)。

若是你检查服务器渲染的输出结果,你会注意到应用程序的根元素上添加了一个特殊的属性:

<div id="app" data-server-rendered="true"> 

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 模板中写入:

<table> <tr><td>hi</td></tr> </table> 

浏览器会在 <table> 内部自动注入 <tbody>,然而,因为 Vue 生成的虚拟 DOM(virtual DOM) 不包含 <tbody>,因此会致使没法匹配。为可以正确匹配,请确保在模板中写入有效的 HTML。

 

使用基本 SSR 的问题

 

const createApp = require('/path/to/built-server-bundle.js')

 

这是理所应当的,然而在每次编辑过应用程序源代码以后,都必须中止并重启服务。这在开发过程当中会影响开发效率。此外,Node.js 自己不支持 source map。

#传入 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。更多细节请查看 CSS 章节。

  • 使用 clientManifest 进行资源注入:自动推断出最佳的预加载(preload)和预取(prefetch)指令,以及初始渲染所需的代码分割 chunk。


在下一章节中,咱们将讨论如何配置 webpack,以生成 bundle renderer 所需的构建工件(build artifact),但如今假设咱们已经有了这些须要的构建工件,如下就是建立和使用 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'

相关文章
相关标签/搜索