感興趣的小伙伴,下面一起跟隨512筆記的小編兩巴掌來看看吧! 花費(fèi)了一個月時間,終于在新養(yǎng)車之家項(xiàng)目中成功部署了vue2服務(wù)端渲染(SSR),并且使用上了Vuex 負(fù)責(zé)狀態(tài)管理,首屏加載時間從之前4G網(wǎng)絡(luò)下的1000ms,提升到了現(xiàn)在500-700ms之間,SSR的優(yōu)勢有很多,現(xiàn)在讓我來跟你細(xì)細(xì)道來。 技術(shù)棧服務(wù)端:Nodejs(v6.3) 前端框架 Vue2.1.10 前端構(gòu)建工具:webpack2.2 && gulp 代碼檢查:eslint 源碼:es6 前端路由:vue-router2.1.0 狀態(tài)管理:vuex2.1.0 服務(wù)端通信:axios 日志管理:log4js 項(xiàng)目自動化部署工具:jenkins Vue2與服務(wù)端渲染(SSR)Vue2.0在服務(wù)端創(chuàng)建了虛擬DOM,因此可以在服務(wù)端可以提前渲染出來,解決了單頁面一直存在的問題:SEO和初次加載耗時較多的問題。同時在真正意義上做到了前后端共用一套代碼。 SSR的實(shí)現(xiàn)原理客戶端請求服務(wù)器,服務(wù)器根據(jù)請求地址獲得匹配的組件,在調(diào)用匹配到的組件返回 Promise (官方是preFetch方法)來將需要的數(shù)據(jù)拿到。最后再通過 代碼如下: <script>window.__initial_state=data</script>
將其寫入網(wǎng)頁,最后將服務(wù)端渲染好的網(wǎng)頁返回回去。 接下來客戶端會將vuex將寫入的 __initial_state__ 替換為當(dāng)前的全局狀態(tài)樹,再用這個狀態(tài)樹去檢查服務(wù)端渲染好的數(shù)據(jù)有沒有問題。遇到?jīng)]被服務(wù)端渲染的組件,再去發(fā)異步請求拿數(shù)據(jù)。說白了就是一個類似React的 shouldComponentUpdate 的Diff操作。 Vue2使用的是單向數(shù)據(jù)流,用了它,就可以通過 SSR 返回唯一一個全局狀態(tài), 并確認(rèn)某個組件是否已經(jīng)SSR過了。 開啟服務(wù)端渲染(SSR)Web框架目前我們使用的是express,之前使用過一次時間的koa來做SSR,結(jié)果發(fā)現(xiàn)坑很多,相關(guān)的案例太少,有些坑不太好解決,所以為了線上項(xiàng)目的穩(wěn)定,從而選擇了express。 SSR流程圖 
安裝SSR相關(guān) 復(fù)制代碼 代碼如下: 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, 在服務(wù)端和客戶端可以同時使用。 vue2使用了虛擬DOM, 因此對瀏覽器環(huán)境和服務(wù)端環(huán)境要分開渲染, 要創(chuàng)建兩個對應(yīng)的入口文件。 瀏覽器入口文件 client-entry.js 使用 $mount 直接掛載 服務(wù)端入口文件 server-entry 使用vue的SSR功能直接將虛擬DOM渲染成網(wǎng)頁 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, 判斷如果在服務(wù)端渲染時已經(jīng)寫入狀態(tài),則將vuex的狀態(tài)進(jìn)行替換,使得服務(wù)端渲染的html和vuex管理的數(shù)據(jù)是同步的。然后將vue實(shí)例掛載到html指定的節(jié)點(diǎn)中。 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 文件中服務(wù)端會傳遞一個context對象,里面包含當(dāng)前用戶請求的url,vue-router 會跳轉(zhuǎn)到當(dāng)前請求的url中,通過 router.getMatchedComponents( ) 來獲得當(dāng)前匹配組件,則去調(diào)用當(dāng)前匹配到的組件里的 preFetch 鉤子,并傳遞store(Vuex下的狀態(tài)),會返回一個 Promise 對象,并在then方法中將現(xiàn)有的vuex state 賦值給context,給服務(wù)端渲染使用,最后返回vue實(shí)例,將虛擬DOM渲染成網(wǎng)頁。服務(wù)端會將vuex初始狀態(tài)也生成到頁面中。 如果 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實(shí)例作為流
const renderStream = renderer.renderToStream(context);
// 當(dāng)塊第一次被渲染時
renderStream.once('data', () => {
// 將預(yù)先的HTML寫入響應(yīng)
res.write(indexHTML.head);
});
// 每當(dāng)新的塊被渲染
renderStream.on('data', chunk => {
// 將塊寫入響應(yīng)
res.write(chunk);
});
// 當(dāng)所有的塊被渲染完成
renderStream.on('end', () => {
// 當(dāng)vuex初始狀態(tài)存在
if (context.initialState) {
// 將vuex初始狀態(tài)以script的方式寫入到頁面中
res.write(
`<script>window.__INITIAL_STATE__=${
serialize(context.initialState, { isJSON: true })
}</script>`
);
}
// 將結(jié)尾的HTML寫入響應(yīng)
res.end(indexHTML.tail);
});
// 當(dāng)渲染時發(fā)生錯誤
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的服務(wù)端渲染方式,用流式渲染的方式,將HTML一邊生成一邊寫入相應(yīng)流,而不是在最后一次全部寫入。這樣的效果就是頁面渲染速度將會很快。還可以引入 lru-cache 這個模塊對數(shù)據(jù)進(jìn)行緩存,并設(shè)置緩存時間,我一般設(shè)置15分鐘的緩存時間。 可以參考vue ssr 官方演示項(xiàng)目的服務(wù)端實(shí)現(xiàn) https://github.com/vuejs/vue-hackernews-2.0/blob/master/server.js axios在客戶端和服務(wù)端的使用創(chuàng)建2個文件用于客戶端和服務(wù)端的的通信 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) => {
// 網(wǎng)絡(luò)異常
return Promise.reject({message: '網(wǎng)絡(luò)異常,請刷新重試', 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 文件(用于服務(wù)端) 代碼如下: 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) => {
// 網(wǎng)絡(luò)異常
return Promise.reject({message: '網(wǎng)絡(luò)異常,請刷新重試', 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;
由于在服務(wù)端,接口不會主動攜帶 cookie,所以需要在headers里寫入cookie。由于接口數(shù)據(jù)經(jīng)常發(fā)生變化,所以沒有做緩存。 以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持512筆記。 注:關(guān)于詳解如何使用Vue2做服務(wù)端渲染的內(nèi)容就先介紹到這里,更多相關(guān)文章的可以留意512筆記的其他信息。
|