vue用法指南05(vue服務端渲染詳解)
今天來說說vue的服務端渲染。
至於為什麼要用服務端渲染,以及服務端渲染的好處?這個問題其實在官網上寫的很詳細,我截兩張圖,給大家參考一下。


在說服務端渲染之前,我們先來說說預渲染。

預渲染的使用也非常簡單,不僅僅是靜態的內容,非同步的請求也能渲染,但是它無法實時動態的編譯HTML。
華麗的分割線,下面根據官方文件來過一遍vue的服務端渲染。當然,只是單純的想用一下服務端渲染的話,官網建議我們使用nuxt框架。(這裡不做介紹,如果大家之前沒有接觸過的話,可以參考一下我的部落格demo( https://gitee.com/yeshaojun/blog ))
文章中的案例程式碼我到時候會放在碼雲上,大家可以下載參考一下(地址在最後)。
1.Vue服務端渲染的基本用法
我們先看一個最簡單的案例

這裡面的核心庫是vue-server-renderer,它可以把vue例項物件,渲染成html。express是node的一個第三方庫,作用是啟動一個服務。
我們來梳理一下邏輯。
當我們訪問頁面的時候,會生成一個vue的例項,然後我們把這個例項傳給vue-server-renderer,然後它會進行編譯,在返回給我們一個html,然後我們再顯示到頁面上。

理清楚了邏輯之後,我們再看下一個例子。
2.Vue服務端渲染基本實現
我們來實現一個簡單的vue服務端渲染。
先來看demo的結構。(依賴如果安裝不上,可以把防毒軟體關掉然後刪除node_modules重新試一下)

先解釋一下為什麼要有client和server兩個檔案,server負責處理vue的例項,然後將結果傳給vue-server-renderer,client負責掛載到html上。(大家可以看一下之前的理邏輯的圖,或者一會看程式碼清楚了)
我們一個個檔案來分析。
build檔案中是webpack的配置檔案(如果大家對webpack不太熟悉,可以先看我之前的文章,或者直接拿來用就行,我這裡也用的是官方的demo)
在配置檔案中,有一個外掛需要注意一下(VueSSRServerPlugin),它會自動生成json來對應我們的打包檔案。
路由配置檔案,import寫法也是官方推薦的程式碼分割寫法,可以實現懶載入優化效能。
// router.js import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) const home = () => import('../views/home.vue') const test1 = () => import('../views/test1.vue') const test2 = () => import('../views/test2.vue') const test3 = () => import('../views/test3.vue') export function createRouter () { return new Router({ mode: 'history', fallback: false, routes: [ { path: '/', component: home }, { path: '/test1', component: test1 }, { path: '/test2', component: test2 }, { path: '/test3', component: test3 } ] }) }
store資料夾,目前只是一個空架子,裡面是沒有內容的,暫時先不講。
views資料夾下是4個頁面,每個頁面的內容類似,只有如下的一句話。
<template> <div> {{msg}} </div> </template> <script> export default { data () { return { msg: 'this is home' } } } </script>
app.js檔案,暴露一個建立vue例項的工廠方法。
import Vue from 'vue' import App from './App.vue' import { createStore } from './store' import { createRouter } from './router' import { sync } from 'vuex-router-sync' export function createApp () { const store = createStore() const router = createRouter() sync(store, router) const app = new Vue({ router, store, render: h => h(App) }) return { app, router, store } }
entry-client.js這個檔案也很簡單,只是實現一下掛載。
import 'es6-promise/auto' import { createApp } from './app' const { app, router } = createApp() router.onReady(() => { app.$mount('#app') })
entry-client.js 這個檔案要實現vue例項的建立,元件的載入,並把結果暴露出去。
import { createApp } from './app' export default context => { return new Promise((resolve, reject) => { const { app, router } = createApp() // context 是express傳入的請求引數 const { url } = context const { fullPath } = router.resolve(url).route if (fullPath !== url) { return reject({ url: fullPath }) } // 設定伺服器端 router 的位置 router.push(url) // 等到 router 將可能的非同步元件和鉤子函式解析完 router.onReady(() => { const matchedComponents = router.getMatchedComponents() // no matched routes if (!matchedComponents.length) { return reject({ code: 404 }) } // Promise 應該 resolve 應用程式例項,以便它可以渲染 resolve(app) }, reject) }) }
模板檔案, 注意(模板裡面的註釋一定要寫,因為vue-ssr-server會根據這個註釋來進行掛載)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>{{title}}</title> </head> <body> <!--vue-ssr-outlet--> </body> </html>
server.js檔案
const fs = require('fs') const path = require('path') const express = require('express') const resolve = file => path.resolve(__dirname, file) const { createBundleRenderer } = require('vue-server-renderer') const app = express() const serve = (path, cache) => express.static(resolve(path), { maxAge: cache && 1000 * 60 * 60 * 24 * 30 }) // 設定靜態資源 app.use('/dist', serve('./dist', true)) function createRenderer (bundle, options) { return createBundleRenderer(bundle, Object.assign(options, { basedir: resolve('./dist'), runInNewContext: false })) } // 引入模板檔案,fs.readFileSync讀取檔案內容 const templatePath = resolve('./src/index.template.html') const template = fs.readFileSync(templatePath, 'utf-8') // 這裡只需要引入json檔案,因為VueSSRServerPlugin會幫我們做對映 const bundle = require('./dist/vue-ssr-server-bundle.json') const clientManifest = require('./dist/vue-ssr-client-manifest.json') // 官方用法,可把clien和server檔案傳入,vue-ssr-server會自動做處理 const renderer = createRenderer(bundle, { template, clientManifest }) function render (req, res) { const context = { title: 'ssr demo', // default title url: req.url } renderer.renderToString(context, (err, html) => { if (err) { res,send(err) } res.send(html) }) } app.get('*',render) // 埠監聽 const port = process.env.PORT || 8080 app.listen(port, () => { console.log(`server started at localhost:${port}`) })
我們先來看一下結果。


我們再來理一下demo2的邏輯。(建議對著demo的圖看)
第一步生成一個vue例項。
我們用工廠模式來生成vue例項,至於為什麼要這麼做,官方也給瞭解釋。

第二步將例項傳給vue-server-render
我們通過webpack打包entry-server.js檔案,並在server.js檔案中引入。
第三步接收html以及第四步顯示頁面
我們通過模板,以及entry-client.js來掛載
這麼一來,是不是也不復雜。
3.Vue服務端渲染非同步實現
我們再進一步,通過vuex把服務端渲染完成。
在store下的檔案。
// actions import axios from 'axios' export default { TEST_LIST: ({ commit }) => { return axios.get('https://api.yeshaojun.com/api/article/list?page=1&pageSize=10').then(res => { commit('TEST_LIST', res.data.data) }) } }
// mutations export default { TEST_LIST: (state, item) => { state.list = item } }
// index.js import Vue from 'vue' import Vuex from 'vuex' import actions from './actions' import mutations from './mutations' import getters from './getters' Vue.use(Vuex) export function createStore () { return new Vuex.Store({ state: { list: [] }, actions, mutations, getters }) }
修改home.vue檔案
<template> <div> {{msg}} <ul> <li v-for="(item,idx) in list" :key="idx"> {{item.title}} </li> </ul> </div> </template> <script> export default { data () { return { msg: 'this is home' } }, computed: { list () { return this.$store.state.list } }, asyncData ({ store }) { return store.dispatch('TEST_LIST') } } </script>
接下來,我們再來修改client和server。
// client import Vue from 'vue' import 'es6-promise/auto' import { createApp } from './app' // 將獲取資料操作分配給 promise // 以便在元件中,我們可以在資料準備就緒後 // 通過執行 `this.dataPromise.then(...)` 來執行其他任務 Vue.mixin({ beforeRouteUpdate (to, from, next) { const { asyncData } = this.$options if (asyncData) { asyncData({ store: this.$store, route: to }).then(next).catch(next) } else { next() } } }) const { app, router, store } = createApp() // 將clien的store與sever的store同步 if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) } router.onReady(() => { // 新增路由鉤子函式,用於處理 asyncData. // 在初始路由 resolve 後執行, // 以便我們不會二次預取(double-fetch)已有的資料。 // 使用 `router.beforeResolve()`,以便確保所有非同步元件都 resolve。 router.beforeResolve((to, from, next) => { const matched = router.getMatchedComponents(to) const prevMatched = router.getMatchedComponents(from) // 我們只關心非預渲染的元件 // 所以我們對比它們,找出兩個匹配列表的差異元件 let diffed = false const activated = matched.filter((c, i) => { return diffed || (diffed = (prevMatched[i] !== c)) }) if (!activated.length) { return next() } // 這裡如果有載入指示器 (loading indicator),就觸發 Promise.all(activated.map(c => { if (c.asyncData) { return c.asyncData({ store, route: to }) } })).then(() => { // 停止載入指示器(loading indicator) next() }).catch(next) }) app.$mount('#app') })
import { createApp } from './app' const isDev = process.env.NODE_ENV !== 'production' export default context => { return new Promise((resolve, reject) => { const s = isDev && Date.now() const { app, router, store } = createApp() const { url } = context const { fullPath } = router.resolve(url).route if (fullPath !== url) { return reject({ url: fullPath }) } // set router's location router.push(url) // wait until router has resolved possible async hooks router.onReady(() => { const matchedComponents = router.getMatchedComponents() // no matched routes if (!matchedComponents.length) { return reject({ code: 404 }) } // 對所有匹配的路由元件呼叫 `asyncData()` Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({ store, route: router.currentRoute }))).then(() => { // 在所有預取鉤子(preFetch hook) resolve 後, // 我們的 store 現在已經填充入渲染應用程式所需的狀態。 // 當我們將狀態附加到上下文, // 並且 `template` 選項用於 renderer 時, // 狀態將自動序列化為 `window.__INITIAL_STATE__`,並注入 HTML。 context.state = store.state resolve(app) }).catch(reject) }, reject) }) }
我們來看一下結果。


我們再來理一下demo3的邏輯,demo3的邏輯其實跟demo2是一樣的,只不過在server和client加入了判斷,判斷是否有非同步請求,如果有,則先執行非同步,然後再進行後面的操作。
因為client和server中的store的資料是不一樣的,所以最後需要同步一下。
到此,vue的服務端渲染就完成了,但是其實還有很多問題,比如我們在開發的時候希望是npm run dev,啟動一個服務,部署的時候再build。
vue官方有一個demo,裡面有比較詳細的配置。本文也是參考官方文件和官方demo寫出來的,大家可以先看官方文件,對照著來學習。
官方文件: https://ssr.vuejs.org/zh/
官方demo: https://github.com/vuejs/vue-hackernews-2.0
文章中demo地址: https://gitee.com/yeshaojun/vue-ssr
如果覺的有收穫,別忘了點贊哈!