做者:百度外賣 耿彩麗 李宗原
轉載請標明出處複製代碼
最近筆者和小夥伴在研究Vue SSR,可是市面上充斥了太多的從0到1的文章,對你們理解這其中的原理幫助並非很大,所以,本文將從Vue SSR的構建流程、運行流程、SSR的特色和利弊這幾方面對Vue SSR有一個較爲詳細的介紹。最後還將附上一個筆者實現的去除Vue全家桶的Demo案例。css
首先咱們鎮上一張官網給出的構建圖:html
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纔是去實現交互的!
順着前面的思路,咱們該看客戶端的entry了。在這裏客戶端拿到存在window中的數據混入咱們客戶端的vuex中,而後分析數據去執行咱們熟悉的其他客戶端操做了。
在SSR中,建立Vue實例、建立store和建立router都是套了一層工廠函數的,目的就是避免數據的交叉污染。
在服務端只能執行生命週期中的created和beforeCreate,緣由是在服務端是沒法操縱dom的,因此可想而知其餘的週期也就是不能執行的了。
服務端渲染和客戶端渲染不一樣,須要建立兩個entry分別跑在服務端和客戶端,而且須要webpack對其分別打包;
SSR服務端請求不帶cookie,須要手動拿到瀏覽器的cookie傳給服務端的請求。實現方式戳這裏。
SSR要求dom結構規範,由於瀏覽器會自動給HTML添加一些結構好比tbody,可是客戶端進行混淆服務端放回的HTML時,不會添加這些標籤,致使混淆後的HTML和瀏覽器渲染的HTML不匹配。
性能問題須要多加關注。
SSR的優勢
不一樣爬蟲工做原理相似,只會爬取源碼,不會執行網站的任何腳本(Google除外,聽說Googlebot能夠運行javaScript)。使用了Vue或者其它MVVM框架以後,頁面大多數DOM元素都是在客戶端根據js動態生成,可供爬蟲抓取分析的內容大大減小。另外,瀏覽器爬蟲不會等待咱們的數據完成以後再去抓取咱們的頁面數據。服務端渲染返回給客戶端的是已經獲取了異步數據並執行JavaScript腳本的最終HTML,網絡爬中就能夠抓取到完整頁面的信息。
首屏的渲染是node發送過來的html字符串,並不依賴於js文件了,這就會使用戶更快的看到頁面的內容。尤爲是針對大型單頁應用,打包後文件體積比較大,普通客戶端渲染加載全部所需文件時間較長,首頁就會有一個很長的白屏等待時間。
SSR的侷限
原本是經過客戶端完成渲染,如今統一到服務端node服務去作。尤爲是高併發訪問的狀況,會大量佔用服務端CPU資源;
在服務端渲染中,created和beforeCreate以外的生命週期鉤子不可用,所以項目引用的第三方的庫也不可用其它生命週期鉤子,這對引用庫的選擇產生了很大的限制;
除了對webpack、Vue要熟悉,還須要掌握node、Express相關技術。相對於客戶端渲染,項目構建、部署過程更加複雜。
先附上demo地址,戳這裏!
說在前面:
官網的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實現方案進行了介紹,若是感興趣或者有疑問,歡迎你們留言交流。