解密Vue SSR

做者:百度外賣 耿彩麗 李宗原
轉載請標明出處複製代碼


引言

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

剖析構建流程

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

Vue SSR構建流程

app.js入口文件vue

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


兩個entrynode

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


webpack打包構建ios

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

剖析運行流程

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

而後咱們能夠暫時移步到服務端的entry中了,這裏要作的就是拿到當前路由匹配的組件,調用組件裏定義的一個方法(官網取名叫asyncData)拿到初始化渲染的數據,而這個方法要作的也很簡單,就是去調用咱們vuex store中的方法去異步獲取數據web

接下來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 '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實現方案進行了介紹,若是感興趣或者有疑問,歡迎你們留言交流。

相關文章
相關標籤/搜索