一、是什么
Server-Side Rendering
我們稱其為SSR
,意為服務端渲染
指由服務側完成頁面的 HTML
結構拼接的頁面處理技術,發送到瀏覽器,然后為其綁定狀態與事件,成為完全可交互頁面的過程
先來看看Web
3個階段的發展史:
-
傳統服務端渲染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
、ngin
x判斷當前用戶有沒有過期,如果沒過期的話就緩存,用剛剛的結果。 -
降級:監控
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