解密Vue SSR

作者:百度外賣 耿彩麗 李宗原

轉載請標明出處

引言

最近筆者和小夥伴在研究Vue SSR,但是市面上充斥了太多的從0到1的文章,對大家理解這其中的原理幫助並不是很大,因此,本文將從Vue SSR的構建流程、運行流程、SSR的特點和利弊這幾方面對Vue SSR有一個較為詳細的介紹。最後還將附上一個筆者實現的去除Vue全家桶的Demo案例。


剖析構建流程

首先我們鎮上一張官網給出的構建圖:

Vue SSR構建流程

app.js入口文件

app.js是我們的通用entry,它的作用就是構建一個Vue的實例以供服務端和客戶端使用,注意一下,在純客戶端的程序中我們的app.js將會掛載實例到dom中,而在ssr中這一部分的功能放到了Client entry中去做了。

兩個entry

接下里我們來看Client entry和Server entry,這兩者分別是客戶端的入口和服務端的入口。Client entry的功能很簡單,就是掛載我們的Vue實例到指定的dom元素上;Server entry是一個使用export導出的函數。主要負責調用組件內定義的獲取數據的方法,獲取到SSR渲染所需數據,並存儲到上下文環境中。這個函數會在每一次的渲染中重複的調用

webpack打包構建

然後我們的服務端代碼和客戶端代碼通過webpack分別打包,生成Server Bundle和Client Bundle,前者會運行在伺服器上通過node生成預渲染的HTML字元串,發送到我們的客戶端以便完成初始化渲染;而客戶端bundle就自由了,初始化渲染完全不依賴它了。客戶端拿到服務端返回的HTML字元串後,會去「激活」這些靜態HTML,是其變成由Vue動態管理的DOM,以便響應後續數據的變化。


剖析運行流程

到這裡我們該談談ssr的程序是怎麼跑起來的了。首先我們得去構建一個vue的實例,也就是我們前面構建流程中說到的app.js做的事情,但是這裡不同於傳統的客戶端渲染的程序,我們需要用一個工廠函數去封裝它,以便每一個用戶的請求都能夠返回一個新的實例,也就是官網說到的避免交叉污染了

然後我們可以暫時移步到服務端的entry中了,這裡要做的就是拿到當前路由匹配的組件,調用組件里定義的一個方法(官網取名叫asyncData)拿到初始化渲染的數據,而這個方法要做的也很簡單,就是去調用我們vuex store中的方法去非同步獲取數據

接下來node伺服器如期啟動了,跑的是我們剛寫好的服務端entry里的函數。在這裡還要做的就是將我們剛剛構建好的Vue實例渲染成HTML字元串,然後將拿到的數據混入我們的HTML字元串中,最後發送到我們客戶端。

打開瀏覽器的network,我們看到了初始化渲染的HTML,並且是我們想要初始化的結構,且完全不依賴於客戶端的js文件了。再仔細研究研究,裡面有初始化的dom結構,有css,還有一個script標籤。script標籤里把我們在服務端entry拿到的數據掛載了window上。原來只是一個純靜態的HTML頁面啊,沒有任何的交互邏輯,所以啊,現在知道為啥子需要服務端跑一個vue客戶端再跑一個vue了,服務端的vue只是混入了個數據渲染了個靜態頁面,客戶端的vue才是去實現交互的!

chrome network

順著前面的思路,我們該看客戶端的entry了。在這裡客戶端拿到存在window中的數據混入我們客戶端的vuex中,然後分析數據去執行我們熟悉的其餘客戶端操作了。


SSR獨特之處

在SSR中,創建Vue實例、創建store和創建router都是套了一層工廠函數的,目的就是避免數據的交叉污染

在服務端只能執行生命周期中的created和beforeCreate,原因是在服務端是無法操縱dom的,所以可想而知其他的周期也就是不能執行的了。

服務端渲染和客戶端渲染不同,需要創建兩個entry分別跑在服務端和客戶端,並且需要webpack對其分別打包

SSR服務端請求不帶cookie,需要手動拿到瀏覽器的cookie傳給服務端的請求。實現方式戳這裡。

SSR要求dom結構規範,因為瀏覽器會自動給HTML添加一些結構比如tbody,但是客戶端進行混淆服務端放回的HTML時,不會添加這些標籤,導致混淆後的HTML和瀏覽器渲染的HTML不匹配。

性能問題需要多加關注。

  • vue.mixin、axios攔截請求使用不當,會內存泄漏。原因戳這裡
  • lru-cache向內存中緩存數據,需要合理緩存改動不頻繁的資源。

可能是把雙刃劍

SSR的優點

  • 更利於SEO。

不同爬蟲工作原理類似,只會爬取源碼,不會執行網站的任何腳本(Google除外,據說Googlebot可以運行javaScript)。使用了Vue或者其它MVVM框架之後,頁面大多數DOM元素都是在客戶端根據js動態生成,可供爬蟲抓取分析的內容大大減少。另外,瀏覽器爬蟲不會等待我們的數據完成之後再去抓取我們的頁面數據。服務端渲染返回給客戶端的是已經獲取了非同步數據並執行JavaScript腳本的最終HTML,網路爬中就可以抓取到完整頁面的信息。

  • 更利於首屏渲染

首屏的渲染是node發送過來的html字元串,並不依賴於js文件了,這就會使用戶更快的看到頁面的內容。尤其是針對大型單頁應用,打包後文件體積比較大,普通客戶端渲染載入所有所需文件時間較長,首頁就會有一個很長的白屏等待時間。

SSR的局限

  • 服務端壓力較大

本來是通過客戶端完成渲染,現在統一到服務端node服務去做。尤其是高並發訪問的情況,會大量佔用服務端CPU資源;

  • 開發條件受限

在服務端渲染中,created和beforeCreate之外的生命周期鉤子不可用,因此項目引用的第三方的庫也不可用其它生命周期鉤子,這對引用庫的選擇產生了很大的限制;

  • 學習成本相對較高

除了對webpack、Vue要熟悉,還需要掌握node、Express相關技術。相對於客戶端渲染,項目構建、部署過程更加複雜。


去除VUEX的SSR實踐

先附上demo地址,戳這裡!

說在前面:

  • vue-router不是必須的,不用router其實做個vue的preRender就可以了,完全沒必要做ssr;
  • vuex不是必須的,vuex是實現我們客戶端和服務端的狀態共享的關鍵,我們可以不使用vuex,但是我們得去實現一套數據預取的邏輯;

官網的demo大而全,集成了vue-router和vuex,想想我們的項目如果沒有使用到這兩者,光引入就又需要改造成本,這並不是我們想搞的「絲滑般」過渡,接下來筆者將帶領大家一步一步的做個「啥都沒有的」demo。

在此筆者的思路是:構造一個Vue的實例,那麼我們可以用這個實例的data來存儲我們的預取數據,而用methods中的方法去做數據的非同步獲取,這樣我們只在需要預取數據的組件中去調用這個方法就可以了

首先我們需要讓我們的組件「共享」這個EventBus,為此筆者簡單的封裝了一個plugin:

export default { install (Vue) { const EventBus = new Vue({ data () { return { list: [], nav: [] } }, methods: { getList () { // get list }, getNav () { // get nav } } }) Vue.prototype.$events = EventBus Vue.$events = EventBus }}

然後我們需要在main.js中export出我們的EventBus以便兩個entry使用。這樣我們的main.js就像下面這樣:

import Vue from vueimport App from ./Appimport EventBus from ./eventVue.use(EventBus)Vue.config.devtools = trueexport function createApp () { const app = new Vue({ // 注入 router 到根 Vue 實例 router, render: h => h(App) }) return { app, router, eventBus: app.$events }}

接下來是我們的兩個entry了。server用來匹配我們的組件並調用組件的asyncData方法去獲取數據,client用來將預渲染的數據存儲到我們eventBus中的data中。

// serverimport { createApp } from ./mainexport default context => { return new Promise((resolve, reject) => { const { app, eventBus, App } = createApp() // 這裡筆者的demo比較簡單,僅app組件需要預取數據,複雜業務可以遞歸遍歷哈; const matchedComponents = [App] Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({ eventBus }))).then(() => { context.state = eventBus._data resolve(app) }).catch(reject) })}// clientimport Vue from vueimport { createApp } from ./mainconst { app, eventBus } = createApp()if (window.__INITIAL_STATE__) { eventBus._data = window.__INITIAL_STATE__}app.$mount(#app)

然後我們需要改造我們的組件了,只需要定義一個async方法去調用EventBus中的方法獲取,考慮到服務端只會執行beforeCreate和created兩個生命周期而beforeCreate不能拿到data,所以我們需要在created中去做數據的獲取。

// 服務端渲染數據預取;asyncData ({ store, eventBus }) { return eventBus.getNav()}// 將服務端拿到的數據混入vue組件中;created () { this.nav = this.$events.nav}

然後是webpack的改造了,webpack的配置其實和純客戶端應用類似,為了區分客戶端和服務端兩個環境我們將配置分為base、client和server三部分,base就是我們的通用基礎配置,而client和server分別用來打包我們的客戶端和服務端代碼。

首先是webpack.server.conf.js,用於生成server bundle來傳遞給createBundleRenderer函數在node伺服器上調用,入口文件是我們的entry-server:

const webpack = require(webpack)const merge = require(webpack-merge)const nodeExternals = require(webpack-node-externals)const baseConfig = require(./webpack.base.conf.js)const VueSSRServerPlugin = require(vue-server-renderer/server-plugin)// 去除打包css的配置baseConfig.module.rules[1].options = module.exports = merge(baseConfig, { entry: ./src/entry-server.js, // 以 Node 適用方式導入 target: node, // 對 bundle renderer 提供 source map 支持 devtool: #source-map, output: { filename: server-bundle.js, libraryTarget: commonjs2 }, externals: nodeExternals({ whitelist: /.css$/ }), plugins: [ new webpack.DefinePlugin({ process.env.NODE_ENV: JSON.stringify(process.env.NODE_ENV || development), process.env.VUE_ENV: "server" }), // 這是將伺服器的整個輸出 // 構建為單個 JSON 文件的插件。 // 默認文件名為 `vue-ssr-server-bundle.json` new VueSSRServerPlugin() ]})

其次是webpack.client.conf.js,這裡我們可以根據官方的配置生成clientManifest,自動推斷和注入資源預載入,以及 css 鏈接 / script 標籤到所渲染的 HTML。入口是我們的client-server:

const webpack = require(webpack)const merge = require(webpack-merge)const base = require(./webpack.base.conf)const VueSSRClientPlugin = require(vue-server-renderer/client-plugin)const config = merge(base, { entry: { app: ./src/entry-client.js }, plugins: [ new webpack.DefinePlugin({ process.env.NODE_ENV: JSON.stringify(process.env.NODE_ENV || development), process.env.VUE_ENV: "client" }), new webpack.optimize.CommonsChunkPlugin({ name: vendor, minChunks: function (module) { return ( /node_modules/.test(module.context) && !/.css$/.test(module.request) ) } }), // 這將 webpack 運行時分離到一個引導 chunk 中, // 以便可以在之後正確注入非同步 chunk。 // 這也為你的 應用程序/vendor 代碼提供了更好的緩存。 new webpack.optimize.CommonsChunkPlugin({ name: manifest }), new VueSSRClientPlugin() ]})

從localhost中我們看到ssr預取的數據已經成功出來了,大功告成!


結語

本文介紹了Vue的SSR的構建和運行流程,也分析了SSR的特點和利弊,希望對大家了解SSR有一定的幫助。最後針對不使用vuex的SSR實現方案進行了介紹,如果感興趣或者有疑問,歡迎大家留言交流。

推薦閱讀:

前端日刊-2018.01.15
[小心得]多層次對象屬性查找-非遞歸
介紹一個導出CSS精靈圖動畫的AE腳本
React ?? 新的 Context API
你需要了解的 nginx 基礎配置

TAG:Vuejs | 前端開發 | SSRServerSideRendering |