vue-ssr在專案中的實踐
vue-ssr在專案中的實踐
寫在文前
由於前端腳手架、打包工具、Node等版本的多樣性,本文無法同時兼顧,文中所述皆基於以下技術棧進行。
腳手架:vue-cli3
打包工具:webpack4,整合在vue-cli3中,通過修改vue.config.js的方式進行配置
Node框架:koa2
簡介
伺服器端渲染,即採用“同構”的策略,在伺服器端對一部分前端程式碼進行渲染,減少瀏覽器對頁面的渲染量。
通常伺服器端渲染的優點和用途有以下幾點:
1.更好的SEO
2.更快的頁面載入速度
3.在伺服器端完成資料的載入
但需要注意,在伺服器端渲染提高客戶端效能的同時,也帶來了更高的伺服器負荷的問題。在專案開發時需要權衡其優點及缺點。
Vue專案中如何實現伺服器端渲染?
在做Vue-ssr之前的一些思考
1.Vue在頁面渲染時以Vue例項為基本單元,在伺服器端進行渲染時,是否也應對Vue例項進行渲染?
2.使用者與客戶端的關係是一對一,而與伺服器端的關係是多對一,如何避免多個使用者之間在伺服器端的資料共享的問題?
3.如何實現同構策略?即讓伺服器端能夠執行前端的程式碼?
4.伺服器端渲染的Vue專案,開發環境和生產環境分別應該如何部署?有何區別?
5.如何保證伺服器端渲染改造後的程式碼仍能通過訪問靜態資源的方式直接訪問到?
對於這些思考,將在文末進行回顧。
具體實現方案
Vue官方提供了【vue-server-renderer】包實現Vue專案的伺服器渲染,安裝方式如下:
npm install vue-server-renderer --save
在使用vue-server-renderer時需要注意以下一些問題:
1.vue-server-renderer版本須與vue保持一致
2.vue-server-renderer只能在node端進行執行,推薦node.js6+版本
一、最簡單的實現
vue-server-renderer為我們提供了一個【createRenderer】方法,支援對單一Vue例項進行渲染,並輸出渲染後的html字串或node可讀的stream流。
// 1.建立Vue例項
const Vue = require('vue');
const app = new Vue({
template: '<div></div>',
});
// 2.引入renderer方法
const renderer = require('vue-server-renderer').createRenderer();
// 3-1.將Vue例項渲染為html字串
renderer.renderToString(app, (err, html) => {});
// or
renderer.renderToString(app).then((html) => {}, (err) => {});
// 3-2.將Vue例項渲染為stream流
const renderStream = renderer.renderToStream(app);
// 通過訂閱事件,在回撥中進行操作
// event可取值'data'、'beforeStart'、'start'、'beforeEnd'、'end'、'error'等
renderStream.on(event, (res) => {});
但通常情況下,我們沒有必要在伺服器端建立Vue例項並進行渲染,而是需要對前端的Vue專案中每個SPA的Vue例項進行渲染,基於此,vue-server-renderer為我們提供了一套如下的伺服器端渲染方案。
二、完整的實現
完整的實現流程如下圖所示分為【模板頁】(HTML)、【客戶端】(Client Bundle)、【伺服器端】(Server Bundle)三個模組。三個模組功能如下:
模板頁:提供給客戶端和伺服器端渲染的html框架,令客戶端和伺服器端在該框架中進行頁面的渲染
客戶端:僅在瀏覽器端執行,向模板頁中注入js、css等靜態資源
伺服器端:僅在伺服器端執行,將Vue例項渲染為html字串,注入到模板頁的對應位置中
整個服務的構建流程分為以下幾步:
1.通過webpack將Vue應用打包為瀏覽器端可執行的客戶端Bundle;
2.通過webpack將Vue應用打包為Node端可執行的伺服器端Bundle;
3.Node端呼叫伺服器端Bundle渲染Vue應用,並將渲染好的html字串以及客戶端Bundle傳送至瀏覽器;
4.瀏覽器端接收到後,呼叫客戶端Bundle向頁面注入靜態資源,並與伺服器端渲染好的頁面進行匹配。
需要注意的是,客戶端與伺服器端渲染的內容需要匹配才能進行正常的頁面載入,一些頁面載入異常問題將在下文進行具體描述。
三、具體程式碼實現
1、Vue應用程式改造
SPA模式下,使用者與Vue應用是一對一的關係,而在SSR模式下,由於Vue例項是在伺服器端進行渲染,而伺服器是所有使用者共用的,使用者與Vue應用的關係變為了多對一。這就導致多個使用者共用同一個Vue例項,導致例項中的資料相互汙染。
針對這個問題,我們需要對Vue應用的入口進行改造,將Vue例項的建立改為“工廠模式”,在每次渲染的時候建立新的Vue例項,避免使用者共用同一個Vue例項的情況。具體改造程式碼如下:
// router.js
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);
export function createRouter() {
return new Router({
mode: 'history',
routes: [],
});
}
// store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export function createStore() {
return new Vuex.Store({
state,
actions: {},
mutations: {},
modules: {},
});
}
// main.js
import Vue from 'vue';
import App from './App.vue';
import {createRouter} from './router';
import {createStore} from './store';
export function createApp() {
const router = createRouter();
const store = createStore();
const app = new Vue({
router,
store,
render: (h) => h(App),
});
return {app, router, store};
}
需要注意的是,我們需要將vue-router、vuex等Vue例項內部使用的模組也配置為“工廠模式”,避免路由、狀態等在多個Vue例項間共用。
同時,由於我們在SSR過程中需要使用到客戶端和伺服器端兩個模組,因此需要配置客戶端、伺服器端兩個入口。
客戶端入口配置如下:
// entry-client.js
import {createApp} from './main';
const {app, router, store} = createApp();
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
router.onReady(() => {
app.$mount('#app');
});
在上文中我們提到,客戶端Bundle的功能是在瀏覽器端接收到伺服器渲染好的html字串後,向頁面中注入靜態資源以及頁面的二次渲染工作,因此我們在Vue應用的客戶端入口中,只需像之前一樣將Vue例項掛載到指定的html標籤上即可。
同時,伺服器端在渲染時如果有資料預取操作,會將store中的資料先注入到【window.__INITIAL STATE\_】,在客戶端中,我們需要將window.__INITIAL STATE\_中的值重新賦給store。
伺服器端入口配置如下:
// entry-server.js
import {createApp} from './main';
export default (context) => {
return new Promise((resolve, reject) => {
const {app, router, store} = createApp();
// 設定伺服器端 router 的位置
router.push(context.url);
// 等到 router 將可能的非同步元件和鉤子函式解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
// 匹配不到的路由,執行 reject 函式,並返回 404
if (!matchedComponents.length) {
return reject({
code: 404
});
}
Promise.all(matchedComponents.map((Component) => {
if (Component.extendOptions.asyncData) {
const result = Component.extendOptions.asyncData({
store,
route: router.currentRoute,
options: {},
});
return result;
}
})).then(() => {
// 狀態將自動序列化為 window.__INITIAL_STATE__,並注入 HTML。
context.state = store.state;
resolve(app);
}).catch(reject);
}, reject);
});
};
伺服器端需要根據使用者的請求,動態匹配需要渲染的Vue元件,並設定router和store等模組。
對於router,只需呼叫vue-router的push方法進行路由切換即可;
對於store,則需要檢測並呼叫Vue元件中的【asyncData】方法進行store的初始化,並將初始化後的state賦值給上下文,伺服器在進行渲染時會將上下文中的state序列化為window.__INITIAL STATE\_,並注入到html中。對於資料預取的操作和處理,我們將在下文【伺服器端資料預取】一節進行具體介紹。
2、Webpack打包邏輯配置
由於伺服器端渲染服務需要客戶端Bundle和伺服器端Bundle兩個包,因此需要利用webpack進行兩次打包,分別打包客戶端和伺服器端。這裡我們可以通過shell指令碼進行打包邏輯的編寫:
#!/bin/bash
set -e
echo "刪除舊dist檔案"
rm -rf dist
echo "打包SSR伺服器端"
export WEBPACK_TARGET=node && vue-cli-service build
echo "將伺服器端Json檔案移出dist"
mv dist/vue-ssr-server-bundle.json bundle
echo "打包SSR客戶端"
export WEBPACK_TARGET=web && vue-cli-service build
echo "將伺服器端Json檔案移回dist"
mv bundle dist/vue-ssr-server-bundle.json
在shell命令中,我們配置了【WEBPACK_TARGET】這一環境變數,為webpack提供可辨別客戶端/伺服器端打包流程的標識。
同時,vue-server-renderer為我們提供了【server-plugin】和【client-plugin】兩個webpack外掛,用於分別打包伺服器端和客戶端Bundle。以下是webpack配置檔案中,使用這兩個外掛進行打包的具體配置:
// vue.config.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
const merge = require('lodash.merge');
const TARGET_NODE = process.env.WEBPACK_TARGET === 'node';
const entry = TARGET_NODE ? 'server' : 'client';
const isPro = process.env.NODE_ENV !== 'development';
module.exports = {
/**
* 靜態資源在請求時,如果請求路徑為相對路徑,則會基於當前域名進行訪問
* 在本地開發時,為保證靜態資源的正常載入,在8080埠啟動一個靜態資源伺服器
* 該處理將會在第四小節《Node端開發環境配置》中進行詳細介紹
*/
publicPath: isPro ? '/' : 'http://127.0.0.1:8080/',
outputDir: 'dist',
pages: {
index: {
entry: `src/pages/index/entry-${entry}.js`,
template: 'public/index.html'
}
},
css: {
extract: isPro ? true : false,
},
chainWebpack: (config) => {
// 關閉vue-loader中預設的伺服器端渲染函式
config.module
.rule('vue')
.use('vue-loader')
.tap((options) => {
merge(options, {
optimizeSSR: false,
});
});
},
configureWebpack: {
// 需要開啟source-map檔案對映,因為伺服器端在渲染時,
// 會通過Bundle中的map檔案對映關係進行檔案的查詢
devtool: 'source-map',
// 伺服器端在Node環境中執行,需要打包為類Node.js環境可用包(使用Node.js require載入chunk)
// 客戶端在瀏覽器中執行,需要打包為類瀏覽器環境裡可用包
target: TARGET_NODE ? 'node' : 'web',
// 關閉對node變數、模組的polyfill
node: TARGET_NODE ? undefined : false,
output: {
// 配置模組的暴露方式,伺服器端採用module.exports的方式,客戶端採用預設的var變數方式
libraryTarget: TARGET_NODE ? 'commonjs2' : undefined,
},
// 外接化應用程式依賴模組。可以使伺服器構建速度更快
externals: TARGET_NODE ? nodeExternals({
whitelist: [/\.css$/],
}) : undefined,
plugins: [
// 根據之前配置的環境變數判斷打包為客戶端/伺服器端Bundle
TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin(),
],
},
};
結合webpack配置檔案的程式碼和註釋,我們再回到打包的shell指令碼中梳理打包流程。
1)打包伺服器端Bundle
首先將【WEBPACK_TARGET】環境變數設定為node,webpack會將入口entry設定為伺服器端入口【entry-server.js】,通過外掛【server-plugin】進行打包。
打包後會在dist資料夾下生成【vue-ssr-server-bundle.json】檔案(該名稱為預設名稱,可在外掛中進行設定),該檔案有三個屬性entry、files、maps。其中entry屬性是打包後的入口檔案路徑字串,files屬性是一組打包後的【檔案路徑-檔案內容 鍵值對】,編譯過的檔案的內容都會被存到該json檔案的files屬性中,而maps則是通過【source-map】編譯出的一組檔案資源配置資訊。
// vue-ssr-server-bundle.json
{
"entry": "js/index.[hash].js",
"files": {
"js/index.[hash].js": "",
},
"maps": {
"js/index.[hash].js": {}
}
}
2)將伺服器端打包後的檔案臨時移出dist資料夾
由於需要進行兩次打包,在打包客戶端的時候會將之前的dist資料夾刪除,為避免伺服器端Bundle丟失,需將其臨時移出dist資料夾。
3)打包客戶端Bundle
在打包客戶端時,將【WEBPACK_TARGET】環境變數修改為web,webpack會將入口entry設定為客戶端入口【entry-client.js】,通過外掛【client-plugin】進行打包。
打包後會在dist資料夾下生成前端專案打包後的靜態資原始檔,以及【vue-ssr-client-manifest.json】檔案,其中靜態資原始檔可部署至伺服器提供傳統SPA服務。而vue-ssr-client-manifest.json檔案中包含publicPath、all、initial、async、modules屬性,其作用分別如下:
publicPath:訪問靜態資源的根相對路徑,與webpack配置中的publicPath一致
all:打包後的所有靜態資原始檔路徑
initial:頁面初始化時需要載入的檔案,會在頁面載入時配置到preload中
async:頁面跳轉時需要載入的檔案,會在頁面載入時配置到prefetch中
modules:專案的各個模組包含的檔案的序號,對應all中檔案的順序
// vue-ssr-client-manifest.json
{
"publicPath": "/",
"all": [],
"initial": [],
"async": [],
"modules": {
"moduleId": [
fileIndex
]
}
}
4)將臨時移出dist的伺服器端Bundle移回dist資料夾
3、Node端生產環境配置
經過以上幾步打包流程,我們已經將專案打包為【vue-ssr-server-bundle.json】、【vue-ssr-client-manifest.json】、【前端靜態資源】三個部分,之後我們需要在Node端利用打包後的這三個模組內容進行伺服器端渲染工作。
1)Renderer、BundleRenderer的區別
vue-server-renderer中存在兩個用於伺服器端渲染的主要類【Renderer】、【BundleRenderer】。
在【最簡單的實現】一節我們提到過【createRenderer】方法,實際上就是建立Renderer物件進行渲染工作,該物件包含renderToString和renderToStream兩個方法,用於將Vue例項渲染成html字串或生成node可讀流。
而在【完整的實現】一節中,我們採用的是將專案打包為客戶端、伺服器端Bundle的方法,此時需要利用vue-server-renderer的另一個方法【createBundleRenderer】,建立BundleRenderer物件進行渲染工作。
// 原始碼中 vue-server-renderer/build.dev.js createBundleRenderer方法
function createBundleRenderer(bundle, rendererOptions) {
if ( rendererOptions === void 0 ) rendererOptions = {};
var files, entry, maps;
var basedir = rendererOptions.basedir;
// load bundle if given filepath
if (
typeof bundle === 'string' &&
/\.js(on)?$/.test(bundle) &&
path$2.isAbsolute(bundle)
) {
// 解析bundle檔案
}
entry = bundle.entry;
files = bundle.files;
basedir = basedir || bundle.basedir;
maps = createSourceMapConsumers(bundle.maps);
var renderer = createRenderer(rendererOptions);
var run = createBundleRunner(
entry,
files,
basedir,
rendererOptions.runInNewContext
);
return {
renderToString: function (context, cb) {
run(context).catch((err) => {}).then((app) => {
renderer.renderToString(app, context, (err, res) => {
cb(err, res);
});
});
},
renderToStream: function (context) {
run(context).catch((err) => {}).then((app) => {
renderer.renderToStream(app, context);
});
}
}
}
以上createBundleRenderer方法程式碼中可以看到,BundleRenderer物件同樣包含【renderToString】和【renderToStream】兩個方法,但與createRenderer方法不同,它接收的是伺服器端Bundle檔案或檔案路徑。在執行時會先判斷接收的是物件還是字串,如果為字串則將其作為檔案路徑去讀取檔案。在讀取到Bundle檔案後會對【Webpack打包邏輯配置】一節中所說的伺服器端Bundle的相關屬性進行解析。同時構建Renderer物件,呼叫Renderer物件的renderToString和renderToStream方法。
可以看出,BundleRenderer和Renderer的區別,僅在於多一步Bundle解析的過程,而後仍使用Renderer進行渲染。
2)程式碼實現
在瞭解到區別後,我們將在這裡採用BundleRenderer物件進行伺服器端渲染,程式碼如下:
// prod.ssr.js
const fs = require('fs');
const path = require('path');
const router = require('koa-router')();
const resolve = file => path.resolve(__dirname, file);
const { createBundleRenderer } = require('vue-server-renderer');
const bundle = require('vue-ssr-server-bundle.json');
const clientManifest = require('vue-ssr-client-manifest.json');
const renderer = createBundleRenderer(bundle, {
runInNewContext: false,
template: fs.readFileSync(resolve('index.html'), 'utf-8'),
clientManifest,
});
const renderToString = (context) => {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
err ? reject(err) : resolve(html);
});
});
};
router.get('*', async (ctx) => {
let html = '';
try {
html = await renderToString(ctx);
ctx.body = html;
} catch(e) {}
});
module.exports = router;
在程式碼中可以看出整個渲染流程分為三步:
1.獲取伺服器端、客戶端、模板檔案,通過createBundleRenderer方法構建BundleRenderer物件;
2.接收到使用者請求,呼叫renderToString方法並傳入請求上下文,此時伺服器端渲染服務會呼叫伺服器端入口檔案entry-server.js進行頁面渲染;
3.將渲染後的html字串配置到response的body中,返回到瀏覽器端。
4、Node端開發環境配置
Vue官方只提供了針對Vue例項和打包後的Bundle包進行伺服器端渲染的方案,但在開發環境中我們會面臨以下幾個問題:
1)webpack將打包後的資原始檔存放在了記憶體中,如何獲取到打包後的Bundle的json檔案?
2)如何在開發環境中同時打包和執行客戶端與伺服器端?
在此,我們採用的策略是使用webpack啟動開發環境的前端專案,通過http請求獲取到存在記憶體中的客戶端靜態資源【vue-ssr-client-manifest.json】;同時在Node中,使用【 @vue/cli-service/webpack.config】獲取到伺服器端的webpack配置,利用webpack包直接進行伺服器端Bundle的打包操作,監聽並獲取到最新的【vue-ssr-server-bundle.json】檔案。這樣,我們就獲取到了客戶端與伺服器端檔案,之後的流程則與生產環境中相同。
首先,我們來看一下npm命令的配置:
// package.json
{
"scripts": {
"serve": "vue-cli-service serve",
"server:dev": "export NODE_ENV=development WEBPACK_TARGET=node SSR_ENV=dev && node --inspect server/bin/www",
"dev": "concurrently \"npm run serve\" \"npm run server:dev\" "
}
}
serve命令是採用客戶端模式啟動前端服務,webpack會在開發環境打包出客戶端Bundle,並存放在記憶體中;
server:dev命令通過設定環境變數【NODE_ENV】與【WEBPACK_TARGET】以獲取開發環境中伺服器端Bundle打包的webpack配置,通過設定環境變數【SSR_ENV】以使node應用程式識別當前環境為開發環境;
dev命令則是開發環境的執行命令,通過concurrently命令雙程式執行serve命令和server:dev命令。
接下來,我們來看一下開發環境的伺服器端渲染服務程式碼:
const webpack = require('webpack');
const axios = require('axios');
const MemoryFS = require('memory-fs');
const fs = require('fs');
const path = require('path');
const Router = require('koa-router');
const router = new Router();
// webpack配置檔案
const webpackConf = require('@vue/cli-service/webpack.config');
const { createBundleRenderer } = require("vue-server-renderer");
const serverCompiler = webpack(webpackConf);
const mfs = new MemoryFS();
serverCompiler.outputFileSystem = mfs;
// 監聽檔案修改,實時編譯獲取最新的 vue-ssr-server-bundle.json
let bundle;
serverCompiler.watch({}, (err, stats) => {
if (err) {
throw err;
}
stats = stats.toJson();
stats.errors.forEach(error => console.error(error));
stats.warnings.forEach(warn => console.warn(warn));
const bundlePath = path.join(
webpackConf.output.path,
'vue-ssr-server-bundle.json',
);
bundle = JSON.parse(mfs.readFileSync(bundlePath, 'utf-8'));
console.log('New bundle generated.');
})
const handleRequest = async ctx => {
if (!bundle) {
ctx.body = '等待webpack打包完成後再訪問';
return;
}
// 獲取最新的 vue-ssr-client-manifest.json
const clientManifestResp = await axios.get(`http://localhost:8080/vue-ssr-client-manifest.json`);
const clientManifest = clientManifestResp.data;
const renderer = createBundleRenderer(bundle, {
runInNewContext: false,
template: fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8'),
clientManifest,
});
return renderer;
}
const renderToString = (context, renderer) => {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
err ? reject(err) : resolve(html);
});
});
};
router.get('*', async (ctx) => {
const renderer = await handleRequest(ctx);
try {
const html = await renderToString(ctx, renderer);
console.log(html);
ctx.body = html;
} catch(e) {}
});
module.exports = router;
從程式碼中可以看出,開發環境的node伺服器端渲染服務流程和生產環境的基本一致,區別在於客戶端、伺服器端Bundle的獲取方式不同。
在生產環境中,node直接讀取本地打包好的靜態資源;
而在開發環境中,首先利用axios傳送http請求,獲取到前端專案打包在記憶體中的客戶端Bundle。同時利用【 @vue/cli-service/webpack.config】包獲取到當前環境(NODE_ENV=development WEBPACK_TARGET=node SSR_ENV=dev)下的webpack配置,使用webpack包和該webpack配置直接在當前node程式中執行伺服器端,並從中獲取到伺服器端Bundle。
後續的流程則與生產環境相同。
5、Node應用配置
到此為止,我們已經配置了伺服器端渲染所需的基本檔案,當然還需要一個node應用來進行服務的啟動。
// app.js
const Koa = require('koa');
const app = new Koa();
const path = require('path');
const koaStatic = require('koa-static');
const koaMount = require('koa-mount');
const favicon = require('koa-favicon');
const isDev = process.env.SSR_ENV === 'dev';
// routes
const ssr = isDev ? require('./dev.ssr') : require('./prod.ssr');
// Static File Server
const resolve = file => path.resolve(__dirname, file);
app.use(favicon(resolve('./favicon.ico')));
app.use(koaMount('/', koaStatic(resolve('../public'))));
app.use(ssr.routes(), ssr.allowedMethods());
module.exports = app;
在node入口檔案中,根據環境變數【SSR_ENV】判斷當前環境為開發環境還是生產環境,並呼叫對應的伺服器端渲染檔案。
需要注意的是,如果webpack中配置的publicPath為相對路徑的話,在客戶端向頁面注入相對路徑的靜態資源後,瀏覽器會基於當前域名/IP訪問靜態資源。如果伺服器沒有做過其他代理(除該node服務以外的代理),這些靜態資源的請求會直接傳到我們的node應用上,最便捷的方式是在node應用中搭建一個靜態資源伺服器,對專案打包後的靜態資源(js、css、png、jpg等)進行代理,在此使用的是【koa-mount】和【koa-static】中介軟體。同時,還可以使用【koa-favicon】中介軟體掛載favicon.ico圖示。
伺服器端資料預取
伺服器端資料預取,是在伺服器端對Vue應用進行渲染的時候,將資料注入到Vue例項中的功能,在以下兩種情況下比較常用:
1.頁面初始化時的資料量較大,影響首屏載入速度
2.部分資料在瀏覽器端無法獲取到
針對資料預取,官方vue-server-renderer包提供的方案主要分為兩個步驟:
1.伺服器端資料預取
伺服器端資料預取,主要是針對客戶端資料讀取慢導致首屏載入卡頓的問題。是在伺服器端的Vue例項渲染完成後,將資料注入到Vue例項的store中,程式碼可回顧【Vue應用程式改造】一節,具體流程如下:
1)將store改為工廠模式,這個已在上文中講過,不再贅述;
2)在vue例項中註冊靜態方法asyncData,提供給伺服器端進行呼叫,該方法的作用即呼叫store中的action方法,調取介面獲得資料;
// vue元件檔案
export default Vue.extend({
asyncData({store, route, options}) {
return store.dispatch('fetchData', {
options,
});
},
});
3)在伺服器端入口【entry-server.js】中呼叫asyncData方法獲取資料,並將資料儲存到【window.__INITIAL STATE\_】中,該配置在上文的【entry-server.js】檔案配置中可見;
4)在客戶端入口【entry-client.js】中將【window.__INITIAL STATE\_】中的資料重新掛載到store中。
// entry-client.js
const {app, router, store} = createApp();
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
router.onReady(() => {
app.$mount('#app');
});
2.客戶端資料預取
客戶端資料預取,其實是作為伺服器端資料預取的補充。針對場景是在伺服器端將渲染完成的頁面交付給瀏覽器端後,路由切換等工作也隨之由瀏覽器端的vue虛擬路由接管,而不會再向伺服器端傳送頁面請求,導致切換到新的頁面後並不會觸發伺服器端資料預取的問題。
針對這個問題,客戶端資料預取的策略是在客戶端入口【entry-client.js】中進行操作,當檢測到路由切換時優先進行資料調取(實際上這裡是在客戶端中複製伺服器端資料預取的操作流程),在資料載入完成後再進行vue應用的掛載。
具體我們需要對【entry-client.js】進行改造:
// entry-client.js
const {app, router, store} = createApp();
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
router.onReady(() => {
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();
}
Promise.all(activated.map(c => {
if (c.extendOptions.asyncData) {
return c.extendOptions.asyncData({
store,
route: to,
options: {},
});
}
})).then(() => {
next();
}).catch(next);
})
app.$mount('#app');
});
注意事項
一、頁面載入異常問題
由於伺服器端渲染後的html字串傳送到瀏覽器端之後,客戶端需要對其模板進行匹配,如果匹配不成功則無法正常渲染頁面,因此在一些情況下,會產生頁面載入異常的問題,主要有以下幾類。
1.模板頁中缺少客戶端或伺服器端可識別的渲染標識
該問題會影響客戶端的靜態資源注入或伺服器端對Vue例項的渲染工作。對於客戶端來說,一般需要可識別的h5標籤元素進行掛載,本文中是採用一個id為app的div標籤;而對於伺服器端來說,需要一個官方vue-server-renderer包可識別的註釋標識,即。完整的模板頁程式碼如下:
// 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>模板頁</title>
</head>
<body>
<div id="app"><!--vue-ssr-outlet--></div>
</body>
</html>
2.客戶端與伺服器端路由不同
在使用者向伺服器端傳送/a路由頁面請求,而伺服器端將/b路由對應的元件渲染成html字串並返回給瀏覽器端時,便會出現路由不匹配的問題。客戶端在瀏覽器端檢測出渲染後的Vue路由與當前瀏覽器中的路由不一致,會重新將頁面切換為/a路由下的頁面,導致頁面二次重新整理。
3.頁面靜態資源載入失敗
由於在頁面靜態資源使用相對路徑時,瀏覽器會基於當前域名/IP進行靜態資源的請求,因此會向我們的Node服務進行靜態資源的請求。如果我們只做了伺服器端渲染服務,而沒有搭建靜態資源伺服器等對靜態資源進行代理,則會出現靜態資源載入失敗的問題。
4.H5標籤書寫不完整
伺服器端在進行頁面渲染時,會對H5標籤進行自動補全,如
相關文章
- TypeScript在react專案中的實踐TypeScriptReact
- TypeScript在node專案中的實踐TypeScript
- DevOps 在企業專案中的實踐落地dev
- Redis在Web專案中的應用與實踐RedisWeb
- Redis 在 Web 專案中的應用與實踐RedisWeb
- 【主流技術】ElasticSearch 在 Spring 專案中的實踐ElasticsearchSpring
- es6非同步方法在專案中的實踐非同步
- Effective Dart 文件註釋在Flutter專案中的實踐DartFlutter
- [前端漫談]Git 在專案中的完全控制實踐前端Git
- 測試驅動開發在專案中的實踐
- Vue 在大型專案中的架構設計和最佳實踐Vue架構
- 專案實戰之gradle在實際專案中的使用Gradle
- AI大模型+低程式碼,在專案管理中的應用實踐AI大模型專案管理
- 專案實戰之Rxjava、RxBinding在實際專案中的使用RxJava
- React Native在Android當中實踐(三)——整合到Android專案當中React NativeAndroid
- JWT 在專案中的實際使用JWT
- 在不同的專案管理實踐中,如何提高運營效率和創收?專案管理
- MapStruct在專案中封裝實踐-帶原始碼Struct封裝原始碼
- mobx專案實踐
- vue實踐06-專案實踐Vue
- C 語言專案中標頭檔案包含的最佳實踐
- Golang專案的測試實踐Golang
- Android執行緒池的原理以及專案中實踐Android執行緒
- axios在vue中的實踐iOSVue
- Elasticsearch在Laravel中的實踐ElasticsearchLaravel
- vue專案實踐004~~~一籃子的實踐技巧Vue
- 微服務專案實踐之中建專案微服務
- maven 專案轉化成 gradle 專案實踐MavenGradle
- 專案去O實踐
- Git在專案中的那些實操(持續更新...)Git
- 在專案管理中,什麼是可實現的?專案管理
- Citypos專案的docker化部署實踐Docker
- React中型專案的優化實踐React優化
- 2023 年的 Web Worker 專案實踐Web
- 什麼是專案管理中的里程碑?如何實踐?專案管理
- 大型開發專案中 git 工作流的最佳實踐Git
- Immutable 操作在 React 中的實踐React
- 協程在RN中的實踐