作者:百度外賣 耿彩麗 李宗原
轉載請標明出處複製程式碼
引言
最近筆者和小夥伴在研究Vue SSR,但是市面上充斥了太多的從0到1的文章,對大家理解這其中的原理幫助並不是很大,因此,本文將從Vue SSR的構建流程、執行流程、SSR的特點和利弊這幾方面對Vue SSR有一個較為詳細的介紹。最後還將附上一個筆者實現的去除Vue全家桶的Demo案例。
剖析構建流程
首先我們鎮上一張官網給出的構建圖:
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才是去實現互動的!
順著前面的思路,我們該看客戶端的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 'vue'
import App from './App'
import EventBus from './event'
Vue.use(EventBus)
Vue.config.devtools = true
export 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中。
// server
import { createApp } from './main'
export 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)
})
}
// client
import Vue from 'vue'
import { createApp } from './main'
const { 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實現方案進行了介紹,如果感興趣或者有疑問,歡迎大家留言交流。