免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
Vue 服務(wù)端渲染實(shí)踐 ——Web應(yīng)用首屏耗時(shí)最優(yōu)化方案

作者:counterxing

鏈接:https://segmentfault.com/a/1190000018577041

隨著各大前端框架的誕生和演變, SPA開始流行,單頁面應(yīng)用的優(yōu)勢在于可以不重新加載整個(gè)頁面的情況下,通過 ajax和服務(wù)器通信,實(shí)現(xiàn)整個(gè) Web應(yīng)用拒不更新,帶來了極致的用戶體驗(yàn)。然而,對(duì)于需要 SEO、追求極致的首屏性能的應(yīng)用,前端渲染的 SPA是糟糕的。好在 Vue 2.0后是支持服務(wù)端渲染的,零零散散花費(fèi)了兩三周事件,通過改造現(xiàn)有項(xiàng)目,基本完成了在現(xiàn)有項(xiàng)目中實(shí)踐了 Vue服務(wù)端渲染。

關(guān)于Vue服務(wù)端渲染的原理、搭建,官方文檔已經(jīng)講的比較詳細(xì)了,因此,本文不是抄襲文檔,而是文檔的補(bǔ)充。特別是對(duì)于如何與現(xiàn)有項(xiàng)目進(jìn)行很好的結(jié)合,還是需要費(fèi)很大功夫的。本文主要對(duì)我所在的項(xiàng)目中進(jìn)行 Vue服務(wù)端渲染的改造過程進(jìn)行闡述,加上一些個(gè)人的理解,作為分享與學(xué)習(xí)。

概述

本文主要分以下幾個(gè)方面:

  • 什么是服務(wù)端渲染?服務(wù)端渲染的原理是什么?

  • 如何在基于 Koa的 Web Server Frame上配置服務(wù)端渲染?

  • 如何對(duì)現(xiàn)有項(xiàng)目進(jìn)行改造?

    • 在服務(wù)端預(yù)拉取數(shù)據(jù);

    • 客戶端托管全局狀態(tài);

    • 常見問題的解決方案;

    • 基本目錄改造;

    • 在服務(wù)端用 vue-router分割代碼;

什么是服務(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ù)端渲染文檔的解釋,用通俗的話來說,大概可以這么理解:

  • 服務(wù)端渲染的目的是:性能優(yōu)勢。 在服務(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)庫運(yùn)行在服務(wù)端,此時(shí), Web Server Frame實(shí)際上是作為代理服務(wù)器去訪問接口服務(wù)器來預(yù)拉取數(shù)據(jù),從而將拉取到的數(shù)據(jù)作為 Vue組件的初始狀態(tài)。

  • 服務(wù)端渲染的原理是:虛擬 DOM。在 Web Server Frame作為代理服務(wù)器去訪問接口服務(wù)器來預(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)庫 vue-server-renderer,通過 npm安裝:

  1. npm install vue vue-server-renderer --save

最簡單的,首先渲染一個(gè) Vue實(shí)例:

  1. // 第 1 步:創(chuàng)建一個(gè) Vue 實(shí)例

  2. const Vue = require('vue');

  3. const app = new Vue({

  4. template: `<div>Hello World</div>`

  5. });

  6. // 第 2 步:創(chuàng)建一個(gè) renderer

  7. const renderer = require('vue-server-renderer').createRenderer();

  8. // 第 3 步:將 Vue 實(shí)例渲染為 HTML

  9. renderer.renderToString(app, (err, html) => {

  10. if (err) {

  11. throw err;

  12. }

  13. console.log(html);

  14. // => <div data-server-rendered='true'>Hello World</div>

  15. });

與服務(wù)器集成:

  1. module.exports = async function(ctx) {

  2. ctx.status = 200;

  3. let html = '';

  4. try {

  5. // ...

  6. html = await renderer.renderToString(app, ctx);

  7. } catch (err) {

  8. ctx.logger('Vue SSR Render error', JSON.stringify(err));

  9. html = await ctx.getErrorPage(err); // 渲染出錯(cuò)的頁面

  10. }

  11. ctx.body = html;

  12. }

使用頁面模板:

當(dāng)你在渲染 Vue應(yīng)用程序時(shí), renderer只從應(yīng)用程序生成 HTML標(biāo)記。在這個(gè)示例中,我們必須用一個(gè)額外的 HTML頁面包裹容器,來包裹生成的 HTML標(biāo)記。

為了簡化這些,你可以直接在創(chuàng)建 renderer時(shí)提供一個(gè)頁面模板。多數(shù)時(shí)候,我們會(huì)將頁面模板放在特有的文件中:

  1. <!DOCTYPE html>

  2. <html lang='en'>

  3. <head><title>Hello</title></head>

  4. <body>

  5. <!--vue-ssr-outlet-->

  6. </body>

  7. </html>

然后,我們可以讀取和傳輸文件到 Vue renderer中:

  1. const tpl = fs.readFileSync(path.resolve(__dirname, './index.html'), 'utf-8');

  2. const renderer = vssr.createRenderer({

  3. template: tpl,

  4. });

Webpack配置

然而在實(shí)際項(xiàng)目中,不止上述例子那么簡單,需要考慮很多方面:路由、數(shù)據(jù)預(yù)取、組件化、全局狀態(tài)等,所以服務(wù)端渲染不是只用一個(gè)簡單的模板,然后加上使用 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)過 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

開發(fā)環(huán)境搭建

我所在的項(xiàng)目使用 Koa作為 Web Server Frame,項(xiàng)目使用koa-webpack進(jìn)行開發(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ù)端引用,而在開發(fā)環(huán)境下,一般情況下放在內(nèi)存中。使用 memory-fs模塊進(jìn)行讀取。

  1. const fs = require('fs')

  2. const path = require( 'path' );

  3. const webpack = require( 'webpack' );

  4. const koaWpDevMiddleware = require( 'koa-webpack' );

  5. const MFS = require('memory-fs');

  6. const appSSR = require('./../../app.ssr.js');

  7. let wpConfig;

  8. let clientConfig, serverConfig;

  9. let wpCompiler;

  10. let clientCompiler, serverCompiler;

  11. let clientManifest;

  12. let bundle;

  13. // 生成服務(wù)端bundle的webpack配置

  14. if ((fs.existsSync(path.resolve(cwd,'webpack.server.config.js')))) {

  15. serverConfig = require(path.resolve(cwd, 'webpack.server.config.js'));

  16. serverCompiler = webpack( serverConfig );

  17. }

  18. // 生成客戶端clientManifest的webpack配置

  19. if ((fs.existsSync(path.resolve(cwd,'webpack.client.config.js')))) {

  20. clientConfig = require(path.resolve(cwd, 'webpack.client.config.js'));

  21. clientCompiler = webpack(clientConfig);

  22. }

  23. if (serverCompiler && clientCompiler) {

  24. let publicPath = clientCompiler.output && clientCompiler.output.publicPath;

  25. const koaDevMiddleware = await koaWpDevMiddleware({

  26. compiler: clientCompiler,

  27. devMiddleware: {

  28. publicPath,

  29. serverSideRender: true

  30. },

  31. });

  32. app.use(koaDevMiddleware);

  33. // 服務(wù)端渲染生成clientManifest

  34. app.use(async (ctx, next) => {

  35. const stats = ctx.state.webpackStats.toJson();

  36. const assetsByChunkName = stats.assetsByChunkName;

  37. stats.errors.forEach(err => console.error(err));

  38. stats.warnings.forEach(err => console.warn(err));

  39. if (stats.errors.length) {

  40. console.error(stats.errors);

  41. return;

  42. }

  43. // 生成的clientManifest放到appSSR模塊,應(yīng)用程序可以直接讀取

  44. let fileSystem = koaDevMiddleware.devMiddleware.fileSystem;

  45. clientManifest = JSON.parse(fileSystem.readFileSync(path.resolve(cwd,'./dist/vue-ssr-client-manifest.json'), 'utf-8'));

  46. appSSR.clientManifest = clientManifest;

  47. await next();

  48. });

  49. // 服務(wù)端渲染的server bundle 存儲(chǔ)到內(nèi)存里

  50. const mfs = new MFS();

  51. serverCompiler.outputFileSystem = mfs;

  52. serverCompiler.watch({}, (err, stats) => {

  53. if (err) {

  54. throw err;

  55. }

  56. stats = stats.toJson();

  57. if (stats.errors.length) {

  58. console.error(stats.errors);

  59. return;

  60. }

  61. // 生成的bundle放到appSSR模塊,應(yīng)用程序可以直接讀取

  62. bundle = JSON.parse(mfs.readFileSync(path.resolve(cwd,'./dist/vue-ssr-server-bundle.json'), 'utf-8'));

  63. appSSR.bundle = bundle;

  64. });

  65. }

渲染中間件配置

產(chǎn)品環(huán)境下,打包后的客戶端和服務(wù)端的 Bundle會(huì)存儲(chǔ)為 vue-ssr-client-manifest.json與 vue-ssr-server-bundle.json,通過文件流模塊 fs讀取即可,但在開發(fā)環(huán)境下,我創(chuàng)建了一個(gè) appSSR模塊,在發(fā)生代碼更改時(shí),會(huì)觸發(fā) Webpack熱更新, appSSR對(duì)應(yīng)的 bundle也會(huì)更新, appSSR模塊代碼如下所示:

  1. let clientManifest;

  2. let bundle;

  3. const appSSR = {

  4. get bundle() {

  5. return bundle;

  6. },

  7. set bundle(val) {

  8. bundle = val;

  9. },

  10. get clientManifest() {

  11. return clientManifest;

  12. },

  13. set clientManifest(val) {

  14. clientManifest = val;

  15. }

  16. };

  17. module.exports = appSSR;

通過引入 appSSR模塊,在開發(fā)環(huán)境下,就可以拿到 clientManifest和 ssrBundle,項(xiàng)目的渲染中間件如下:

  1. const fs = require('fs');

  2. const path = require('path');

  3. const ejs = require('ejs');

  4. const vue = require('vue');

  5. const vssr = require('vue-server-renderer');

  6. const createBundleRenderer = vssr.createBundleRenderer;

  7. const dirname = process.cwd();

  8. const env = process.env.RUN_ENVIRONMENT;

  9. let bundle;

  10. let clientManifest;

  11. if (env === 'development') {

  12. // 開發(fā)環(huán)境下,通過appSSR模塊,拿到clientManifest和ssrBundle

  13. let appSSR = require('./../../core/app.ssr.js');

  14. bundle = appSSR.bundle;

  15. clientManifest = appSSR.clientManifest;

  16. } else {

  17. bundle = JSON.parse(fs.readFileSync(path.resolve(__dirname, './dist/vue-ssr-server-bundle.json'), 'utf-8'));

  18. clientManifest = JSON.parse(fs.readFileSync(path.resolve(__dirname, './dist/vue-ssr-client-manifest.json'), 'utf-8'));

  19. }

  20. module.exports = async function(ctx) {

  21. ctx.status = 200;

  22. let html;

  23. let context = await ctx.getTplContext();

  24. ctx.logger('進(jìn)入SSR,context為: ', JSON.stringify(context));

  25. const tpl = fs.readFileSync(path.resolve(__dirname, './newTemplate.html'), 'utf-8');

  26. const renderer = createBundleRenderer(bundle, {

  27. runInNewContext: false,

  28. template: tpl, // (可選)頁面模板

  29. clientManifest: clientManifest // (可選)客戶端構(gòu)建 manifest

  30. });

  31. ctx.logger('createBundleRenderer renderer:', JSON.stringify(renderer));

  32. try {

  33. html = await renderer.renderToString({

  34. ...context,

  35. url: context.CTX.url,

  36. });

  37. } catch(err) {

  38. ctx.logger('SSR renderToString 失?。?', JSON.stringify(err));

  39. console.error(err);

  40. }

  41. ctx.body = html;

  42. };

如何對(duì)現(xiàn)有項(xiàng)目進(jìn)行改造?

基本目錄改造

使用 Webpack來處理服務(wù)器和客戶端的應(yīng)用程序,大部分源碼可以使用通用方式編寫,可以使用 Webpack支持的所有功能。

一個(gè)基本項(xiàng)目可能像是這樣:

  1. src

  2. ├── components

  3. ├── Foo.vue

  4. ├── Bar.vue

  5. └── Baz.vue

  6. ├── frame

  7. ├── app.js # 通用 entry(universal entry)

  8. ├── entry-client.js # 僅運(yùn)行于瀏覽器

  9. ├── entry-server.js # 僅運(yùn)行于服務(wù)器

  10. └── index.vue # 項(xiàng)目入口組件

  11. ├── pages

  12. ├── routers

  13. └── store

app.js是我們應(yīng)用程序的「通用 entry」。在純客戶端應(yīng)用程序中,我們將在此文件中創(chuàng)建根 Vue實(shí)例,并直接掛載到 DOM。但是,對(duì)于服務(wù)器端渲染( SSR),責(zé)任轉(zhuǎn)移到純客戶端 entry文件。 app.js簡單地使用 export導(dǎo)出一個(gè) createApp函數(shù):

  1. import Router from '~ut/router';

  2. import { sync } from 'vuex-router-sync';

  3. import Vue from 'vue';

  4. import { createStore } from './../store';

  5. import Frame from './index.vue';

  6. import myRouter from './../routers/myRouter';

  7. function createVueInstance(routes, ctx) {

  8. const router = Router({

  9. base: '/base',

  10. mode: 'history',

  11. routes: [routes],

  12. });

  13. const store = createStore({ ctx });

  14. // 把路由注入到vuex中

  15. sync(store, router);

  16. const app = new Vue({

  17. router,

  18. render: function(h) {

  19. return h(Frame);

  20. },

  21. store,

  22. });

  23. return { app, router, store };

  24. }

  25. module.exports = function createApp(ctx) {

  26. return createVueInstance(myRouter, ctx);

  27. }

注:在我所在的項(xiàng)目中,需要?jiǎng)討B(tài)判斷是否需要注冊(cè) DicomView,只有在客戶端才初始化 DicomView,由于 Node.js環(huán)境沒有 window對(duì)象,對(duì)于代碼運(yùn)行環(huán)境的判斷,可以通過 typeof window === 'undefined'來進(jìn)行判斷。

避免創(chuàng)建單例

如 Vue SSR文檔所述:

當(dāng)編寫純客戶端 (client-only) 代碼時(shí),我們習(xí)慣于每次在新的上下文中對(duì)代碼進(jìn)行取值。但是,Node.js 服務(wù)器是一個(gè)長期運(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方法通過返回一個(gè)返回值創(chuàng)建 Vue實(shí)例的對(duì)象的函數(shù)調(diào)用,在函數(shù) createVueInstance中,為每一個(gè)請(qǐng)求創(chuàng)建了 Vue, VueRouter, Vuex實(shí)例。并暴露給 entry-client和 entry-server模塊。

在客戶端 entry-client.js只需創(chuàng)建應(yīng)用程序,并且將其掛載到 DOM中:

  1. import { createApp } from './app';

  2. // 客戶端特定引導(dǎo)邏輯……

  3. const { app } = createApp();

  4. // 這里假定 App.vue 模板中根元素具有 `id='app'`

  5. 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ù)取邏輯:

  1. import { createApp } from './app';

  2. export default context => {

  3. const { app } = createApp();

  4. return app;

  5. }

在服務(wù)端用 vue-router分割代碼

與 Vue實(shí)例一樣,也需要?jiǎng)?chuàng)建單例的 vueRouter對(duì)象。對(duì)于每個(gè)請(qǐng)求,都需要?jiǎng)?chuàng)建一個(gè)新的 vueRouter實(shí)例:

  1. function createVueInstance(routes, ctx) {

  2. const router = Router({

  3. base: '/base',

  4. mode: 'history',

  5. routes: [routes],

  6. });

  7. const store = createStore({ ctx });

  8. // 把路由注入到vuex中

  9. sync(store, router);

  10. const app = new Vue({

  11. router,

  12. render: function(h) {

  13. return h(Frame);

  14. },

  15. store,

  16. });

  17. return { app, router, store };

  18. }

同時(shí),需要在 entry-server.js中實(shí)現(xiàn)服務(wù)器端路由邏輯,使用 router.getMatchedComponents方法獲取到當(dāng)前路由匹配的組件,如果當(dāng)前路由沒有匹配到相應(yīng)的組件,則 reject到 404頁面,否則 resolve整個(gè) app,用于 Vue渲染虛擬 DOM,并使用對(duì)應(yīng)模板生成對(duì)應(yīng)的 HTML字符串。

  1. const createApp = require('./app');

  2. module.exports = context => {

  3. return new Promise((resolve, reject) => {

  4. // ...

  5. // 設(shè)置服務(wù)器端 router 的位置

  6. router.push(context.url);

  7. // 等到 router 將可能的異步組件和鉤子函數(shù)解析完

  8. router.onReady(() => {

  9. const matchedComponents = router.getMatchedComponents();

  10. // 匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 404

  11. if (!matchedComponents.length) {

  12. return reject('匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 404');

  13. }

  14. // Promise 應(yīng)該 resolve 應(yīng)用程序?qū)嵗员闼梢凿秩?/span>

  15. resolve(app);

  16. }, reject);

  17. });

  18. }

在服務(wù)端預(yù)拉取數(shù)據(jù)

在 Vue服務(wù)端渲染,本質(zhì)上是在渲染我們應(yīng)用程序的'快照',所以如果應(yīng)用程序依賴于一些異步數(shù)據(jù),那么在開始渲染過程之前,需要先預(yù)取和解析好這些數(shù)據(jù)。服務(wù)端 WebServer Frame作為代理服務(wù)器,在服務(wù)端對(duì)接口服務(wù)發(fā)起請(qǐng)求,并將數(shù)據(jù)拼裝到全局 Vuex狀態(tài)中。

另一個(gè)需要關(guā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)用,所以它無法訪問 this。需要將 store和路由信息作為參數(shù)傳遞進(jìn)去:

舉個(gè)例子:

  1. <!-- Lung.vue -->

  2. <template>

  3. <div></div>

  4. </template>

  5. <script>

  6. export default {

  7. // ...

  8. async asyncData({ store, route }) {

  9. return Promise.all([

  10. store.dispatch('getA'),

  11. store.dispatch('myModule/getB', { root:true }),

  12. store.dispatch('myModule/getC', { root:true }),

  13. store.dispatch('myModule/getD', { root:true }),

  14. ]);

  15. },

  16. // ...

  17. }

  18. </script>

在 entry-server.js中,我們可以通過路由獲得與 router.getMatchedComponents()相匹配的組件,如果組件暴露出 asyncData,我們就調(diào)用這個(gè)方法。然后我們需要將解析完成的狀態(tài),附加到渲染上下文中。

  1. const createApp = require('./app');

  2. module.exports = context => {

  3. return new Promise((resolve, reject) => {

  4. const { app, router, store } = createApp(context);

  5. // 針對(duì)沒有Vue router 的Vue實(shí)例,在項(xiàng)目中為列表頁,直接resolve app

  6. if (!router) {

  7. resolve(app);

  8. }

  9. // 設(shè)置服務(wù)器端 router 的位置

  10. router.push(context.url.replace('/base', ''));

  11. // 等到 router 將可能的異步組件和鉤子函數(shù)解析完

  12. router.onReady(() => {

  13. const matchedComponents = router.getMatchedComponents();

  14. // 匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 404

  15. if (!matchedComponents.length) {

  16. return reject('匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 404');

  17. }

  18. Promise.all(matchedComponents.map(Component => {

  19. if (Component.asyncData) {

  20. return Component.asyncData({

  21. store,

  22. route: router.currentRoute,

  23. });

  24. }

  25. })).then(() => {

  26. // 在所有預(yù)取鉤子(preFetch hook) resolve 后,

  27. // 我們的 store 現(xiàn)在已經(jīng)填充入渲染應(yīng)用程序所需的狀態(tài)。

  28. // 當(dāng)我們將狀態(tài)附加到上下文,并且 `template` 選項(xiàng)用于 renderer 時(shí),

  29. // 狀態(tài)將自動(dòng)序列化為 `window.__INITIAL_STATE__`,并注入 HTML。

  30. context.state = store.state;

  31. resolve(app);

  32. }).catch(reject);

  33. }, reject);

  34. });

  35. }

客戶端托管全局狀態(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被改造為如下所示:

  1. import createApp from './app';

  2. const { app, router, store } = createApp();

  3. // 客戶端把初始化的store替換為window.__INITIAL_STATE__

  4. if (window.__INITIAL_STATE__) {

  5. store.replaceState(window.__INITIAL_STATE__);

  6. }

  7. if (router) {

  8. router.onReady(() => {

  9. app.$mount('#app')

  10. });

  11. } else {

  12. app.$mount('#app');

  13. }

常見問題的解決方案

至此,基本的代碼改造也已經(jīng)完成了,下面說的是一些常見問題的解決方案:

對(duì)于舊項(xiàng)目遷移到 SSR肯定會(huì)經(jīng)歷的問題,一般為在項(xiàng)目入口處或是 created、 beforeCreate生命周期使用了 DOM操作,或是獲取了 location對(duì)象,通用的解決方案一般為判斷執(zhí)行環(huán)境,通過 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,沒有找到_Vue實(shí)例的問題:

通過查看 Vue-router源碼發(fā)現(xiàn)沒有手動(dòng)調(diào)用 Vue.use(Vue-Router);。沒有調(diào)用 Vue.use(Vue-Router);在瀏覽器端沒有出現(xiàn)問題,但在服務(wù)端就會(huì)出現(xiàn)問題。對(duì)應(yīng)的 Vue-router源碼所示:

  1. VueRouter.prototype.init = function init (app /* Vue component instance */) {

  2. var this$1 = this;

  3. process.env.NODE_ENV !== 'production' && assert(

  4. install.installed,

  5. 'not installed. Make sure to call `Vue.use(VueRouter)` ' +

  6. 'before creating root instance.'

  7. );

  8. // ...

  9. }

由于 hash路由的參數(shù),會(huì)導(dǎo)致 vue-router不起效果,對(duì)于使用了 vue-router的前后端同構(gòu)應(yīng)用,必須換為 history路由。

由于客戶端每次請(qǐng)求都會(huì)對(duì)應(yīng)地把 cookie帶給接口側(cè),而服務(wù)端 Web ServerFrame作為代理服務(wù)器,并不會(huì)每次維持 cookie,所以需要我們手動(dòng)把
cookie透傳給接口側(cè),常用的解決方案是,將 ctx掛載到全局狀態(tài)中,當(dāng)發(fā)起異步請(qǐng)求時(shí),手動(dòng)帶上 cookie,如下代碼所示:

  1. // createStore.js

  2. // 在創(chuàng)建全局狀態(tài)的函數(shù)`createStore`時(shí),將`ctx`掛載到全局狀態(tài)

  3. export function createStore({ ctx }) {

  4. return new Vuex.Store({

  5. state: {

  6. ...state,

  7. ctx,

  8. },

  9. getters,

  10. actions,

  11. mutations,

  12. modules: {

  13. // ...

  14. },

  15. plugins: debug ? [createLogger()] : [],

  16. });

  17. }

當(dāng)發(fā)起異步請(qǐng)求時(shí),手動(dòng)帶上 cookie,項(xiàng)目中使用的是 Axios

  1. // actions.js

  2. // ...

  3. const actions = {

  4. async getUserInfo({ commit, state }) {

  5. let requestParams = {

  6. params: {

  7. random: tool.createRandomString(8, true),

  8. },

  9. headers: {

  10. 'X-Requested-With': 'XMLHttpRequest',

  11. },

  12. };

  13. // 手動(dòng)帶上cookie

  14. if (state.ctx.request.headers.cookie) {

  15. requestParams.headers.Cookie = state.ctx.request.headers.cookie;

  16. }

  17. // ...

  18. let res = await Axios.get(`${requestUrlOrigin}${url.GET_A}`, requestParams);

  19. commit(globalTypes.SET_A, {

  20. res: res.data,

  21. });

  22. }

  23. };

  24. // ...

  • 接口請(qǐng)求時(shí)報(bào) connect ECONNREFUSED 127.0.0.1:80的問題

原因是改造之前,使用客戶端渲染時(shí),使用了 devServer.proxy代理配置來解決跨域問題,而服務(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即可:

  1. const requestUrlOrigin = requestUrlOrigin = state.ctx.URL.origin;

  2. const res = await Axios.get(`${requestUrlOrigin}${url.GET_A}`, requestParams);

  • 對(duì)于 vue-router配置項(xiàng)有 base參數(shù)時(shí),初始化時(shí)匹配不到對(duì)應(yīng)路由的問題

在官方示例中的 entry-server.js

  1. // entry-server.js

  2. import { createApp } from './app';

  3. export default context => {

  4. // 因?yàn)橛锌赡軙?huì)是異步路由鉤子函數(shù)或組件,所以我們將返回一個(gè) Promise,

  5. // 以便服務(wù)器能夠等待所有的內(nèi)容在渲染前,

  6. // 就已經(jīng)準(zhǔn)備就緒。

  7. return new Promise((resolve, reject) => {

  8. const { app, router } = createApp();

  9. // 設(shè)置服務(wù)器端 router 的位置

  10. router.push(context.url);

  11. // ...

  12. });

  13. }

原因是設(shè)置服務(wù)器端 router的位置時(shí), context.url為訪問頁面的 url,并帶上了 base,在 router.push時(shí)應(yīng)該去除 base,如下所示:

  1. router.push(context.url.replace('/base', ''));

小結(jié)

本文為筆者通過對(duì)現(xiàn)有項(xiàng)目進(jìn)行改造,給現(xiàn)有項(xiàng)目加上 Vue服務(wù)端渲染的實(shí)踐過程的總結(jié)。

首先闡述了什么是 Vue服務(wù)端渲染,其目的、本質(zhì)及原理,通過在服務(wù)端使用 Vue的虛擬 DOM,形成初始化的 HTML字符串,即應(yīng)用程序的“快照”。帶來極大的性能優(yōu)勢,包括 SEO優(yōu)勢和首屏渲染的極速體驗(yàn)。之后闡述了 Vue服務(wù)端渲染的基本用法,即兩個(gè)入口、兩個(gè) webpack配置,分別作用于客戶端和服務(wù)端,分別生成 vue-ssr-client-manifest.json與 vue-ssr-server-bundle.json作為打包結(jié)果。最后通過對(duì)現(xiàn)有項(xiàng)目的改造過程,包括對(duì)路由進(jìn)行改造、數(shù)據(jù)預(yù)獲取和狀態(tài)初始化,并解釋了在 Vue服務(wù)端渲染項(xiàng)目改造過程中的常見問題,幫助我們進(jìn)行現(xiàn)有項(xiàng)目往 Vue服務(wù)端渲染的遷移。

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
利用 React/Redux/React
骨架屏技術(shù)講解以及如何在Vue中實(shí)現(xiàn)骨架屏
vite —— 一種新的、更快地 web 開發(fā)工具
徹底理解服務(wù)端渲染 - SSR原理
詳解如何使用Vue2做服務(wù)端渲染
入職第一天:前端leader手把手教我入門Vue服務(wù)器端渲染
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服