摘要:說起,其實(shí)早在出現(xiàn)之前,網(wǎng)頁就是在服務(wù)端渲染的。沒有涉及流式渲染組件緩存對(duì)的服務(wù)端渲染有更深一步的認(rèn)識(shí),實(shí)際在生產(chǎn)環(huán)境中的應(yīng)用可能還需要考慮很多因素。選擇的服務(wù)端渲染方案,是情理之中的選擇,不是對(duì)新技術(shù)的盲目追捧,而是一切為了需要。
背景作者:威威(滬江前端開發(fā)工程師)
本文原創(chuàng),轉(zhuǎn)載請(qǐng)注明作者及出處。
最近, 產(chǎn)品同學(xué)一如往常笑嘻嘻的遞來需求文檔, 縱使內(nèi)心萬般拒絕, 身體倒是很誠(chéng)實(shí)。 接過需求,好在需求不復(fù)雜, 簡(jiǎn)單構(gòu)思 后決定用Vue, 得心應(yīng)手。 切好圖, 挽起袖子準(zhǔn)備擼代碼的時(shí)候, SEO同學(xué)不知何時(shí)已經(jīng)站到了背后。
"聽說你要用Vue?" "恩..." "SEO考慮了嗎?整個(gè)SPA出來,網(wǎng)頁的SEO咋辦?" "奧..."
換以前, 估計(jì)只能無奈的換個(gè)實(shí)現(xiàn)方式, 但是Vue 2.0時(shí)代的到來, 給你多了一種可能。 你可以對(duì)SEO工程師說:用Vue沒問題!
想必,很多前端同學(xué)都有類似這樣的經(jīng)歷, 為了SEO,只能放棄得心應(yīng)手的框架。 SEO(Search Engine Optimization)顧名思義就是一系列為了提高 網(wǎng)站收錄排名,吸引精準(zhǔn)用戶的方案。 這么看來,SEO確實(shí)是有舉足輕重的作用。 不過,好消息是,Vue2.0的發(fā)布為SEO提供了可能, 這就是SSR(serve side render)。
說起SSR,其實(shí)早在SPA (Single Page Application) 出現(xiàn)之前,網(wǎng)頁就是在服務(wù)端渲染的。服務(wù)器接收到客戶端請(qǐng)求后,將數(shù)據(jù)和模板拼接成完整的頁面響應(yīng)到客戶端。 客戶端直接渲染, 此時(shí)用戶希望瀏覽新的頁面,就必須重復(fù)這個(gè)過程, 刷新頁面. 這種體驗(yàn)在Web技術(shù)發(fā)展的當(dāng)下是幾乎不能被接受的,于是越來越多的技術(shù)方案涌現(xiàn),力求 實(shí)現(xiàn)無頁面刷新或者局部刷新來達(dá)到優(yōu)秀的交互體驗(yàn)。 比如Vue:
- 在客戶端管理路由,用戶切換路由,無需向服務(wù)器重新請(qǐng)求頁面和靜態(tài)資源,只需要使用 ajax 獲取數(shù)據(jù)在客戶端完成渲染,這樣可以減少了很多不必要的網(wǎng)絡(luò)傳輸,縮短了響應(yīng)時(shí)間。 - 聲明式渲染(告訴 vue 你要做什么,讓它幫你做),把我們從煩人的DOM操作中解放出來,集中處理業(yè)務(wù)邏輯。 - 組件化視圖,無論是功能組件還是UI組件都可以進(jìn)行抽象,寫一次到處用。 - 前后端并行開發(fā),只需要與后端定好數(shù)據(jù)格式,前期用模擬數(shù)據(jù),就可以與后端并行開發(fā)了。 - 對(duì)復(fù)雜項(xiàng)目的各個(gè)組件之間的數(shù)據(jù)傳遞 vue - Vuex 狀態(tài)管理模式
缺點(diǎn)大家自然猜到了, 對(duì),主要的一點(diǎn)就是不利于SEO,或者說對(duì)SEO不友好。 來看下面兩張圖;
SPA頁面的源代碼
下圖SSR頁面的源代碼
上面兩張圖就是使用了傳統(tǒng)單頁應(yīng)用和SSR的頁面源代碼, 第一張圖中,很明顯頁面的數(shù)據(jù)都是通過Ajax異步獲取,然而搜索引擎度娘家的爬蟲看到這樣空曠的源碼并不會(huì)絲毫留戀. 相反,通過服務(wù)端渲染的頁面,就有很多對(duì)于爬蟲來講有效的連接. 畢竟度娘一家獨(dú)大,看來服務(wù)端渲染確實(shí)有探究的必要了。
Vue.js 的服務(wù)端渲染是怎么回事?先看一張Vue官網(wǎng)的服務(wù)端渲染示意圖
從圖上可以看出,ssr 有兩個(gè)入口文件,client.js 和 server.js, 都包含了應(yīng)用代碼,webpack 通過兩個(gè)入口文件分別打包成給服務(wù)端用的 server bundle 和給客戶端用的 client bundle. 當(dāng)服務(wù)器接收到了來自客戶端的請(qǐng)求之后,會(huì)創(chuàng)建一個(gè)渲染器 bundleRenderer,這個(gè) bundleRenderer 會(huì)讀取上面生成的 server bundle 文件,并且執(zhí)行它的代碼, 然后發(fā)送一個(gè)生成好的 html 到瀏覽器,等到客戶端加載了 client bundle 之后,會(huì)和服務(wù)端生成的DOM 進(jìn)行 Hydration(判斷這個(gè)DOM 和自己即將生成的DOM 是否相同,如果相同就將客戶端的vue實(shí)例掛載到這個(gè)DOM上, 否則會(huì)提示警告)。
怎么實(shí)現(xiàn)?知道了Vue服務(wù)端渲染的大致流程,那怎么用代碼來實(shí)現(xiàn)呢?
1. 創(chuàng)建一個(gè) vue 實(shí)例 2. 配置路由,以及相應(yīng)的視圖組件 3. 使用 vuex 管理數(shù)據(jù) 4. 創(chuàng)建服務(wù)端入口文件 5. 創(chuàng)建客戶端入口文件 6. 配置 webpack,分服務(wù)端打包配置和客戶端打包配置 7. 創(chuàng)建服務(wù)器端的渲染器,將vue實(shí)例渲染成html
首先我們來創(chuàng)建一個(gè) vue 實(shí)例
// app.js import Vue from "vue"; import router from "./router"; import store from "./store"; import App from "./components/app"; let app = new Vue({ template: "", base: "/c/", components: { App }, router, store }); export { app, router, store }
和我們以前寫的vue實(shí)例差別不大,但是我們不會(huì)在這里將app mount到DOM上,因?yàn)檫@個(gè)實(shí)例也會(huì)在服務(wù)端去運(yùn)行,這里直接將 app 暴露出去。
配置 vue 路由
import Vue from "vue"; import VueRouter from "vue-router"; import IndexView from "../views/indexView"; import ArticleItems from "../views/articleItems"; Vue.use(VueRouter); const router = new VueRouter({ mode: "history", base: "/c/", routes: [ { path: "/:alias", component: IndexView }, { path: "/:alias/list", component: ArticleItems } ] });
注意這里的 base,在服務(wù)端傳遞 path 給 vue-router 的時(shí)候要注意去掉前面的 "/c/",否則會(huì)匹配不到。
創(chuàng)建視圖組件,這里我們使用單文件組件,下面是 indexView.vue 文件的實(shí)例代碼
這里我們暴露一個(gè) fetchServerData 方法用來在服務(wù)端渲染時(shí)做數(shù)據(jù)的預(yù)加載,具體在哪調(diào)用,下面會(huì)講到。 beforeMount 是vue的生命周期鉤子函數(shù),當(dāng)應(yīng)用在客戶端切換到這個(gè)視圖的時(shí)候會(huì)在特定的時(shí)候去執(zhí)行,用于在客戶端獲取數(shù)據(jù)。
使用 vuex 管理數(shù)據(jù),vue2.0 的服務(wù)端官方推薦使用 STORE 來管理數(shù)據(jù),和1.0相比 api 有一些調(diào)整
import Vue from "vue"; import Vuex from "vuex"; import axios from "axios"; Vue.use(Vuex); let apiHost = "http://localhost:3000"; const store = new Vuex.Store({ state: { alias: "", ztData: {}, courseListItems: [], articleItems: [] }, actions: { FETCH_ZT: ({ commit, dispatch, state }, { alias }) = { commit("SET_ALIAS", { alias }); return axios.get(`${apiHost}/api/zt`) .then(response => { let data = response.data || {}; commit("SET_ZT_DATA", data); }) }, FETCH_COURSE_ITEMS: ({ commit, dispatch, state }) => { return axios.get(`${apiHost}/api/course_items`).then(response => { let data = response.data; commit("SET_COURSE_ITEMS", data); }); }, FETCH_ARTICLE_ITEMS: ({ commit, dispatch, state }) => { return axios.get(`${apiHost}/api/article_items`) .then(response => { let data = response.data; commit("SET_ARTICLE_ITEMS", data); }) } }, mutations: { SET_COURSE_ITEMS: (state, data) => { state.courseListItems = data; }, SET_ALIAS: (state, { alias }) => { state.alias = alias; }, SET_ZT_DATA: (state, { ztData }) => { state.ztData = ztData; }, SET_ARTICLE_ITEMS: (state, items) => { state.articleItems = items; } } }) export default store;
state 使我們應(yīng)用層的數(shù)據(jù),相當(dāng)于一個(gè)倉庫,整個(gè)應(yīng)用層的數(shù)據(jù)都存在這里,與不使用vuex的vue應(yīng)用有兩點(diǎn)不同:
- Vuex 的狀態(tài)存儲(chǔ)是響應(yīng)式的。當(dāng) Vue 組件從 store 中讀取狀態(tài)的時(shí)候,若 store 中的狀態(tài)發(fā)生變化,那么相應(yīng)的組件也會(huì)相應(yīng)地得到高效更新。 - Vuex 不允許我們直接對(duì) store 中的數(shù)據(jù)進(jìn)行操作。改變 store 中的狀態(tài)的唯一途徑就是顯式地提交(commit) mutations。這樣使得我們可以方便地跟蹤每一個(gè)狀態(tài)的變化,從而讓我們能夠?qū)崿F(xiàn)一些工具幫助我們更好地了解我們的應(yīng)用。 action 響應(yīng)在view上的用戶輸入導(dǎo)致的狀態(tài)變化,并不直接操作數(shù)據(jù),異步的邏輯都封裝在這里執(zhí)行,它最終的目的是提交 mutation 來操作數(shù)據(jù)。 mutation vuex 中修改store 數(shù)據(jù)的唯一方法,使用 commit 來提交。
創(chuàng)建服務(wù)端的入口文件 server-entry.js
// server-entry.js import {app, router, store} from "./app"; export default context => { const s = Date.now(); router.push(context.url); const matchedComponents = router.getMatchedComponents(); if(!matchedComponents) { return Promise.reject({ code: "404" }); } return Promise.all( matchedComponents.map(component => { if(component.fetchServerData) { return component.fetchServerData(store); } }) ).then(() => { context.initialState = store.state; return app; }) }
server.js 返回一個(gè)函數(shù),該函數(shù)接受一個(gè)從服務(wù)端傳遞過來的 context 的參數(shù),將 vue 實(shí)例通過 promise 返回。 context 一般包含 當(dāng)前頁面的url,首先我們調(diào)用 vue-router 的 router.push(url) 切換到到對(duì)應(yīng)的路由, 然后調(diào)用 getMatchedComponents 方法返回對(duì)應(yīng)要渲染的組件, 這里會(huì)檢查組件是否有 fetchServerData 方法,如果有就會(huì)執(zhí)行它。
下面這行代碼將服務(wù)端獲取到的數(shù)據(jù)掛載到 context 對(duì)象上,后面會(huì)把這些數(shù)據(jù)直接發(fā)送到瀏覽器端與客戶端的vue 實(shí)例進(jìn)行數(shù)據(jù)(狀態(tài))同步。
context.initialState = store.state
創(chuàng)建客戶端入口文件 client-entry.js
// client-entry.js import { app, store } from "./app"; import "./main.scss"; store.replaceState(window.__INITIAL_STATE__); app.$mount("#app");
客戶端入口文件很簡(jiǎn)單,同步服務(wù)端發(fā)送過來的數(shù)據(jù),然后把 vue 實(shí)例掛載到服務(wù)端渲染的 DOM 上。
配置 webpack
// webpack.server.config.js const base = require("./webpack.base.config"); // webpack 的通用配置 module.exports = Object.assign({}, base, { target: "node", entry: "./src/server-entry.js", output: { filename: "server-bundle.js", libraryTarget: "commonjs2" }, externals: Object.keys(require("../package.json").dependencies), plugins: [ new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"), "process.env.VUE_ENV": ""server"" }) ] })
注意這里添加了 target: "node" 和 libraryTarget: "commonjs2",然后入口文件改成我們的 server-entry.js, 客戶端的 webpack 和以前一樣,這里就不貼了。
分別打包服務(wù)端代碼和客戶端代碼
因?yàn)橛袃蓚€(gè) webpack 配置文件,執(zhí)行 webpack 時(shí)候就需要指定 --config 參數(shù)來編譯不同的 bundle。 我們可以配置兩個(gè) npm script
"packclient": "webpack --config webpack.client.config.js", "packserver": "webpack --config webpack.server.config.js"
然后在命令行運(yùn)行
npm run packclient npm run packserver
就會(huì)生成兩個(gè)文件 client-bundle.js 和 server-bundle.js
創(chuàng)建服務(wù)端渲染器
// controller.js const serialize = require("serialize-javascript"); // 因?yàn)槲覀冊(cè)趘ue-router 的配置里面使用了 `base: "/c"`,這里需要去掉請(qǐng)求path中的 "/c" let url = this.url.replace(//c/, ""); let context = { url: this.url }; // 創(chuàng)建渲染器 let bundleRenderer = createRenderer(fs.readFileSync(resolve("./dist/server-bundle.js"), "utf-8")) let html = yield new Promise((resolve, reject) => { // 將vue實(shí)例編譯成一個(gè)字符串 bundleRenderer.renderToString( context, // 傳遞context 給 server-bundle.js 使用 (err, html) => { if(err) { console.error("server render error", err); resolve(""); } /** * 還記得在 server-entry.js 里面 `context.initialState = store.state` 這行代碼么? * 這里就直接把數(shù)據(jù)發(fā)送到瀏覽器端啦 **/ html += ``; resolve(html); } ) }) yield this.render("ssr", html); // 創(chuàng)建渲染器函數(shù) function createRenderer(code) { return require("vue-server-renderer").createBundleRenderer(code); }
在 node 的 views 模板文件中只需要將上面的 html 輸出就可以了
// ssr.html {% extends "layout.html" %} {% block body %} {{ html | safe }} {% endblock %}
這樣,一個(gè)簡(jiǎn)單的服務(wù)端渲染就結(jié)束了。
小結(jié)限于篇幅,詳細(xì)的代碼請(qǐng)參考 Github代碼庫:https://github.com/ikcamp/vue...
整個(gè)demo包含了:
vue + vue-router + vuex 的使用
服務(wù)端數(shù)據(jù)獲取
客戶端數(shù)據(jù)同步以及DOM hydration。
沒有涉及:
流式渲染
組件緩存
對(duì)Vue的服務(wù)端渲染有更深一步的認(rèn)識(shí),實(shí)際在生產(chǎn)環(huán)境中的應(yīng)用可能還需要考慮很多因素。
選擇Vue的服務(wù)端渲染方案,是情理之中的選擇,不是對(duì)新技術(shù)的盲目追捧,而是一切為了需要。 Vue 2.0的SSR方案只是提供了一種可能,多了一種選擇,框架本身在于服務(wù)開發(fā)者,根據(jù)不同的場(chǎng)景選擇不同的方案,才會(huì)事半功倍。
文章僅代表個(gè)人觀點(diǎn),有不妥當(dāng)?shù)胤綗┱?qǐng)大家指出,共同進(jìn)步!
iKcamp原創(chuàng)新書《移動(dòng)Web前端高效開發(fā)實(shí)戰(zhàn)》已在亞馬遜、京東、當(dāng)當(dāng)開售。
>> 滬江Web前端上海團(tuán)隊(duì)招聘【W(wǎng)eb前端架構(gòu)師】,有意者簡(jiǎn)歷至:zhouyao@hujiang.com <<
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/88377.html
摘要:斯坦福宣布使用作為計(jì)算機(jī)課程的首選語言近日,某位有年教學(xué)經(jīng)驗(yàn)的斯坦福教授決定放棄,而使用作為計(jì)算機(jī)入門課程的教學(xué)語言。斯坦福官方站點(diǎn)將它們新的課程描述為是最流行的構(gòu)建交互式的開發(fā)語言,本課程會(huì)用講解中的實(shí)例。 前端每周清單第 11 期:Angular 4.1支持TypeScript 2.3,Vue 2.3優(yōu)化服務(wù)端渲染,優(yōu)秀React界面框架合集 為InfoQ中文站特供稿件,首發(fā)地址為...
摘要:前端每周清單第期微服務(wù)實(shí)踐,與,組件技巧,攻防作者王下邀月熊編輯徐川前端每周清單專注前端領(lǐng)域內(nèi)容,以對(duì)外文資料的搜集為主,幫助開發(fā)者了解一周前端熱點(diǎn)分為新聞熱點(diǎn)開發(fā)教程工程實(shí)踐深度閱讀開源項(xiàng)目巔峰人生等欄目。 前端每周清單第 26 期:Node.js 微服務(wù)實(shí)踐,Vue.js 與 GraphQL,Angular 組件技巧,HeadlessChrome 攻防 作者:王下邀月熊 編輯:徐川...
摘要:特意對(duì)前端學(xué)習(xí)資源做一個(gè)匯總,方便自己學(xué)習(xí)查閱參考,和好友們共同進(jìn)步。 特意對(duì)前端學(xué)習(xí)資源做一個(gè)匯總,方便自己學(xué)習(xí)查閱參考,和好友們共同進(jìn)步。 本以為自己收藏的站點(diǎn)多,可以很快搞定,沒想到一入?yún)R總深似海。還有很多不足&遺漏的地方,歡迎補(bǔ)充。有錯(cuò)誤的地方,還請(qǐng)斧正... 托管: welcome to git,歡迎交流,感謝star 有好友反應(yīng)和斧正,會(huì)及時(shí)更新,平時(shí)業(yè)務(wù)工作時(shí)也會(huì)不定期更...
摘要:一團(tuán)隊(duì)組織網(wǎng)站說明騰訊團(tuán)隊(duì)騰訊前端團(tuán)隊(duì),代表作品,致力于前端技術(shù)的研究騰訊社交用戶體驗(yàn)設(shè)計(jì),簡(jiǎn)稱,騰訊設(shè)計(jì)團(tuán)隊(duì)網(wǎng)站騰訊用戶研究與體驗(yàn)設(shè)計(jì)部百度前端研發(fā)部出品淘寶前端團(tuán)隊(duì)用技術(shù)為體驗(yàn)提供無限可能凹凸實(shí)驗(yàn)室京東用戶體驗(yàn)設(shè)計(jì)部出品奇舞團(tuán)奇虎旗下前 一、團(tuán)隊(duì)組織 網(wǎng)站 說明 騰訊 AlloyTeam 團(tuán)隊(duì) 騰訊Web前端團(tuán)隊(duì),代表作品WebQQ,致力于前端技術(shù)的研究 ISUX 騰...
摘要:一團(tuán)隊(duì)組織網(wǎng)站說明騰訊團(tuán)隊(duì)騰訊前端團(tuán)隊(duì),代表作品,致力于前端技術(shù)的研究騰訊社交用戶體驗(yàn)設(shè)計(jì),簡(jiǎn)稱,騰訊設(shè)計(jì)團(tuán)隊(duì)網(wǎng)站騰訊用戶研究與體驗(yàn)設(shè)計(jì)部百度前端研發(fā)部出品淘寶前端團(tuán)隊(duì)用技術(shù)為體驗(yàn)提供無限可能凹凸實(shí)驗(yàn)室京東用戶體驗(yàn)設(shè)計(jì)部出品奇舞團(tuán)奇虎旗下前 一、團(tuán)隊(duì)組織 網(wǎng)站 說明 騰訊 AlloyTeam 團(tuán)隊(duì) 騰訊Web前端團(tuán)隊(duì),代表作品WebQQ,致力于前端技術(shù)的研究 ISUX 騰...
閱讀 1951·2021-09-07 10:24
閱讀 2087·2019-08-30 15:55
閱讀 2037·2019-08-30 15:43
閱讀 669·2019-08-29 15:25
閱讀 1044·2019-08-29 12:19
閱讀 1927·2019-08-23 18:32
閱讀 1515·2019-08-23 17:59
閱讀 947·2019-08-23 12:22