您好,登錄后才能下訂單哦!
這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)?lái)有關(guān)Vue服務(wù)端渲染之Web應(yīng)用首屏耗時(shí)最優(yōu)化方法,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
什么是服務(wù)端渲染?服務(wù)端渲染的原理是什么?
Vue.js
是構(gòu)建客戶端應(yīng)用程序的框架。默認(rèn)情況下,可以在瀏覽器中輸出Vue
組件,進(jìn)行生成DOM
和操作DOM
。然而,也可以將同一個(gè)組件渲染為服務(wù)器端的HTML
字符串,將它們直接發(fā)送到瀏覽器,最后將這些靜態(tài)標(biāo)記"激活"為客戶端上完全可交互的應(yīng)用程序。
上面這段話是源自Vue服務(wù)端渲染文檔的解釋,用通俗的話來(lái)說(shuō),大概可以這么理解:
服務(wù)端渲染的目的是:性能優(yōu)勢(shì)。 在服務(wù)端生成對(duì)應(yīng)的HTML
字符串,客戶端接收到對(duì)應(yīng)的HTML
字符串,能立即渲染DOM
,最高效的首屏耗時(shí)。此外,由于服務(wù)端直接生成了對(duì)應(yīng)的HTML
字符串,對(duì)SEO
也非常友好;
服務(wù)端渲染的本質(zhì)是:生成應(yīng)用程序的“快照”。將Vue
及對(duì)應(yīng)庫(kù)運(yùn)行在服務(wù)端,此時(shí),Web Server Frame
實(shí)際上是作為代理服務(wù)器去訪問(wèn)接口服務(wù)器來(lái)預(yù)拉取數(shù)據(jù),從而將拉取到的數(shù)據(jù)作為Vue
組件的初始狀態(tài)。
服務(wù)端渲染的原理是:虛擬DOM
。在Web Server Frame
作為代理服務(wù)器去訪問(wèn)接口服務(wù)器來(lái)預(yù)拉取數(shù)據(jù)后,這是服務(wù)端初始化組件需要用到的數(shù)據(jù),此后,組件的beforeCreate
和created
生命周期會(huì)在服務(wù)端調(diào)用,初始化對(duì)應(yīng)的組件后,Vue
啟用虛擬DOM
形成初始化的HTML
字符串。之后,交由客戶端托管。實(shí)現(xiàn)前后端同構(gòu)應(yīng)用。
如何在基于Koa的Web Server Frame上配置服務(wù)端渲染?
基本用法
需要用到Vue
服務(wù)端渲染對(duì)應(yīng)庫(kù)vue-server-renderer
,通過(guò)npm
安裝:
npm install vue vue-server-renderer --save
最簡(jiǎn)單的,首先渲染一個(gè)Vue
實(shí)例:
// 第 1 步:創(chuàng)建一個(gè) Vue 實(shí)例 const Vue = require('vue'); const app = new Vue({ template: `<div>Hello World</div>` }); // 第 2 步:創(chuàng)建一個(gè) renderer const renderer = require('vue-server-renderer').createRenderer(); // 第 3 步:將 Vue 實(shí)例渲染為 HTML renderer.renderToString(app, (err, html) => { if (err) { throw err; } console.log(html); // => <div data-server-rendered="true">Hello World</div> });
與服務(wù)器集成:
module.exports = async function(ctx) { ctx.status = 200; let html = ''; try { // ... html = await renderer.renderToString(app, ctx); } catch (err) { ctx.logger('Vue SSR Render error', JSON.stringify(err)); html = await ctx.getErrorPage(err); // 渲染出錯(cuò)的頁(yè)面 } ctx.body = html; }
使用頁(yè)面模板:
當(dāng)你在渲染Vue
應(yīng)用程序時(shí),renderer
只從應(yīng)用程序生成HTML
標(biāo)記。在這個(gè)示例中,我們必須用一個(gè)額外的HTML
頁(yè)面包裹容器,來(lái)包裹生成的HTML
標(biāo)記。
為了簡(jiǎn)化這些,你可以直接在創(chuàng)建renderer
時(shí)提供一個(gè)頁(yè)面模板。多數(shù)時(shí)候,我們會(huì)將頁(yè)面模板放在特有的文件中:
<!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body> <!--vue-ssr-outlet--> </body> </html>
然后,我們可以讀取和傳輸文件到Vue renderer
中:
const tpl = fs.readFileSync(path.resolve(__dirname, './index.html'), 'utf-8'); const renderer = vssr.createRenderer({ template: tpl, });
Webpack配置
然而在實(shí)際項(xiàng)目中,不止上述例子那么簡(jiǎn)單,需要考慮很多方面:路由、數(shù)據(jù)預(yù)取、組件化、全局狀態(tài)等,所以服務(wù)端渲染不是只用一個(gè)簡(jiǎn)單的模板,然后加上使用vue-server-renderer
完成的,如下面的示意圖所示:
如示意圖所示,一般的Vue
服務(wù)端渲染項(xiàng)目,有兩個(gè)項(xiàng)目入口文件,分別為entry-client.js
和entry-server.js
,一個(gè)僅運(yùn)行在客戶端,一個(gè)僅運(yùn)行在服務(wù)端,經(jīng)過(guò)Webpack
打包后,會(huì)生成兩個(gè)Bundle
,服務(wù)端的Bundle
會(huì)用于在服務(wù)端使用虛擬DOM
生成應(yīng)用程序的“快照”,客戶端的Bundle
會(huì)在瀏覽器執(zhí)行。
因此,我們需要兩個(gè)Webpack
配置,分別命名為webpack.client.config.js
和webpack.server.config.js
,分別用于生成客戶端Bundle
與服務(wù)端Bundle
,分別命名為vue-ssr-client-manifest.json
與vue-ssr-server-bundle.json
,關(guān)于如何配置,Vue
官方有相關(guān)示例vue-hackernews-2.0
開(kāi)發(fā)環(huán)境搭建
我所在的項(xiàng)目使用Koa
作為Web Server Frame
,項(xiàng)目使用koa-webpack進(jìn)行開(kāi)發(fā)環(huán)境的構(gòu)建。如果是在產(chǎn)品環(huán)境下,會(huì)生成vue-ssr-client-manifest.json
與vue-ssr-server-bundle.json
,包含對(duì)應(yīng)的Bundle
,提供客戶端和服務(wù)端引用,而在開(kāi)發(fā)環(huán)境下,一般情況下放在內(nèi)存中。使用memory-fs
模塊進(jìn)行讀取。
const fs = require('fs') const path = require( 'path' ); const webpack = require( 'webpack' ); const koaWpDevMiddleware = require( 'koa-webpack' ); const MFS = require('memory-fs'); const appSSR = require('./../../app.ssr.js'); let wpConfig; let clientConfig, serverConfig; let wpCompiler; let clientCompiler, serverCompiler; let clientManifest; let bundle; // 生成服務(wù)端bundle的webpack配置 if ((fs.existsSync(path.resolve(cwd,'webpack.server.config.js')))) { serverConfig = require(path.resolve(cwd, 'webpack.server.config.js')); serverCompiler = webpack( serverConfig ); } // 生成客戶端clientManifest的webpack配置 if ((fs.existsSync(path.resolve(cwd,'webpack.client.config.js')))) { clientConfig = require(path.resolve(cwd, 'webpack.client.config.js')); clientCompiler = webpack(clientConfig); } if (serverCompiler && clientCompiler) { let publicPath = clientCompiler.output && clientCompiler.output.publicPath; const koaDevMiddleware = await koaWpDevMiddleware({ compiler: clientCompiler, devMiddleware: { publicPath, serverSideRender: true }, }); app.use(koaDevMiddleware); // 服務(wù)端渲染生成clientManifest app.use(async (ctx, next) => { const stats = ctx.state.webpackStats.toJson(); const assetsByChunkName = stats.assetsByChunkName; stats.errors.forEach(err => console.error(err)); stats.warnings.forEach(err => console.warn(err)); if (stats.errors.length) { console.error(stats.errors); return; } // 生成的clientManifest放到appSSR模塊,應(yīng)用程序可以直接讀取 let fileSystem = koaDevMiddleware.devMiddleware.fileSystem; clientManifest = JSON.parse(fileSystem.readFileSync(path.resolve(cwd,'./dist/vue-ssr-client-manifest.json'), 'utf-8')); appSSR.clientManifest = clientManifest; await next(); }); // 服務(wù)端渲染的server bundle 存儲(chǔ)到內(nèi)存里 const mfs = new MFS(); serverCompiler.outputFileSystem = mfs; serverCompiler.watch({}, (err, stats) => { if (err) { throw err; } stats = stats.toJson(); if (stats.errors.length) { console.error(stats.errors); return; } // 生成的bundle放到appSSR模塊,應(yīng)用程序可以直接讀取 bundle = JSON.parse(mfs.readFileSync(path.resolve(cwd,'./dist/vue-ssr-server-bundle.json'), 'utf-8')); appSSR.bundle = bundle; }); }
渲染中間件配置
產(chǎn)品環(huán)境下,打包后的客戶端和服務(wù)端的Bundle
會(huì)存儲(chǔ)為vue-ssr-client-manifest.json
與vue-ssr-server-bundle.json
,通過(guò)文件流模塊fs
讀取即可,但在開(kāi)發(fā)環(huán)境下,我創(chuàng)建了一個(gè)appSSR
模塊,在發(fā)生代碼更改時(shí),會(huì)觸發(fā)Webpack
熱更新,appSSR
對(duì)應(yīng)的bundle
也會(huì)更新,appSSR
模塊代碼如下所示:
let clientManifest; let bundle; const appSSR = { get bundle() { return bundle; }, set bundle(val) { bundle = val; }, get clientManifest() { return clientManifest; }, set clientManifest(val) { clientManifest = val; } }; module.exports = appSSR;
通過(guò)引入appSSR
模塊,在開(kāi)發(fā)環(huán)境下,就可以拿到clientManifest
和ssrBundle
,項(xiàng)目的渲染中間件如下:
const fs = require('fs'); const path = require('path'); const ejs = require('ejs'); const vue = require('vue'); const vssr = require('vue-server-renderer'); const createBundleRenderer = vssr.createBundleRenderer; const dirname = process.cwd(); const env = process.env.RUN_ENVIRONMENT; let bundle; let clientManifest; if (env === 'development') { // 開(kāi)發(fā)環(huán)境下,通過(guò)appSSR模塊,拿到clientManifest和ssrBundle let appSSR = require('./../../core/app.ssr.js'); bundle = appSSR.bundle; clientManifest = appSSR.clientManifest; } else { bundle = JSON.parse(fs.readFileSync(path.resolve(__dirname, './dist/vue-ssr-server-bundle.json'), 'utf-8')); clientManifest = JSON.parse(fs.readFileSync(path.resolve(__dirname, './dist/vue-ssr-client-manifest.json'), 'utf-8')); } module.exports = async function(ctx) { ctx.status = 200; let html; let context = await ctx.getTplContext(); ctx.logger('進(jìn)入SSR,context為: ', JSON.stringify(context)); const tpl = fs.readFileSync(path.resolve(__dirname, './newTemplate.html'), 'utf-8'); const renderer = createBundleRenderer(bundle, { runInNewContext: false, template: tpl, // (可選)頁(yè)面模板 clientManifest: clientManifest // (可選)客戶端構(gòu)建 manifest }); ctx.logger('createBundleRenderer renderer:', JSON.stringify(renderer)); try { html = await renderer.renderToString({ ...context, url: context.CTX.url, }); } catch(err) { ctx.logger('SSR renderToString 失?。?nbsp;', JSON.stringify(err)); console.error(err); } ctx.body = html; };
如何對(duì)現(xiàn)有項(xiàng)目進(jìn)行改造?
基本目錄改造
使用Webpack
來(lái)處理服務(wù)器和客戶端的應(yīng)用程序,大部分源碼可以使用通用方式編寫,可以使用Webpack
支持的所有功能。
一個(gè)基本項(xiàng)目可能像是這樣:
src ├── components │ ├── Foo.vue │ ├── Bar.vue │ └── Baz.vue ├── frame │ ├── app.js # 通用 entry(universal entry) │ ├── entry-client.js # 僅運(yùn)行于瀏覽器 │ ├── entry-server.js # 僅運(yùn)行于服務(wù)器 │ └── index.vue # 項(xiàng)目入口組件 ├── pages ├── routers └── store
app.js
是我們應(yīng)用程序的「通用entry
」。在純客戶端應(yīng)用程序中,我們將在此文件中創(chuàng)建根Vue
實(shí)例,并直接掛載到DOM
。但是,對(duì)于服務(wù)器端渲染(SSR
),責(zé)任轉(zhuǎn)移到純客戶端entry
文件。app.js
簡(jiǎn)單地使用export
導(dǎo)出一個(gè)createApp
函數(shù):
import Router from '~ut/router'; import { sync } from 'vuex-router-sync'; import Vue from 'vue'; import { createStore } from './../store'; import Frame from './index.vue'; import myRouter from './../routers/myRouter'; function createVueInstance(routes, ctx) { const router = Router({ base: '/base', mode: 'history', routes: [routes], }); const store = createStore({ ctx }); // 把路由注入到vuex中 sync(store, router); const app = new Vue({ router, render: function(h) { return h(Frame); }, store, }); return { app, router, store }; } module.exports = function createApp(ctx) { return createVueInstance(myRouter, ctx); }
注:在我所在的項(xiàng)目中,需要?jiǎng)討B(tài)判斷是否需要注冊(cè)DicomView
,只有在客戶端才初始化DicomView
,由于Node.js
環(huán)境沒(méi)有window
對(duì)象,對(duì)于代碼運(yùn)行環(huán)境的判斷,可以通過(guò)typeof window === 'undefined'
來(lái)進(jìn)行判斷。
避免創(chuàng)建單例
如Vue SSR
文檔所述:
當(dāng)編寫純客戶端 (client-only) 代碼時(shí),我們習(xí)慣于每次在新的上下文中對(duì)代碼進(jìn)行取值。但是,Node.js 服務(wù)器是一個(gè)長(zhǎng)期運(yùn)行的進(jìn)程。當(dāng)我們的代碼進(jìn)入該進(jìn)程時(shí),它將進(jìn)行一次取值并留存在內(nèi)存中。這意味著如果創(chuàng)建一個(gè)單例對(duì)象,它將在每個(gè)傳入的請(qǐng)求之間共享。如基本示例所示,我們?yōu)槊總€(gè)請(qǐng)求創(chuàng)建一個(gè)新的根 Vue 實(shí)例。這與每個(gè)用戶在自己的瀏覽器中使用新應(yīng)用程序的實(shí)例類似。如果我們?cè)诙鄠€(gè)請(qǐng)求之間使用一個(gè)共享的實(shí)例,很容易導(dǎo)致交叉請(qǐng)求狀態(tài)污染 (cross-request state pollution)。因此,我們不應(yīng)該直接創(chuàng)建一個(gè)應(yīng)用程序?qū)嵗?,而是?yīng)該暴露一個(gè)可以重復(fù)執(zhí)行的工廠函數(shù),為每個(gè)請(qǐng)求創(chuàng)建新的應(yīng)用程序?qū)嵗?。同樣的?guī)則也適用于 router、store 和 event bus 實(shí)例。你不應(yīng)該直接從模塊導(dǎo)出并將其導(dǎo)入到應(yīng)用程序中,而是需要在 createApp 中創(chuàng)建一個(gè)新的實(shí)例,并從根 Vue 實(shí)例注入。
如上代碼所述,createApp
方法通過(guò)返回一個(gè)返回值創(chuàng)建Vue
實(shí)例的對(duì)象的函數(shù)調(diào)用,在函數(shù)createVueInstance
中,為每一個(gè)請(qǐng)求創(chuàng)建了Vue
,Vue Router
,Vuex
實(shí)例。并暴露給entry-client
和entry-server
模塊。
在客戶端entry-client.js
只需創(chuàng)建應(yīng)用程序,并且將其掛載到DOM
中:
import { createApp } from './app'; // 客戶端特定引導(dǎo)邏輯…… const { app } = createApp(); // 這里假定 App.vue 模板中根元素具有 `id="app"` app.$mount('#app');
服務(wù)端entry-server.js
使用default export
導(dǎo)出函數(shù),并在每次渲染中重復(fù)調(diào)用此函數(shù)。此時(shí),除了創(chuàng)建和返回應(yīng)用程序?qū)嵗猓粫?huì)做太多事情 - 但是稍后我們將在此執(zhí)行服務(wù)器端路由匹配和數(shù)據(jù)預(yù)取邏輯:
import { createApp } from './app'; export default context => { const { app } = createApp(); return app; }
在服務(wù)端用vue-router分割代碼
與Vue
實(shí)例一樣,也需要?jiǎng)?chuàng)建單例的vueRouter
對(duì)象。對(duì)于每個(gè)請(qǐng)求,都需要?jiǎng)?chuàng)建一個(gè)新的vueRouter
實(shí)例:
function createVueInstance(routes, ctx) { const router = Router({ base: '/base', mode: 'history', routes: [routes], }); const store = createStore({ ctx }); // 把路由注入到vuex中 sync(store, router); const app = new Vue({ router, render: function(h) { return h(Frame); }, store, }); return { app, router, store }; }
同時(shí),需要在entry-server.js
中實(shí)現(xiàn)服務(wù)器端路由邏輯,使用router.getMatchedComponents
方法獲取到當(dāng)前路由匹配的組件,如果當(dāng)前路由沒(méi)有匹配到相應(yīng)的組件,則reject
到404
頁(yè)面,否則resolve
整個(gè)app
,用于Vue
渲染虛擬DOM
,并使用對(duì)應(yīng)模板生成對(duì)應(yīng)的HTML
字符串。
const createApp = require('./app'); module.exports = context => { return new Promise((resolve, reject) => { // ... // 設(shè)置服務(wù)器端 router 的位置 router.push(context.url); // 等到 router 將可能的異步組件和鉤子函數(shù)解析完 router.onReady(() => { const matchedComponents = router.getMatchedComponents(); // 匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 404 if (!matchedComponents.length) { return reject('匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 404'); } // Promise 應(yīng)該 resolve 應(yīng)用程序?qū)嵗?,以便它可以渲? resolve(app); }, reject); }); }
在服務(wù)端預(yù)拉取數(shù)據(jù)
在Vue
服務(wù)端渲染,本質(zhì)上是在渲染我們應(yīng)用程序的"快照",所以如果應(yīng)用程序依賴于一些異步數(shù)據(jù),那么在開(kāi)始渲染過(guò)程之前,需要先預(yù)取和解析好這些數(shù)據(jù)。服務(wù)端Web Server Frame
作為代理服務(wù)器,在服務(wù)端對(duì)接口服務(wù)發(fā)起請(qǐng)求,并將數(shù)據(jù)拼裝到全局Vuex
狀態(tài)中。
另一個(gè)需要關(guān)注的問(wèn)題是在客戶端,在掛載到客戶端應(yīng)用程序之前,需要獲取到與服務(wù)器端應(yīng)用程序完全相同的數(shù)據(jù) - 否則,客戶端應(yīng)用程序會(huì)因?yàn)槭褂门c服務(wù)器端應(yīng)用程序不同的狀態(tài),然后導(dǎo)致混合失敗。
目前較好的解決方案是,給路由匹配的一級(jí)子組件一個(gè)asyncData
,在asyncData
方法中,dispatch
對(duì)應(yīng)的action
。asyncData
是我們約定的函數(shù)名,表示渲染組件需要預(yù)先執(zhí)行它獲取初始數(shù)據(jù),它返回一個(gè)Promise
,以便我們?cè)诤蠖虽秩镜臅r(shí)候可以知道什么時(shí)候該操作完成。注意,由于此函數(shù)會(huì)在組件實(shí)例化之前調(diào)用,所以它無(wú)法訪問(wèn)this
。需要將store
和路由信息作為參數(shù)傳遞進(jìn)去:
舉個(gè)例子:
<!-- Lung.vue --> <template> <div></div> </template> <script> export default { // ... async asyncData({ store, route }) { return Promise.all([ store.dispatch('getA'), store.dispatch('myModule/getB', { root:true }), store.dispatch('myModule/getC', { root:true }), store.dispatch('myModule/getD', { root:true }), ]); }, // ... } </script>
在entry-server.js
中,我們可以通過(guò)路由獲得與router.getMatchedComponents()
相匹配的組件,如果組件暴露出asyncData
,我們就調(diào)用這個(gè)方法。然后我們需要將解析完成的狀態(tài),附加到渲染上下文中。
const createApp = require('./app'); module.exports = context => { return new Promise((resolve, reject) => { const { app, router, store } = createApp(context); // 針對(duì)沒(méi)有Vue router 的Vue實(shí)例,在項(xiàng)目中為列表頁(yè),直接resolve app if (!router) { resolve(app); } // 設(shè)置服務(wù)器端 router 的位置 router.push(context.url.replace('/base', '')); // 等到 router 將可能的異步組件和鉤子函數(shù)解析完 router.onReady(() => { const matchedComponents = router.getMatchedComponents(); // 匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 404 if (!matchedComponents.length) { return reject('匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 404'); } Promise.all(matchedComponents.map(Component => { if (Component.asyncData) { return Component.asyncData({ store, route: router.currentRoute, }); } })).then(() => { // 在所有預(yù)取鉤子(preFetch hook) resolve 后, // 我們的 store 現(xiàn)在已經(jīng)填充入渲染應(yīng)用程序所需的狀態(tài)。 // 當(dāng)我們將狀態(tài)附加到上下文,并且 `template` 選項(xiàng)用于 renderer 時(shí), // 狀態(tài)將自動(dòng)序列化為 `window.__INITIAL_STATE__`,并注入 HTML。 context.state = store.state; resolve(app); }).catch(reject); }, reject); }); }
客戶端托管全局狀態(tài)
當(dāng)服務(wù)端使用模板進(jìn)行渲染時(shí),context.state
將作為window.__INITIAL_STATE__
狀態(tài),自動(dòng)嵌入到最終的HTML
中。而在客戶端,在掛載到應(yīng)用程序之前,store
就應(yīng)該獲取到狀態(tài),最終我們的entry-client.js
被改造為如下所示:
import createApp from './app'; const { app, router, store } = createApp(); // 客戶端把初始化的store替換為window.__INITIAL_STATE__ if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__); } if (router) { router.onReady(() => { app.$mount('#app') }); } else { app.$mount('#app'); }
常見(jiàn)問(wèn)題的解決方案
至此,基本的代碼改造也已經(jīng)完成了,下面說(shuō)的是一些常見(jiàn)問(wèn)題的解決方案:
在服務(wù)端沒(méi)有window
、location
對(duì)象:
對(duì)于舊項(xiàng)目遷移到SSR
肯定會(huì)經(jīng)歷的問(wèn)題,一般為在項(xiàng)目入口處或是created
、beforeCreate
生命周期使用了DOM
操作,或是獲取了location
對(duì)象,通用的解決方案一般為判斷執(zhí)行環(huán)境,通過(guò)typeof window
是否為'undefined'
,如果遇到必須使用location
對(duì)象的地方用于獲取url
中的相關(guān)參數(shù),在ctx
對(duì)象中也可以找到對(duì)應(yīng)參數(shù)。
vue-router
報(bào)錯(cuò)Uncaught TypeError: _Vue.extend is not _Vue function
,沒(méi)有找到_Vue
實(shí)例的問(wèn)題:
通過(guò)查看Vue-router
源碼發(fā)現(xiàn)沒(méi)有手動(dòng)調(diào)用Vue.use(Vue-Router);
。沒(méi)有調(diào)用Vue.use(Vue-Router);
在瀏覽器端沒(méi)有出現(xiàn)問(wèn)題,但在服務(wù)端就會(huì)出現(xiàn)問(wèn)題。對(duì)應(yīng)的Vue-router
源碼所示:
VueRouter.prototype.init = function init (app /* Vue component instance */) { var this$1 = this; process.env.NODE_ENV !== 'production' && assert( install.installed, "not installed. Make sure to call `Vue.use(VueRouter)` " + "before creating root instance." ); // ... }
服務(wù)端無(wú)法獲取hash
路由的參數(shù)
由于hash
路由的參數(shù),會(huì)導(dǎo)致vue-router
不起效果,對(duì)于使用了vue-router
的前后端同構(gòu)應(yīng)用,必須換為history
路由。
接口處獲取不到cookie
的問(wèn)題:
由于客戶端每次請(qǐng)求都會(huì)對(duì)應(yīng)地把cookie
帶給接口側(cè),而服務(wù)端Web Server Frame
作為代理服務(wù)器,并不會(huì)每次維持cookie
,所以需要我們手動(dòng)把cookie
透?jìng)鹘o接口側(cè),常用的解決方案是,將ctx
掛載到全局狀態(tài)中,當(dāng)發(fā)起異步請(qǐng)求時(shí),手動(dòng)帶上cookie
,如下代碼所示:
// createStore.js // 在創(chuàng)建全局狀態(tài)的函數(shù)`createStore`時(shí),將`ctx`掛載到全局狀態(tài) export function createStore({ ctx }) { return new Vuex.Store({ state: { ...state, ctx, }, getters, actions, mutations, modules: { // ... }, plugins: debug ? [createLogger()] : [], }); }
當(dāng)發(fā)起異步請(qǐng)求時(shí),手動(dòng)帶上cookie
,項(xiàng)目中使用的是Axios
:
// actions.js // ... const actions = { async getUserInfo({ commit, state }) { let requestParams = { params: { random: tool.createRandomString(8, true), }, headers: { 'X-Requested-With': 'XMLHttpRequest', }, }; // 手動(dòng)帶上cookie if (state.ctx.request.headers.cookie) { requestParams.headers.Cookie = state.ctx.request.headers.cookie; } // ... let res = await Axios.get(`${requestUrlOrigin}${url.GET_A}`, requestParams); commit(globalTypes.SET_A, { res: res.data, }); } }; // ...
接口請(qǐng)求時(shí)報(bào)connect ECONNREFUSED 127.0.0.1:80
的問(wèn)題
原因是改造之前,使用客戶端渲染時(shí),使用了devServer.proxy
代理配置來(lái)解決跨域問(wèn)題,而服務(wù)端作為代理服務(wù)器對(duì)接口發(fā)起異步請(qǐng)求時(shí),不會(huì)讀取對(duì)應(yīng)的webpack
配置,對(duì)于服務(wù)端而言會(huì)對(duì)應(yīng)請(qǐng)求當(dāng)前域下的對(duì)應(yīng)path
下的接口。
解決方案為去除webpack
的devServer.proxy
配置,對(duì)于接口請(qǐng)求帶上對(duì)應(yīng)的origin
即可:
const requestUrlOrigin = requestUrlOrigin = state.ctx.URL.origin; const res = await Axios.get(`${requestUrlOrigin}${url.GET_A}`, requestParams);
對(duì)于vue-router
配置項(xiàng)有base
參數(shù)時(shí),初始化時(shí)匹配不到對(duì)應(yīng)路由的問(wèn)題
在官方示例中的entry-server.js
:
// entry-server.js import { createApp } from './app'; export default context => { // 因?yàn)橛锌赡軙?huì)是異步路由鉤子函數(shù)或組件,所以我們將返回一個(gè) Promise, // 以便服務(wù)器能夠等待所有的內(nèi)容在渲染前, // 就已經(jīng)準(zhǔn)備就緒。 return new Promise((resolve, reject) => { const { app, router } = createApp(); // 設(shè)置服務(wù)器端 router 的位置 router.push(context.url); // ... }); }
原因是設(shè)置服務(wù)器端router
的位置時(shí),context.url
為訪問(wèn)頁(yè)面的url
,并帶上了base
,在router.push
時(shí)應(yīng)該去除base
,如下所示:
router.push(context.url.replace('/base', ''));
上述就是小編為大家分享的Vue服務(wù)端渲染之Web應(yīng)用首屏耗時(shí)最優(yōu)化方法了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。