
一、是什么
Server-Side Rendering 我們稱其為SSR,意為服務端渲染
指由服務側完成頁面的 HTML 結構拼接的頁面處理技術,發送到瀏覽器,然后為其綁定狀態與事件,成為完全可交互頁面的過程
先來看看Web3個階段的發展史:
-
傳統服務端渲染SSR
-
單頁面應用SPA
-
服務端渲染SSR
傳統web開發
網頁內容在服務端渲染完成,⼀次性傳輸到瀏覽器

打開頁面查看源碼,瀏覽器拿到的是全部的dom結構
單頁應用SPA
單頁應用優秀的用戶體驗,使其逐漸成為主流,頁面內容由JS渲染出來,這種方式稱為客戶端渲染

打開頁面查看源碼,瀏覽器拿到的僅有宿主元素#app,並沒有內容
服務端渲染SSR
SSR解決方案,后端渲染出完整的首屏的dom結構返回,前端拿到的內容包括首屏及完整spa結構,應用激活后依然按照spa方式運行

看完前端發展,我們再看看Vue官方對SSR的解釋:
Vue.js 是構建客戶端應用程序的框架。默認情況下,可以在瀏覽器中輸出 Vue 組件,進行生成 DOM 和操作 DOM。然而,也可以將同一個組件渲染為服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最后將這些靜態標記"激活"為客戶端上完全可交互的應用程序
服務器渲染的 Vue.js 應用程序也可以被認為是"同構"或"通用",因為應用程序的大部分代碼都可以在服務器和客戶端上運行
我們從上門解釋得到以下結論:
-
Vue SSR是一個在SPA上進行改良的服務端渲染 -
通過
Vue SSR渲染的頁面,需要在客戶端激活才能實現交互 -
Vue SSR將包含兩部分:服務端渲染的首屏,包含交互的SPA
二、解決了什么
SSR主要解決了以下兩種問題:
-
seo:搜索引擎優先爬取頁面
HTML結構,使用ssr時,服務端已經生成了和業務想關聯的HTML,有利於seo -
首屏呈現渲染:用戶無需等待頁面所有
js加載完成就可以看到頁面視圖(壓力來到了服務器,所以需要權衡哪些用服務端渲染,哪些交給客戶端)
但是使用SSR同樣存在以下的缺點:
-
復雜度:整個項目的復雜度
-
庫的支持性,代碼兼容
-
性能問題
-
-
每個請求都是
n個實例的創建,不然會污染,消耗會變得很大 -
緩存
node serve、nginx判斷當前用戶有沒有過期,如果沒過期的話就緩存,用剛剛的結果。 -
降級:監控
cpu、內存占用過多,就spa,返回單個的殼
-
-
服務器負載變大,相對於前后端分離務器只需要提供靜態資源來說,服務器負載更大,所以要慎重使用
所以在我們選擇是否使用SSR前,我們需要慎重問問自己這些問題:
-
需要
SEO的頁面是否只是少數幾個,這些是否可以使用預渲染(Prerender SPA Plugin)實現 -
首屏的請求響應邏輯是否復雜,數據返回是否大量且緩慢
三、如何實現
對於同構開發,我們依然使用webpack打包,我們要解決兩個問題:服務端首屏渲染和客戶端激活
這里需要生成一個服務器bundle文件用於服務端首屏渲染和一個客戶端bundle文件用於客戶端激活

代碼結構 除了兩個不同入口之外,其他結構和之前vue應用完全相同
src
├── router
├────── index.js # 路由聲明
├── store
├────── index.js # 全局狀態
├── main.js # ⽤於創建vue實例
├── entry-client.js # 客戶端⼊⼝,⽤於靜態內容“激活”
└── entry-server.js # 服務端⼊⼝,⽤於⾸屏內容渲染
路由配置
import Vue from "vue"; import Router from "vue-router"; Vue.use(Router); //導出⼯⼚函數 export function createRouter() { return new Router({ mode: 'history', routes: [ // 客戶端沒有編譯器,這⾥要寫成渲染函數 { path: "/", component: { render: h => h('div', 'index page') } }, { path: "/detail", component: { render: h => h('div', 'detail page') } } ] }); }
主文件main.js
跟之前不同,主文件是負責創建vue實例的工廠,每次請求均會有獨立的vue實例創建
import Vue from "vue"; import App from "./App.vue"; import { createRouter } from "./router"; // 導出Vue實例⼯⼚函數,為每次請求創建獨⽴實例 // 上下⽂⽤於給vue實例傳遞參數 export function createApp(context) { const router = createRouter(); const app = new Vue({ router, context, render: h => h(App) }); return { app, router }; }
編寫服務端入口src/entry-server.js
它的任務是創建Vue實例並根據傳入url指定首屏
import { createApp } from "./main";
// 返回⼀個函數,接收請求上下⽂,返回創建的vue實例
export default context => {
// 這⾥返回⼀個Promise,確保路由或組件准備就緒
return new Promise((resolve, reject) => {
const { app, router } = createApp(context);
// 跳轉到⾸屏的地址
router.push(context.url);
// 路由就緒,返回結果
router.onReady(() => {
resolve(app);
}, reject);
});
};
編寫客戶端入口entry-client.js
客戶端入口只需創建vue實例並執行掛載,這⼀步稱為激活
import { createApp } from "./main";
// 創建vue、router實例
const { app, router } = createApp();
// 路由就緒,執⾏掛載
router.onReady(() => {
app.$mount("#app");
});
對webpack進行配置
安裝依賴
npm install webpack-node-externals lodash.merge -D
對vue.config.js進行配置
// 兩個插件分別負責打包客戶端和服務端 const VueSSRServerPlugin = require("vue-server-renderer/server-plugin"); const VueSSRClientPlugin = require("vue-server-renderer/client-plugin"); const nodeExternals = require("webpack-node-externals"); const merge = require("lodash.merge"); // 根據傳⼊環境變量決定⼊⼝⽂件和相應配置項 const TARGET_NODE = process.env.WEBPACK_TARGET === "node"; const target = TARGET_NODE ? "server" : "client"; module.exports = { css: { extract: false }, outputDir: './dist/'+target, configureWebpack: () => ({ // 將 entry 指向應⽤程序的 server / client ⽂件 entry: `./src/entry-${target}.js`, // 對 bundle renderer 提供 source map ⽀持 devtool: 'source-map', // target設置為node使webpack以Node適⽤的⽅式處理動態導⼊, // 並且還會在編譯Vue組件時告知`vue-loader`輸出⾯向服務器代碼。 target: TARGET_NODE ? "node" : "web", // 是否模擬node全局變量 node: TARGET_NODE ? undefined : false, output: { // 此處使⽤Node⻛格導出模塊 libraryTarget: TARGET_NODE ? "commonjs2" : undefined }, // https://webpack.js.org/configuration/externals/#function // https://github.com/liady/webpack-node-externals // 外置化應⽤程序依賴模塊。可以使服務器構建速度更快,並⽣成較⼩的打包⽂件。 externals: TARGET_NODE ? nodeExternals({ // 不要外置化webpack需要處理的依賴模塊。 // 可以在這⾥添加更多的⽂件類型。例如,未處理 *.vue 原始⽂件, // 還應該將修改`global`(例如polyfill)的依賴模塊列⼊⽩名單 whitelist: [/\.css$/] }) : undefined, optimization: { splitChunks: undefined }, // 這是將服務器的整個輸出構建為單個 JSON ⽂件的插件。 // 服務端默認⽂件名為 `vue-ssr-server-bundle.json` // 客戶端默認⽂件名為 `vue-ssr-client-manifest.json`。 plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()] }), chainWebpack: config => { // cli4項⽬添加 if (TARGET_NODE) { config.optimization.delete('splitChunks') } config.module .rule("vue") .use("vue-loader") .tap(options => { merge(options, { optimizeSSR: false }); }); } };
對腳本進行配置,安裝依賴
npm i cross-env -D
定義創建腳本package.json
"scripts": { "build:client": "vue-cli-service build", "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build", "build": "npm run build:server && npm run build:client" }
執行打包:npm run build
最后修改宿主文件/public/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>Document</title> </head> <body> <!--vue-ssr-outlet--> </body> </html>
是服務端渲染入口位置,注意不能為了好看而在前后加空格
安裝vuex
npm install -S vuex
創建vuex工廠函數
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export function createStore () { return new Vuex.Store({ state: { count:108 }, mutations: { add(state){ state.count += 1; } } }) }
在main.js文件中掛載store
import { createStore } from './store'
export function createApp (context) {
// 創建實例
const store = createStore()
const app = new Vue({
store, // 掛載
render: h => h(App)
})
return { app, router, store }
}
服務器端渲染的是應用程序的"快照",如果應用依賴於⼀些異步數據,那么在開始渲染之前,需要先預取和解析好這些數據
在store進行一步數據獲取
export function createStore() { return new Vuex.Store({ mutations: { // 加⼀個初始化 init(state, count) { state.count = count; }, }, actions: { // 加⼀個異步請求count的action getCount({ commit }) { return new Promise(resolve => { setTimeout(() => { commit("init", Math.random() * 100); resolve(); }, 1000); }); }, }, }); }
組件中的數據預取邏輯
export default { asyncData({ store, route }) { // 約定預取邏輯編寫在預取鈎⼦asyncData中 // 觸發 action 后,返回 Promise 以便確定請求結果 return store.dispatch("getCount"); } };
服務端數據預取,entry-server.js
import { createApp } from "./app";
export default context => {
return new Promise((resolve, reject) => {
// 拿出store和router實例
const { app, router, store } = createApp(context);
router.push(context.url);
router.onReady(() => {
// 獲取匹配的路由組件數組
const matchedComponents = router.getMatchedComponents();
// 若⽆匹配則拋出異常
if (!matchedComponents.length) {
return reject({ code: 404 });
}
// 對所有匹配的路由組件調⽤可能存在的`asyncData()`
Promise.all(
matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute,
});
}
}),
)
.then(() => {
// 所有預取鈎⼦ resolve 后,
// store 已經填充⼊渲染應⽤所需狀態
// 將狀態附加到上下⽂,且 `template` 選項⽤於 renderer 時,
// 狀態將⾃動序列化為 `window.__INITIAL_STATE__`,並注⼊ HTML
context.state = store.state;
resolve(app);
})
.catch(reject);
}, reject);
});
};
客戶端在掛載到應用程序之前,store 就應該獲取到狀態,entry-client.js
// 導出store const { app, router, store } = createApp(); // 當使⽤ template 時,context.state 將作為 window.__INITIAL_STATE__ 狀態⾃動嵌⼊到最終的 HTML // 在客戶端掛載到應⽤程序之前,store 就應該獲取到狀態: if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__); }
replaceState
-
replaceState(state: Object)
替換 store 的根狀態,僅用狀態合並或時光旅行調試。
客戶端數據預取處理,main.js
Vue.mixin({ beforeMount() { const { asyncData } = this.$options; if (asyncData) { // 將獲取數據操作分配給 promise // 以便在組件中,我們可以在數據准備就緒后 // 通過運⾏ `this.dataPromise.then(...)` 來執⾏其他任務 this.dataPromise = asyncData({ store: this.$store, route: this.$route, }); } }, });
修改服務器啟動文件
// 獲取⽂件路徑 const resolve = dir => require('path').resolve(__dirname, dir) // 第 1 步:開放dist/client⽬錄,關閉默認下載index⻚的選項,不然到不了后⾯路由 app.use(express.static(resolve('../dist/client'), {index: false})) // 第 2 步:獲得⼀個createBundleRenderer const { createBundleRenderer } = require("vue-server-renderer"); // 第 3 步:服務端打包⽂件地址 const bundle = resolve("../dist/server/vue-ssr-server-bundle.json"); // 第 4 步:創建渲染器 const renderer = createBundleRenderer(bundle, { runInNewContext: false, // https://ssr.vuejs.org/zh/api/#runinnewcontext template: require('fs').readFileSync(resolve("../public/index.html"), "utf8"), // 宿主⽂件 clientManifest: require(resolve("../dist/client/vue-ssr-clientmanifest.json")) // 客戶端清單 }); app.get('*', async (req,res)=>{ // 設置url和title兩個重要參數 const context = { title:'ssr test', url:req.url } const html = await renderer.renderToString(context); res.send(html) })
小結
-
使用
ssr不存在單例模式,每次用戶請求都會創建一個新的vue實例 -
實現
ssr需要實現服務端首屏渲染和客戶端激活 -
服務端異步獲取數據
asyncData可以分為首屏異步獲取和切換組件獲取-
首屏異步獲取數據,在服務端預渲染的時候就應該已經完成
-
切換組件通過
mixin混入,在beforeMount鈎子完成數據獲取
-
轉自:https://blog.csdn.net/weixin_44475093/article/details/112001279
