摘要:說起,其實早在出現(xiàn)之前,網(wǎng)頁就是在服務(wù)端渲染的。二服務(wù)器渲染如此快有部分原因來自,但是排除這部分,還是比瀏覽器端要快至少幾倍。當(dāng)然,我們可以考慮首屏采用服務(wù)端渲染的方式,因為完全服務(wù)端渲染會考慮到很多問題,比如復(fù)雜均衡等等等
Vue 2.0 開始支持服務(wù)端渲染的功能,所以本文章也是基于vue 2.0以上版本。網(wǎng)上對于服務(wù)端渲染的資料還是比較少,最經(jīng)典的莫過于Vue作者尤雨溪大神的 vue-hacker-news。本人在公司做Vue項目的時候,一直苦于產(chǎn)品、客戶對首屏加載要求,SEO的訴求,也想過很多解決方案,本次也是針對瀏覽器渲染不足之處,采用了服務(wù)端渲染,并且做了兩個一樣的Demo作為比較,更能直觀的對比Vue前后端的渲染。
話不多說,我們分別來看兩個Demo:(歡迎star 歡迎pull request)
1.瀏覽器端渲染Demo: https://github.com/monkeyWang...
2.服務(wù)端渲染Demo:https://github.com/monkeyWang...
兩套代碼運行結(jié)果都是為了展示豆瓣電影的,運行效果也都是差不多,下面我們來分別簡單的闡述一下項目的機理:
一、瀏覽器端渲染豆瓣電影首先我們用官網(wǎng)的腳手架搭建起來一個vue項目
npm install -g vue-cli vue init webpack doubanMovie cd doubanMovie npm install npm run dev
這樣便可以簡單地打起來一個cli框架,下面我們要做的事情就是分別配置 vue-router, vuex,然后配置我們的webpack proxyTable 讓他支持代理訪問豆瓣API。
1.配置Vue-router
我們需要三個導(dǎo)航頁:正在上映、即將上映、Top250;一個詳情頁,一個搜索頁。這里我給他們分別配置了各自的路由。在 router/index.js 下配置以下信息:
import Vue from "vue" import Router from "vue-router" import Moving from "@/components/moving" import Upcoming from "@/components/upcoming" import Top250 from "@/components/top250" import MoviesDetail from "@/components/common/moviesDetail" import Search from "@/components/searchList" Vue.use(Router) /** * 路由信息配置 */ export default new Router({ routes: [ { path: "/", name: "Moving", component: Moving }, { path: "/upcoming", name: "upcoming", component: Upcoming }, { path: "/top250", name: "Top250", component: Top250 }, { path: "/search", name: "Search", component: Search }, { path: "/moviesDetail", name: "moviesDetail", component: MoviesDetail } ] })
這樣我們的路由信息配置好了,然后每次切換路由的時候,盡量避免不要重復(fù)請求數(shù)據(jù),所以我們還需要配置一下組件的keep-alive:在app.vue組件里面。
這樣一個基本的vue-router就配置好了。
2.引入vuex
Vuex 是一個專為 Vue.js 應(yīng)用程序開發(fā)的狀態(tài)管理模式。它采用集中式存儲管理應(yīng)用的所有組件的狀態(tài),并以相應(yīng)的規(guī)則保證狀態(tài)以一種可預(yù)測的方式發(fā)生變化。Vuex 也集成到 Vue 的官方調(diào)試工具 devtools extension,提供了諸如零配置的 time-travel 調(diào)試、狀態(tài)快照導(dǎo)入導(dǎo)出等高級調(diào)試功能。
簡而言之:Vuex 相當(dāng)于某種意義上設(shè)置了讀寫權(quán)限的全局變量,將數(shù)據(jù)保存保存到該“全局變量”下,并通過一定的方法去讀寫數(shù)據(jù)。
Vuex 并不限制你的代碼結(jié)構(gòu)。但是,它規(guī)定了一些需要遵守的規(guī)則:
應(yīng)用層級的狀態(tài)應(yīng)該集中到單個 store 對象中。
提交 mutation 是更改狀態(tài)的唯一方法,并且這個過程是同步的。
異步邏輯都應(yīng)該封裝到 action 里面。
對于大型應(yīng)用我們會希望把 Vuex 相關(guān)代碼分割到模塊中。下面是項目結(jié)構(gòu)示例:
├── index.html ├── main.js ├── api │ └── ... # 抽取出API請求 ├── components │ ├── App.vue │ └── ... └── store ├── index.js # 我們組裝模塊并導(dǎo)出 store 的地方 └── moving # 電影模塊 ├── index.js # 模塊內(nèi)組裝,并導(dǎo)出模塊的地方 ├── actions.js # 模塊基本 action ├── getters.js # 模塊級別 getters ├── mutations.js # 模塊級別 mutations └── types.js # 模塊級別 types
所以我們開始在我們的src目錄下新建一個名為store 的文件夾 為了后期考慮 我們新建了moving 文件夾,用來組織電影,考慮到所有的action,getters,mutations,都寫在一起,文件太混亂,所以我又給他們分別提取出來。
stroe文件夾建好,我們要開始在main.js里面引用vuex實例:
import store from "./store" new Vue({ el: "#app", router, store, template: "", components: { App } })
這樣,我們便可以在所有的子組件里通過 this.$store 來使用vuex了。
3.webpack proxyTable 代理跨域
webpack 開發(fā)環(huán)境可以使用proxyTable 來代理跨域,生產(chǎn)環(huán)境的話可以根據(jù)各自的服務(wù)器進(jìn)行配置代理跨域就行了。在我們的項目config/index.js 文件下可以看到有一個proxyTable的屬性,我們對其簡單的改寫
proxyTable: { "/api": { target: "http://api.douban.com/v2", changeOrigin: true, pathRewrite: { "^/api": "" } } }
這樣當(dāng)我們訪問
localhost:8080/api/movie
的時候 其實我們訪問的是
http://api.douban.com/v2/movie
這樣便達(dá)到了一種跨域請求的方案。
至此,瀏覽器端的主要配置已經(jīng)介紹完了,下面我們來看看運行的結(jié)果:
為了介紹瀏覽器渲染是怎么回事,我們運行一下npm run build 看看我們的發(fā)布版本的文件,到底是什么鬼東西....
run build 后會都出一個dist目錄 ,我們可以看到里面有個index.html,這個便是我們最終頁面將要展示的html,我們打開,可以看到下面:
觀察好的小伙伴可以發(fā)現(xiàn),我們并沒有多余的dom元素,就只有一個div,那么頁面要怎么呈現(xiàn)呢?答案是js append,對,下面的那些js會負(fù)責(zé)innerHTML。而js是由瀏覽器解釋執(zhí)行的,所以呢,我們稱之為瀏覽器渲染,這有幾個致命的缺點:
**1.js放在dom結(jié)尾,如果js文件過大,那么必然造成頁面阻塞。用戶體驗明顯不好(這也是我我在公司反復(fù)被產(chǎn)品逼問的事情)** **2.不利于SEO** **3.客戶端運行在老的JavaScript引擎上**
對于世界上的一些地區(qū)人,可能只能用1998年產(chǎn)的電腦訪問互聯(lián)網(wǎng)的方式使用計算機。而Vue只能運行在IE9以上的瀏覽器,你可能也想為那些老式瀏覽器提供基礎(chǔ)內(nèi)容 - 或者是在命令行中使用 Lynx的時髦的黑客
基于以上的一些問題,服務(wù)端渲染呼之欲出....
二、服務(wù)器端渲染豆瓣電影先看一張Vue官網(wǎng)的服務(wù)端渲染示意圖
從圖上可以看出,ssr 有兩個入口文件,client.js 和 server.js, 都包含了應(yīng)用代碼,webpack 通過兩個入口文件分別打包成給服務(wù)端用的 server bundle 和給客戶端用的 client bundle. 當(dāng)服務(wù)器接收到了來自客戶端的請求之后,會創(chuàng)建一個渲染器 bundleRenderer,這個 bundleRenderer 會讀取上面生成的 server bundle 文件,并且執(zhí)行它的代碼, 然后發(fā)送一個生成好的 html 到瀏覽器,等到客戶端加載了 client bundle 之后,會和服務(wù)端生成的DOM 進(jìn)行 Hydration(判斷這個DOM 和自己即將生成的DOM 是否相同,如果相同就將客戶端的vue實例掛載到這個DOM上, 否則會提示警告)。
具體實現(xiàn):
我們需要vuex,需要router,需要服務(wù)器,需要服務(wù)緩存,需要代理跨域....不急我們慢慢來。
1.建立nodejs服務(wù)
首先我們需要一個服務(wù)器,那么對于nodejs,express是很好地選擇。我們來建立一個server.js
const port = process.env.PORT || 8080 app.listen(port, () => { console.log(`server started at localhost:${port}`) })
這里用來啟動服務(wù)監(jiān)聽 8080 端口。
然后我們開始處理所有的get請求,當(dāng)請求頁面的時候,我們需要渲染頁面
app.get("*", (req, res) => { if (!renderer) { return res.end("waiting for compilation... refresh in a moment.") } const s = Date.now() res.setHeader("Content-Type", "text/html") res.setHeader("Server", serverInfo) const errorHandler = err => { if (err && err.code === 404) { res.status(404).end("404 | Page Not Found") } else { // Render Error Page or Redirect res.status(500).end("500 | Internal Server Error") console.error(`error during render : ${req.url}`) console.error(err) } } renderer.renderToStream({ url: req.url }) .on("error", errorHandler) .on("end", () => console.log(`whole request: ${Date.now() - s}ms`)) .pipe(res) })
然后我們需要代理請求,這樣才能進(jìn)行跨域,我們引入http-proxy-middleware模塊:
const proxy = require("http-proxy-middleware");//引入代理中間件 /** * proxy middleware options * 代理跨域配置 * @type {{target: string, changeOrigin: boolean, pathRewrite: {^/api: string}}} */ var options = { target: "http://api.douban.com/v2", // target host changeOrigin: true, // needed for virtual hosted sites pathRewrite: { "^/api": "" } }; var exampleProxy = proxy(options); app.use("/api", exampleProxy);
這樣我們的服務(wù)端server.js便配置完成。接下來 我們需要配置服務(wù)端入口文件,還有客戶端入口文件,首先來配置一下客戶端文件,新建src/entry-client.js
import "es6-promise/auto" import { app, store, router } from "./app" // prime the store with server-initialized state. // the state is determined during SSR and inlined in the page markup. if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) } /** * 異步組件 */ router.onReady(() => { // 開始掛載到dom上 app.$mount("#app") }) // service worker if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { navigator.serviceWorker.register("/service-worker.js") }
客戶端入口文件很簡單,同步服務(wù)端發(fā)送過來的數(shù)據(jù),然后把 vue 實例掛載到服務(wù)端渲染的 DOM 上。
再配置一下服務(wù)端入口文件:src/entry-server.js
import { app, router, store } from "./app" const isDev = process.env.NODE_ENV !== "production" // This exported function will be called by `bundleRenderer`. // This is where we perform data-prefetching to determine the // state of our application before actually rendering it. // Since data fetching is async, this function is expected to // return a Promise that resolves to the app instance. export default context => { const s = isDev && Date.now() return new Promise((resolve, reject) => { // set router"s location router.push(context.url) // wait until router has resolved possible async hooks router.onReady(() => { const matchedComponents = router.getMatchedComponents() // no matched routes if (!matchedComponents.length) { reject({ code: 404 }) } // Call preFetch hooks on components matched by the route. // A preFetch hook dispatches a store action and returns a Promise, // which is resolved when the action is complete and store state has been // updated. Promise.all(matchedComponents.map(component => { return component.preFetch && component.preFetch(store) })).then(() => { isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`) // After all preFetch hooks are resolved, our store is now // filled with the state needed to render the app. // Expose the state on the render context, and let the request handler // inline the state in the HTML response. This allows the client-side // store to pick-up the server-side state without having to duplicate // the initial data fetching on the client. context.state = store.state resolve(app) }).catch(reject) }) }) }
server.js 返回一個函數(shù),該函數(shù)接受一個從服務(wù)端傳遞過來的 context 的參數(shù),將 vue 實例通過 promise 返回。context 一般包含 當(dāng)前頁面的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 實例進(jìn)行數(shù)據(jù)(狀態(tài))同步。
context.state = store.state
然后我們分別配置客戶端和服務(wù)端webpack,這里可以在我的github上fork下來參考配置,里面每一步都有注釋,這里不再贅述。
接著我們需要創(chuàng)建app.js:
import Vue from "vue" import App from "./App.vue" import store from "./store" import router from "./router" import { sync } from "vuex-router-sync" import Element from "element-ui" Vue.use(Element) // sync the router with the vuex store. // this registers `store.state.route` sync(store, router) /** * 創(chuàng)建vue實例 * 在這里注入 router store 到所有的子組件 * 這樣就可以在任何地方使用 `this.$router` and `this.$store` * @type {Vue$2} */ const app = new Vue({ router, store, render: h => h(App) }) /** * 導(dǎo)出 router and store. * 在這里不需要掛載到app上。這里和瀏覽器渲染不一樣 */ export { app, router, store }
這樣 服務(wù)端入口文件和客戶端入口文件便有了一個公共實例Vue, 和我們以前寫的vue實例差別不大,但是我們不會在這里將app mount到DOM上,因為這個實例也會在服務(wù)端去運行,這里直接將 app 暴露出去。
接下來創(chuàng)建路由router,創(chuàng)建vuex跟客戶端都差不多。詳細(xì)的可以參考我的項目...
到此,服務(wù)端渲染配置 就簡單介紹完了,下面我們啟動項目簡單的看下:
這里跟服務(wù)端界面一樣,不一樣的是url已經(jīng)不是之前的 #/而變成了請求形式 /
這樣每當(dāng)瀏覽器發(fā)送一個頁面的請求,會有服務(wù)器渲染出一個dom字符串返回,直接在瀏覽器段顯示,這樣就避免了瀏覽器端渲染的很多問題。
說起SSR,其實早在SPA (Single Page Application) 出現(xiàn)之前,網(wǎng)頁就是在服務(wù)端渲染的。服務(wù)器接收到客戶端請求后,將數(shù)據(jù)和模板拼接成完整的頁面響應(yīng)到客戶端。 客戶端直接渲染, 此時用戶希望瀏覽新的頁面,就必須重復(fù)這個過程, 刷新頁面. 這種體驗在Web技術(shù)發(fā)展的當(dāng)下是幾乎不能被接受的,于是越來越多的技術(shù)方案涌現(xiàn),力求 實現(xiàn)無頁面刷新或者局部刷新來達(dá)到優(yōu)秀的交互體驗。但是SEO卻是致命的,所以一切看應(yīng)用場景,這里只為大家提供技術(shù)思路,為vue開發(fā)提供多一種可能的方案。
最后再加一下前后端渲染build后 把netWork切到3G網(wǎng)絡(luò)下進(jìn)行響應(yīng)對比:
先看看瀏覽器端:加載dom頁面花了大約800s
在對比一下服務(wù)端渲染:加載dom頁面只花了8ms 整整快了10倍,用戶可能在網(wǎng)絡(luò)比較慢的情況下從遠(yuǎn)處訪問網(wǎng)站 - 或者通過比較差的帶寬。 這些情況下,盡量減少頁面請求數(shù)量,來保證用戶盡快看到基本的內(nèi)容。
補充一下一:一般高復(fù)雜度項目都會使用webpack的拆分,實現(xiàn)異步組件,按需加載,這樣可以有效提高時間。二:服務(wù)器渲染如此快有部分原因來自serviceworker,但是排除這部分,還是比瀏覽器端要快至少幾倍。 當(dāng)然,我們可以考慮首屏采用服務(wù)端渲染的方式,因為完全服務(wù)端渲染會考慮到很多問題,比如復(fù)雜均衡等等等
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/82163.html
平日學(xué)習(xí)接觸過的網(wǎng)站積累,以每月的形式發(fā)布。2017年以前看這個網(wǎng)址:http://www.kancloud.cn/jsfron... 03月份前端資源分享 1. Javascript 175453545 Redux compose and middleware 源碼分析 深入 Promise(二)——進(jìn)擊的 Promise Effective JavaScript leeheys blog -...
平日學(xué)習(xí)接觸過的網(wǎng)站積累,以每月的形式發(fā)布。2017年以前看這個網(wǎng)址:http://www.kancloud.cn/jsfron... 03月份前端資源分享 1. Javascript 175453545 Redux compose and middleware 源碼分析 深入 Promise(二)——進(jìn)擊的 Promise Effective JavaScript leeheys blog -...
平日學(xué)習(xí)接觸過的網(wǎng)站積累,以每月的形式發(fā)布。2017年以前看這個網(wǎng)址:http://www.kancloud.cn/jsfron... 03月份前端資源分享 1. Javascript 175453545 Redux compose and middleware 源碼分析 深入 Promise(二)——進(jìn)擊的 Promise Effective JavaScript leeheys blog -...
平日學(xué)習(xí)接觸過的網(wǎng)站積累,以每月的形式發(fā)布。2017年以前看這個網(wǎng)址:http://www.kancloud.cn/jsfron... 03月份前端資源分享 1. Javascript 175453545 Redux compose and middleware 源碼分析 深入 Promise(二)——進(jìn)擊的 Promise Effective JavaScript leeheys blog -...
閱讀 1025·2021-10-27 14:15
閱讀 2782·2021-10-25 09:45
閱讀 1945·2021-09-02 09:45
閱讀 3372·2019-08-30 15:55
閱讀 1810·2019-08-29 16:05
閱讀 3205·2019-08-28 18:13
閱讀 3120·2019-08-26 13:58
閱讀 457·2019-08-26 12:01