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