本篇內容介紹了“Vue項目中如何實現服務器端渲染”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
由于前端腳手架、打包工具、Node等版本的多樣性,本文無法同時兼顧,文中所述皆基于以下技術棧進行。
腳手架:vue-cli3
打包工具:webpack4,集成在vue-cli3中,通過修改vue.config.js的方式進行配置
Node框架:koa2
服務器端渲染,即采用“同構”的策略,在服務器端對一部分前端代碼進行渲染,減少瀏覽器對頁面的渲染量。
通常服務器端渲染的優點和用途有以下幾點:
1.更好的SEO
2.更快的頁面加載速度
3.在服務器端完成數據的加載
但需要注意,在服務器端渲染提高客戶端性能的同時,也帶來了更高的服務器負荷的問題。在項目開發時需要權衡其優點及缺點。
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向頁面注入靜態資源,并與服務器端渲染好的頁面進行匹配。
需要注意的是,客戶端與服務器端渲染的內容需要匹配才能進行正常的頁面加載,一些頁面加載異常問題將在下文進行具體描述。
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.__INITIALSTATE\_】,在客戶端中,我們需要將window.__INITIALSTATE\_中的值重新賦給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.__INITIALSTATE\_,并注入到html中。對于數據預取的操作和處理,我們將在下文【服務器端數據預取】一節進行具體介紹。
由于服務器端渲染服務需要客戶端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腳本中梳理打包流程。
首先將【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": {}
}
}由于需要進行兩次打包,在打包客戶端的時候會將之前的dist文件夾刪除,為避免服務器端Bundle丟失,需將其臨時移出dist文件夾。
在打包客戶端時,將【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
]
}
}經過以上幾步打包流程,我們已經將項目打包為【vue-ssr-server-bundle.json】、【vue-ssr-client-manifest.json】、【前端靜態資源】三個部分,之后我們需要在Node端利用打包后的這三個模塊內容進行服務器端渲染工作。
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進行渲染。
在了解到區別后,我們將在這里采用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中,返回到瀏覽器端。
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。
后續的流程則與生產環境相同。
到此為止,我們已經配置了服務器端渲染所需的基本文件,當然還需要一個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包提供的方案主要分為兩個步驟:
服務器端數據預取,主要是針對客戶端數據讀取慢導致首屏加載卡頓的問題。是在服務器端的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.__INITIALSTATE\_】中,該配置在上文的【entry-server.js】文件配置中可見;
4)在客戶端入口【entry-client.js】中將【window.__INITIALSTATE\_】中的數據重新掛載到store中。
// entry-client.js
const {app, router, store} = createApp();
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
router.onReady(() => {
app.$mount('#app');
});客戶端數據預取,其實是作為服務器端數據預取的補充。針對場景是在服務器端將渲染完成的頁面交付給瀏覽器端后,路由切換等工作也隨之由瀏覽器端的vue虛擬路由接管,而不會再向服務器端發送頁面請求,導致切換到新的頁面后并不會觸發服務器端數據預取的問題。
針對這個問題,客戶端數據預取的策略是在客戶端入口【entry-client.js】中進行操作,當檢測到路由切換時優先進行數據調?。▽嶋H上這里是在客戶端中復制服務器端數據預取的操作流程),在數據加載完成后再進行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實例的渲染工作。對于客戶端來說,一般需要可識別的h6標簽元素進行掛載,本文中是采用一個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字符串并返回給瀏覽器端時,便會出現路由不匹配的問題??蛻舳嗽跒g覽器端檢測出渲染后的Vue路由與當前瀏覽器中的路由不一致,會重新將頁面切換為/a路由下的頁面,導致頁面二次刷新。
3.頁面靜態資源加載失敗
由于在頁面靜態資源使用相對路徑時,瀏覽器會基于當前域名/IP進行靜態資源的請求,因此會向我們的Node服務進行靜態資源的請求。如果我們只做了服務器端渲染服務,而沒有搭建靜態資源服務器等對靜態資源進行代理,則會出現靜態資源加載失敗的問題。
4.H5標簽書寫不完整
服務器端在進行頁面渲染時,會對H5標簽進行自動補全,如
標簽會自動補全未寫的或
vue-server-renderer包中client-plugin和server-plugin插件與SPA頁面的關系是一對一的,即一個SPA頁面對應一套客戶端Bundle和服務器端Bundle,也就是一個客戶端json文件和一個服務器端json文件對應一個SPA應用。如果我們在項目中創建了多個SPA頁面,則在打包時,client-plugin和server-plugin插件會報錯提示有多個入口entry,無法正常匹配。
但很多情況我們需要在一個項目中擁有多個SPA頁面,對于這個問題,我們可以使用shell腳本調用npm命令使用webpack進行多次打包,而在webpack中根據命令參數進行動態的SPA頁面入口entry匹配。實際上,我們可以把這種做法理解為,將一個多SPA項目拆解成多個單SPA項目。
由于asyncData函數中進行數據預取和store初始化工作,是一個異步操作,而服務器端渲染需要在數據預取完成后將渲染好的頁面返回給瀏覽器。因此需要將asyncData的返回值設置為Promise對象,同樣,vuex中的action對象也需要返回一個Promise對象。
服務器端在Vue實例組件渲染時,僅會觸發beforeCreate、created兩個鉤子。因此需要注意以下幾點問題:
1.頁面初始化的內容盡量放在beforeCreate、created鉤子中;
2.會占用全局內存的邏輯,如定時器、全局變量、閉包等,盡量不要放在beforeCreate、created鉤子中,否則在beforeDestory方法中將無法注銷,導致內存泄漏。
有時,我們為了方便、易于管理以及項目簡潔,想直接將SPA應用的模板頁作為服務器端渲染時的模板頁。這時需要注意一個問題,就是服務器端渲染的模板頁比SPA應用模板頁多一個注釋標識,而在webpack打包時,會將SPA應用的模板中的注釋刪除掉。
對于這個問題,可以在webpack配置中設置不對SPA應用模板頁進行打包,具體設置如下:
// vue.config.js
module.exports = {
chainWebpack: (config) => {
config
.plugin('html-index')
.tap(options => {
options[0].minify = false;
return options;
});
},
};vue-cli3會對每個SPA頁面注冊一個html插件進行webpack配置的管理。需要注意的是,當項目為單entry時,該插件的名稱為’html’;而項目為多entry(即配置了pages屬性,即使pages中只有一個entry也會被識別為“多entry項目”)時,該插件名稱為`html-${entryName}`,其中entryName為入口entry名。
當客戶端、服務器端共用js包時,主要是在數據預取的場景下,須使用具有“同構”策略的包,如使用axios代替vue-resource等。
在開頭我們對一些服務器端渲染的問題進行過思考,并在文中做出了解答,在這里重新一一回顧下。
1.Vue在頁面渲染時以Vue實例為基本單元,在服務器端進行渲染時,是否也應對Vue實例進行渲染?
官方【vue-server-renderer】包提供的方式就是對Vue實例進行渲染,并提供了Renderer、BundleRenderer兩個對象,分別是對“單一Vue實例”、“Vue項目中的Vue實例”進行渲染。常用的方式是后者,會在服務器端根據用戶請求的路由,動態匹配需要渲染的Vue實例。
2.用戶與客戶端的關系是一對一,而與服務器端的關系是多對一,如何避免多個用戶之間在服務器端的數據共享的問題?
Vue服務器端渲染采用客戶端、服務器端協作渲染的方案。
客戶端負責靜態資源的加載,采用的是單例模式;
而服務器端負責Vue實例的渲染工作,采用的是工廠模式,即所有可能產生“閉包”或“全局變量”的地方,都需要改造成工廠模式,包括但不僅限于創建Vue實例、Vuex實例(store)、store中的module模塊、vue-router實例、其他公用js配置文件等。
3.如何實現同構策略?即讓服務器端能夠運行前端的代碼?
首先,通過webpack進行打包,根據客戶端、服務器端環境變量的不同,分別將項目打包為瀏覽器端可識別的模式,和Node端可識別的commonjs2模式;
其次,對一些公用js包,采用兼容瀏覽器、Node端的的包進行開發,如接口請求可采用axios.js進行處理。
4.服務器端渲染的Vue項目,開發環境和生產環境分別應該如何部署?有何區別?
共同點:
無論哪種環境下,該服務器端渲染方案均需使用客戶端、服務器端兩個Bundle共同渲染,因此需要對項目進行兩次打包。其中,客戶端Bundle包括前端項目原本打包出的瀏覽器可識別的靜態文件,和客戶端Bundle入口文件;服務器端Bundle則是將項目打包為commonjs2模式并使用source-map方式注入到json文件中。
不同點:
首先,生產環境的部署相對簡單粗暴,即將打包后的客戶端、服務器端Bundle放置到服務器上,使用一個node服務進行運行;
但開發環境的部署方式,則由于webpack打包運行后的客戶端存儲于內存中,而變得相對復雜一些。本文中使用的方案是通過http請求去讀取客戶端Bundle,而在Node中直接使用webpack包打包、讀取和監聽服務器Bundle。
5.如何保證服務器端渲染改造后的代碼仍能通過訪問靜態資源的方式直接訪問到?
針對這個問題,一種方案是在Node服務中對所有靜態資源請求進行代理,通過http轉發的方式將靜態資源轉發回瀏覽器端;另一種則是本文中使用的相對簡單快捷的方式,在Node服務中搭建靜態資源服務器,將所有靜態資源掛載到特定路由下。
“Vue項目中如何實現服務器端渲染”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。