本文記錄一個實作服務端渲染html卡片服務的例子,使用vue-ssr加express實作服務
什麼是伺服器端渲染(SSR)
Vue.js 是建構用戶端應用程式的架構。預設情況下,可以在浏覽器中輸出 Vue 元件,進行生成 DOM 和操作 DOM。然而,也可以将同一個元件渲染為伺服器端的 HTML 字元串,将它們直接發送到浏覽器,最後将這些靜态标記"激活"為用戶端上完全可互動的應用程式。
搭建vue工程
利用 webpack 搭建一個簡單的 vue 開發環境
結構如下:

router 和 store 以及 vue 都采用了工廠函數來生成執行個體,這是為了友善代碼在後面的服務端渲染中進行複用,因為 “Node.js 伺服器是一個長期運作的程序。必須為每個請求建立一個新的 Vue 執行個體”
app.js:
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './routes'
import { createStore } from './stores'
import { sync } from 'vuex-router-sync'
// 導出一個工廠函數,用于建立新的
// 應用程式、router 和 store 執行個體
export function createApp () {
// 建立 router 和 store 執行個體
const router = createRouter()
const store = createStore()
// 同步路由狀态(route state)到 store
sync(store, router)
// 建立應用程式執行個體,将 router 和 store 注入
const app = new Vue({
router,
store,
render: h => h(App)
})
return { router, app, store }
// return { app }
}
實作服務端渲染
安裝:
npm install vue vue-server-renderer --save
實作服務端渲染,需增加如下 webpack 配置:
webpack.server.conf.js:
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const baseWebpackConfig = require('./webpack.client.conf')
const config = require('./config')
module.exports = merge(baseWebpackConfig, {
entry: './src/entry-server.js',
// 告知 `vue-loader` 輸送面向伺服器代碼(server-oriented code)。
target: 'node',
output: {
filename: 'server-bundle.js',
libraryTarget: 'commonjs2',
},
plugins: [
new VueSSRServerPlugin()
]
})
為服務端渲染準備的入口檔案:
entry-server.js:
import { createApp } from './app'
// 這裡的 context 是服務端渲染模闆時傳入的
export default context => {
// 因為有可能會是異步路由鈎子函數或元件,是以我們将傳回一個 Promise,
// 以便伺服器能夠等待所有的内容在渲染前,
// 就已經準備就緒。
return new Promise((resolve, reject) => {
const { app, router, store } = createApp()
const { url } = context
const { fullPath } = router.resolve(url).route
if (fullPath !== url) {
return reject({ url: fullPath })
}
router.push(url)
// 等到 router 将可能的異步元件和鈎子函數解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// 比對不到的路由,執行 reject 函數,并傳回 404
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// 執行所有元件中的異步資料請求
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)
})
}
編譯一下,運作 npm run build:server ,将會在 dist 目錄下得到 vue-ssr-server-bundle.json 檔案。該檔案包含了 webpack 打包生成的所有 chunk 并指定了入口。後面服務端會基于該檔案來做渲染。
編寫服務端渲染代碼,使用express做服務容器:
server.js:
const Vue = require('vue')
const express = require('express')
const bodyParser = require('body-parser')
const fs = require('fs')
const path = require('path')
const { createBundleRenderer } = require('vue-server-renderer')
const bundle = require('./dist/vue-ssr-server-bundle.json')
const server = express()
const renderer = createBundleRenderer(bundle, {
template: fs.readFileSync('./index.template.html', 'utf-8')
})
// 這個方法傳回一個僅僅用來解析json格式的中間件。這個中間件能接受任何body中任何Unicode編碼的字元。支援自動的解析gzip和 zlib。
server.use(bodyParser.json());
// 這個方法也傳回一個中間件,這個中間件用來解析body中的urlencoded字元,隻支援utf-8的編碼的字元。同樣也支援自動的解析gzip和 zlib。
server.use(bodyParser.urlencoded({ extended: false }));
// 服務端渲染
server.get('*', (req, res) => {
const context = { url: req.originalUrl }
renderer.renderToString(context, (err, html) => {
if (err) {
if (err.code === 404) {
res.status(404).end('Page not found')
} else {
res.status(500).end('Internal Server Error')
}
} else {
res.end(html)
}
})
})
server.listen(8081)
使用上面生成的檔案建立了一個 renderer 對象,然後調用其 renderToString 方法并傳入包含請求路徑的對象作為參數來進行渲染,最後将渲染好的資料即 html 傳回。
編寫服務端渲染的卡片模闆
robot_baike.vue
<template>
<div>
<span>
<p>{{data}}</p>
</span>
<div id="aaa" ></div>
<div v-html="bbb"></div>
</div>
</template>
<script>
let charts = [];
export default {
data(){
return {
data:null,
question:null,
bbb:""
};
},
methods: {
},
created() {
this.data = "馬雲";
charts = "[129.9, 171.5, 306.4, 429.2, 144.0, 176.0, 135.6, 248.5, 216.4, 194.1, 95.6, 54.4]";
this.bbb = "<script>setTimeout(function (){let chart = Highcharts.chart('aaa', {'chart':{'type':'column',},'series': [{'data':"+charts+"}]});},100);<\/script>";
},
}
</script>
<style >
</style>
文本内容可處理後直接渲染到頁面節點,但圖形js插件生成的圖形則需要在用戶端進行渲染。
可以将繪圖代碼放入一個 < script> 标簽,拼裝為字元串,用v-html拼入頁面中,則在用戶端會執行此繪圖代碼生成圖形。
最後配置下路由:
routes/inex.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export function createRouter () {
return new Router({
mode: 'history',
routes: [
{ path: '/baike', component: () => import('COMPONENTS/robot_baike') }
]
})
}
運作 npm run server 啟動服務端,打開 http://localhost:8081/baike 就可以看到效果了。