Server-Side Rendering
我們稱其為SSR
,意為伺服器端渲染
指由服務側完成頁面的 HTML
結構拼接的頁面處理技術,傳送到瀏覽器,然後為其繫結狀態與事件,成為完全可互動頁面的過程。【相關推薦:】
先來看看Web
3個階段的發展史:
網頁內容在伺服器端渲染完成,⼀次性傳輸到瀏覽器
開啟頁面檢視原始碼,瀏覽器拿到的是全部的dom
結構
單頁應用優秀的使用者體驗,使其逐漸成為主流,頁面內容由JS
渲染出來,這種方式稱為使用者端渲染
開啟頁面檢視原始碼,瀏覽器拿到的僅有宿主元素#app
,並沒有內容
SSR
解決方案,後端渲染出完整的首屏的dom
結構返回,前端拿到的內容包括首屏及完整spa
結構,應用啟用後依然按照spa
方式執行
看完前端發展,我們再看看Vue
官方對SSR
的解釋:
Vue.js 是構建使用者端應用程式的框架。預設情況下,可以在瀏覽器中輸出 Vue 元件,進行生成 DOM 和操作 DOM。然而,也可以將同一個元件渲染為伺服器端的 HTML 字串,將它們直接傳送到瀏覽器,最後將這些靜態標記"啟用"為使用者端上完全可互動的應用程式
伺服器渲染的 Vue.js 應用程式也可以被認為是"同構"或"通用",因為應用程式的大部分程式碼都可以在伺服器和使用者端上執行
我們從上門解釋得到以下結論:
Vue SSR
是一個在SPA
上進行改良的伺服器端渲染Vue SSR
渲染的頁面,需要在使用者端啟用才能實現互動Vue SSR
將包含兩部分:伺服器端渲染的首屏,包含互動的SPA
SSR主要解決了以下兩種問題:
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
"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> <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__); }
使用者端資料預取處理,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
勾點完成資料獲取(學習視訊分享:)
以上就是什麼是SSR?vue中怎麼實現SSR伺服器端渲染?的詳細內容,更多請關注TW511.COM其它相關文章!