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'

相關文章
相關標籤/搜索