如何使用Vue2做服務端渲染
技術棧
服務端:Nodejs(v6.3)
前端框架 Vue2.1.10
前端構建工具:webpack2.2 && gulp
代碼檢查:eslint
源碼:es6
前端路由:vue-router2.1.0
狀態管理:vuex2.1.0
服務端通信:axios
日誌管理:log4js
項目自動化部署工具:jenkins
Vue2與服務端渲染(SSR)
Vue2.0在服務端創建了虛擬DOM,因此可以在服務端可以提前渲染出來,解決了單頁面一直存在的問題:SEO和初次載入耗時較多的問題。同時在真正意義上做到了前後端共用一套代碼。
SSR的實現原理
客戶端請求伺服器,伺服器根據請求地址獲得匹配的組件,在調用匹配到的組件返回 Promise (官方是preFetch方法)來將需要的數據拿到。最後再通過
<script>window.__initial_state=data</script>
將其寫入網頁,最後將服務端渲染好的網頁返回回去。
接下來客戶端會將vuex將寫入的 __initial_state__ 替換為當前的全局狀態樹,再用這個狀態樹去檢查服務端渲染好的數據有沒有問題。遇到沒被服務端渲染的組件,再去發非同步請求拿數據。說白了就是一個類似React的 shouldComponentUpdate 的Diff操作。
Vue2使用的是單向數據流,用了它,就可以通過 SSR 返回唯一一個全局狀態, 並確認某個組件是否已經SSR過了。
開啟服務端渲染(SSR)
Web框架目前我們使用的是express,之前使用過一次時間的koa來做SSR,結果發現坑很多,相關的案例太少,有些坑不太好解決,所以為了線上項目的穩定,從而選擇了express。
SSR流程圖
安裝SSR相關
npm install --save express vue-server-renderer lru-cache es6-promise serialize-javascript vue vue-router axios
vue更新到2.0之後,作者就宣告不再對vue-resource更新,並且vue-resource不支持SSR,所以我推薦使用axios, 在服務端和客戶端可以同時使用。
vue2使用了虛擬DOM, 因此對瀏覽器環境和服務端環境要分開渲染, 要創建兩個對應的入口文件。
瀏覽器入口文件 client-entry.js
使用 $mount 直接掛載
服務端入口文件 server-entry
使用vue的SSR功能直接將虛擬DOM渲染成網頁
client-entry.js 文件
import es6-promise/auto;import { app, store } from ./app;store.replaceState(window.__INITIAL_STATE__);app.$mount(#app);
在 client-entry.js 文件中引入了app.js, 判斷如果在服務端渲染時已經寫入狀態,則將vuex的狀態進行替換,使得服務端渲染的html和vuex管理的數據是同步的。然後將vue實例掛載到html指定的節點中。
server-entry 文件
import { app, router, store } from ./app;const isDev = process.env.NODE_ENV !== production; export default context => { const s = isDev && Date.now(); router.push(context.url); const matchedComponents = router.getMatchedComponents(); if (!matchedComponents.length) { return Promise.reject({ code: 404 }); } return Promise.all(matchedComponents.map(component => { if (component.preFetch) { return component.preFetch(store); } })).then(() => { return app; });};
在 server-entry 文件中服務端會傳遞一個context對象,裡面包含當前用戶請求的url,vue-router 會跳轉到當前請求的url中,通過 router.getMatchedComponents( ) 來獲得當前匹配組件,則去調用當前匹配到的組件里的 preFetch 鉤子,並傳遞store(Vuex下的狀態),會返回一個 Promise 對象,並在then方法中將現有的vuex state 賦值給context,給服務端渲染使用,最後返回vue實例,將虛擬DOM渲染成網頁。服務端會將vuex初始狀態也生成到頁面中。 如果 vue-router 沒有匹配到請求的url,直接返回 Promise中的reject方法,傳入404,這時候會走到下方renderStream的error事件,讓頁面顯示錯誤信息。
// 處理所有的get請求app.get(*, (req, res) => { // 等待編譯 if (!renderer) { return res.end(waiting for compilation... refresh in a moment.); } var s = Date.now(); const context = { url: req.url }; // 渲染我們的Vue實例作為流 const renderStream = renderer.renderToStream(context); // 當塊第一次被渲染時 renderStream.once(data, () => { // 將預先的HTML寫入響應 res.write(indexHTML.head); }); // 每當新的塊被渲染 renderStream.on(data, chunk => { // 將塊寫入響應 res.write(chunk); }); // 當所有的塊被渲染完成 renderStream.on(end, () => { // 當vuex初始狀態存在 if (context.initialState) { // 將vuex初始狀態以script的方式寫入到頁面中 res.write( `<script>window.__INITIAL_STATE__=${ serialize(context.initialState, { isJSON: true }) }</script>` ); } // 將結尾的HTML寫入響應 res.end(indexHTML.tail); }); // 當渲染時發生錯誤 renderStream.on(error, err => { if (err && err.code === 404) { res.status(404).end(404 | Page Not Found); return; } res.status(500).end(Internal Error 500); });})
上面是vue2.0的服務端渲染方式,用流式渲染的方式,將HTML一邊生成一邊寫入相應流,而不是在最後一次全部寫入。這樣的效果就是頁面渲染速度將會很快。還可以引入 lru-cache 這個模塊對數據進行緩存,並設置緩存時間,我一般設置15分鐘的緩存時間。
可以參考vue ssr 官方演示項目的服務端實現 https://github.com/vuejs/vue-hackernews-2.0/blob/master/server.js>
axios在客戶端和服務端的使用
創建2個文件用於客戶端和服務端的的通信
create-api-client.js 文件(用於客戶端)
const axios = require(axios);let api;axios.defaults.timeout = 10000;axios.interceptors.response.use((res) => { if (res.status >= 200 && res.status < 300) { return res; } return Promise.reject(res);}, (error) => { // 網路異常 return Promise.reject({message: 網路異常,請刷新重試, err: error});});if (process.__API__) { api = process.__API__;} else { api = { get: function(target, params = {}) { const suffix = Object.keys(params).map(name => { return `${name}=${JSON.stringify(params[name])}`; }).join(&); const urls = `${target}?${suffix}`; return new Promise((resolve, reject) => { axios.get(urls, params).then(res => { resolve(res.data); }).catch((error) => { reject(error); }); }); }, post: function(target, options = {}) { return new Promise((resolve, reject) => { axios.post(target, options).then(res => { resolve(res.data); }).catch((error) => { reject(error); }); }); } };}module.exports = api;
create-api-server.js 文件(用於服務端)
const isProd = process.env.NODE_ENV === production;const axios = require(axios);let host = isProd ? http://yczj.api.autohome.com.cn : http://t.yczj.api.autohome.com.cn;let cook = process.__COOKIE__ || ;let api;axios.defaults.baseURL = host;axios.defaults.timeout = 10000;axios.interceptors.response.use((res) => { if (res.status >= 200 && res.status < 300) { return res; } return Promise.reject(res);}, (error) => { // 網路異常 return Promise.reject({message: 網路異常,請刷新重試, err: error, type: 1});});if (process.__API__) { api = process.__API__;} else { api = { get: function(target, options = {}) { return new Promise((resolve, reject) => { axios.request({ url: target, method: get, headers: { Cookie: cook }, params: options }).then(res => { resolve(res.data); }).catch((error) => { reject(error); }); }); }, post: function(target, options = {}) { return new Promise((resolve, reject) => { axios.request({ url: target, method: post, headers: { Cookie: cook }, params: options }).then(res => { resolve(res.data); }).catch((error) => { reject(error); }); }); } };}module.exports = api;
由於在服務端,介面不會主動攜帶 cookie,所以需要在headers里寫入cookie。由於介面數據經常發生變化,所以沒有做緩存。
如果您想了解更多最新前端技術,請關注 AutoHome車服務前端團隊 微信公眾號
推薦閱讀:
※Vue2.0 中,「漸進式框架」和「自底向上增量開發的設計」這兩個概念是什麼?
※關於Vue組件化的疑惑,這是Vue的缺陷嗎?
※既然用 virtual dom 可以提高性能,為什麼瀏覽器不直接自帶這個功能呢?