Vue 處事端襯實在踐 ——Web操縱首屏耗時最最佳化籌劃

尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️

加入LINE好友

隨著各大前端框架的誕生和演變, SPA開始流行,單頁面應用的優勢在於可以不重新加載整個頁面的情況下,通過 ajax和服務器通信,做到整個 Web應用拒不更新,帶來了極致的用戶體驗。然而,對於需要 SEO、追求極致的首屏性能的應用,前端渲染的 SPA是糟糕的。好在 Vue2.0後是支持服務端渲染的,零零散散花費了兩三周事件,通過改造現有項目,基本完成了在現有項目中實踐了 Vue服務端渲染。

關於Vue服務端渲染的原理、搭建,官方文檔已經講的比較詳細了,因此,本文不是抄襲文檔,而是文檔的補充。特別是對於如何與現有項目進行很好的結合,還是需要費很大功夫的。本文主要對我所在的項目中進行 Vue服務端渲染的改造過程進行闡述,加上一些個人的理解,作為分享與學習。

概述

本文主要分以下幾個方面:

  • 什麼是服務端渲染?服務端渲染的原理是什麼?
  • 如何在基於 Koa的 Web Server Frame上配置服務端渲染?
  • 如何對現有項目進行改造?
    • 在服務端預拉取數據;
    • 客戶端托管全局狀態;
    • 常見問題的解決方案;
    • 基本目錄改造;
    • 在服務端用 vue-router分割代碼;

什麼是服務端渲染?服務端渲染的原理是什麼?

Vue.js是構建客戶端應用程序的框架。默認情況下,可以在瀏覽器中輸出 Vue組件,進行生成 DOM和操作 DOM。然而,也可以將同一個組件渲染為服務器端的 HTML字符串,將它們直接髮送到瀏覽器,最後將這些靜態標記”激活”為客戶端上完全可交互的應用程序。

上面這段話是源自Vue服務端渲染文檔的解釋,用通俗的話來說,大概可以這麼理解:

  • 服務端渲染的目的是:性能優勢。 在服務端生成對應的 HTML字符串,客戶端接收到對應的 HTML字符串,能立即渲染 DOM,最高效的首屏耗時。此外,由於服務端直接生成了對應的 HTML字符串,對 SEO也非常友好;
  • 服務端渲染的本質是:生成應用程序的「快照」。將 Vue及對應庫運行在服務端,此時, Web Server Frame實際上是作為代理服務器去訪問接口服務器來預拉取數據,從而將拉取到的數據作為 Vue組件的初始狀態。
  • 服務端渲染的原理是:虛擬 DOM。在 Web Server Frame作為代理服務器去訪問接口服務器來預拉取數據後,這是服務端初始化組件需要用到的數據,此後,組件的beforeCreate和 created生命周期會在服務端調用,初始化對應的組件後, Vue啟用虛擬 DOM形成初始化的 HTML字符串。之後,交由客戶端托管。做到前後端同構應用。

如何在基於 Koa的 Web Server Frame上配置服務端渲染? 基本用法

需要用到 Vue服務端渲染對應庫 vue–server–renderer,通過 npm安裝:

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

最簡單的,首先渲染一個 Vue實例:

  1. // 第 1 步:創建一個 Vue 實例
  2. const Vue = require(‘vue’);
  3. const app = new Vue({
  4. template: `<div>Hello World</div>`
  5. });
  6. // 第 2 步:創建一個 renderer
  7. const renderer = require(‘vue-server-renderer’).createRenderer();
  8. // 第 3 步:將 Vue 實例渲染為 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. });

與服務器集成:

  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); // 渲染出錯的頁面
  10. }
  11. ctx.body = html;
  12. }

使用頁面模板:

當你在渲染 Vue應用程序時, renderer只從應用程序生成 HTML標記。在這個示例中,我們必須用一個額外的 HTML頁麵包裹容器,來包裹生成的 HTML標記。

為了簡化這些,你可以直接在創建 renderer時提供一個頁面模板。多數時候,我們會將頁面模板放在特有的文件中:

  1. <!DOCTYPE html>
  2. <html lang=”en”>
  3. <head><title>Hello</title></head>
  4. <body>
  5. <!–vue-ssr-outlet–>
  6. </body>
  7. </html>

然後,我們可以讀取和傳輸文件到 Vuerenderer中:

  1. const tpl = fs.readFileSync(path.resolve(__dirname, ‘./index.html’), ‘utf-8’);
  2. const renderer = vssr.createRenderer({
  3. template: tpl,
  4. });

Webpack配置

然而在實際項目中,不止上述例子那麼簡單,需要考慮很多方面:路由、數據預取、組件化、全局狀態等,所以服務端渲染不是只用一個簡單的模板,然後加上使用 vue–server–renderer完成的,如下面的示意圖所示:

Vue 處事端襯實在踐 ——Web操縱首屏耗時最最佳化籌劃 科技 第1張

如示意圖所示,一般的 Vue服務端渲染項目,有兩個項目入口文件,分別為 entry–client.js和 entry–server.js,一個僅運行在客戶端,一個僅運行在服務端,經過 Webpack打包後,會生成兩個 Bundle,服務端的 Bundle會用於在服務端使用虛擬 DOM生成應用程序的「快照」,客戶端的 Bundle會在瀏覽器執行。

因此,我們需要兩個 Webpack配置,分別命名為 webpack.client.config.js和 webpack.server.config.js,分別用於生成客戶端 Bundle與服務端 Bundle,分別命名為 vue–ssr–client–manifest.json與 vue–ssr–server–bundle.json,關於如何配置, Vue官方有相關示例vue-hackernews-2.0

開發環境搭建

我所在的項目使用 Koa作為 WebServerFrame,項目使用koa-webpack進行開發環境的構建。如果是在產品環境下,會生成 vue–ssr–client–manifest.json與 vue–ssr–server–bundle.json,包含對應的 Bundle,提供客戶端和服務端引用,而在開發環境下,一般情況下放在內存中。使用 memory–fs模塊進行讀取。

  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. // 生成服務端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. // 服務端渲染生成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模塊,應用程序可以直接讀取
  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. // 服務端渲染的server bundle 存儲到內存里
  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模塊,應用程序可以直接讀取
  62. bundle = JSON.parse(mfs.readFileSync(path.resolve(cwd,’./dist/vue-ssr-server-bundle.json’), ‘utf-8’));
  63. appSSR.bundle = bundle;
  64. });
  65. }

渲染中間件配置

產品環境下,打包後的客戶端和服務端的 Bundle會存儲為 vue–ssr–client–manifest.json與 vue–ssr–server–bundle.json,通過文件流模塊 fs讀取即可,但在開發環境下,我創建了一個 appSSR模塊,在發生代碼更改時,會觸發 Webpack熱更新, appSSR對應的 bundle也會更新, 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模塊,在開發環境下,就可以拿到 clientManifest和 ssrBundle,項目的渲染中間件如下:

  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. // 開發環境下,通過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(‘進入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 // (可選)客戶端構建 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. };

如何對現有項目進行改造? 基本目錄改造

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

一個基本項目可能像是這樣:

  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 # 僅運行於瀏覽器
  9. │ ├── entry-server.js # 僅運行於服務器
  10. │ └── index.vue # 項目入口組件
  11. ├── pages
  12. ├── routers
  13. └── store

app.js是我們應用程序的「通用 entry」。在純客戶端應用程序中,我們將在此文件中創建根 Vue實例,並直接掛載到 DOM。但是,對於服務器端渲染( SSR),責任轉移到純客戶端 entry文件。 app.js簡單地使用 export導出一個 createApp函數:

  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. }

註:在我所在的項目中,需要動態判斷是否需要註冊 DicomView,只有在客戶端才初始化 DicomView,由於 Node.js環境沒有 window對象,對於代碼運行環境的判斷,可以通過 typeofwindow ===‘undefined’來進行判斷。

避免創建單例

如 VueSSR文檔所述:

當編寫純客戶端 (client-only) 代碼時,我們習慣於每次在新的上下文中對代碼進行取值。但是,Node.js 服務器是一個長期運行的進程。當我們的代碼進入該進程時,它將進行一次取值並留存在內存中。這意味著如果創建一個單例對象,它將在每個傳入的請求之間共享。如基本示例所示,我們為每個請求創建一個新的根 Vue 實例。這與每個用戶在自己的瀏覽器中使用新應用程序的實例類似。如果我們在多個請求之間使用一個共享的實例,很容易導致交叉請求狀態污染 (cross-request state pollution)。因此,我們不應該直接創建一個應用程序實例,而是應該暴露一個可以重復執行的工廠函數,為每個請求創建新的應用程序實例。同樣的規則也適用於 router、store 和 event bus 實例。你不應該直接從模塊導出並將其導入到應用程序中,而是需要在 createApp 中創建一個新的實例,並從根 Vue 實例注入。

如上代碼所述, createApp方法通過返回一個返回值創建 Vue實例的對象的函數調用,在函數 createVueInstance中,為每一個請求創建了 Vue, VueRouter, Vuex實例。並暴露給 entry–client和 entry–server模塊。

在客戶端 entry–client.js只需創建應用程序,並且將其掛載到 DOM中:

  1. import { createApp } from ‘./app’;
  2. // 客戶端特定引導邏輯……
  3. const { app } = createApp();
  4. // 這里假定 App.vue 模板中根元素具有 `id=”app”`
  5. app.$mount(‘#app’);

服務端 entry–server.js使用 defaultexport導出函數,並在每次渲染中重復調用此函數。此時,除了創建和返回應用程序實例之外,它不會做太多事情 – 但是稍後我們將在此執行服務器端路由匹配和數據預取邏輯:

  1. import { createApp } from ‘./app’;
  2. export default context => {
  3. const { app } = createApp();
  4. return app;
  5. }

在服務端用 vue-router 分割代碼

與 Vue實例一樣,也需要創建單例的 vueRouter對象。對於每個請求,都需要創建一個新的 vueRouter實例:

  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. }

同時,需要在 entry–server.js中做到服務器端路由邏輯,使用 router.getMatchedComponents方法獲取到當前路由匹配的組件,如果當前路由沒有匹配到相應的組件,則 reject到 404頁面,否則 resolve整個 app,用於 Vue渲染虛擬 DOM,並使用對應模板生成對應的 HTML字符串。

  1. const createApp = require(‘./app’);
  2. module.exports = context => {
  3. return new Promise((resolve, reject) => {
  4. // …
  5. // 設置服務器端 router 的位置
  6. router.push(context.url);
  7. // 等到 router 將可能的異步組件和鉤子函數解析完
  8. router.onReady(() => {
  9. const matchedComponents = router.getMatchedComponents();
  10. // 匹配不到的路由,執行 reject 函數,並返回 404
  11. if (!matchedComponents.length) {
  12. return reject(‘匹配不到的路由,執行 reject 函數,並返回 404’);
  13. }
  14. // Promise 應該 resolve 應用程序實例,以便它可以渲染
  15. resolve(app);
  16. }, reject);
  17. });
  18. }

在服務端預拉取數據

在 Vue服務端渲染,本質上是在渲染我們應用程序的”快照”,所以如果應用程序依賴於一些異步數據,那麼在開始渲染過程之前,需要先預取和解析好這些數據。服務端 WebServerFrame作為代理服務器,在服務端對接口服務發起請求,並將數據拼裝到全局 Vuex狀態中。

另一個需要關注的問題是在客戶端,在掛載到客戶端應用程序之前,需要獲取到與服務器端應用程序完全相同的數據 – 否則,客戶端應用程序會因為使用與服務器端應用程序不同的狀態,然後導致混合失敗。

目前較好的解決方案是,給路由匹配的一級子組件一個 asyncData,在 asyncData方法中, dispatch對應的 action。 asyncData是我們約定的函數名,表示渲染組件需要預先執行它獲取初始數據,它返回一個 Promise,以便我們在後端渲染的時候可以知道什麼時候該操作完成。注意,由於此函數會在組件實例化之前調用,所以它無法訪問 this。需要將 store和路由信息作為參數傳遞進去:

舉個例子:

  1. <!– Lung.vue –>
  2. <template>
  3. <div></div>
  4. </template>
  5. <>
  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. </>

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

  1. const createApp = require(‘./app’);
  2. module.exports = context => {
  3. return new Promise((resolve, reject) => {
  4. const { app, router, store } = createApp(context);
  5. // 針對沒有Vue router 的Vue實例,在項目中為列表頁,直接resolve app
  6. if (!router) {
  7. resolve(app);
  8. }
  9. // 設置服務器端 router 的位置
  10. router.push(context.url.replace(‘/base’, ”));
  11. // 等到 router 將可能的異步組件和鉤子函數解析完
  12. router.onReady(() => {
  13. const matchedComponents = router.getMatchedComponents();
  14. // 匹配不到的路由,執行 reject 函數,並返回 404
  15. if (!matchedComponents.length) {
  16. return reject(‘匹配不到的路由,執行 reject 函數,並返回 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. // 在所有預取鉤子(preFetch hook) resolve 後,
  27. // 我們的 store 現在已經填充入渲染應用程序所需的狀態。
  28. // 當我們將狀態附加到上下文,並且 `template` 選項用於 renderer 時,
  29. // 狀態將自動序列化為 `window.__INITIAL_STATE__`,並注入 HTML。
  30. context.state = store.state;
  31. resolve(app);
  32. }).catch(reject);
  33. }, reject);
  34. });
  35. }

客戶端托管全局狀態

當服務端使用模板進行渲染時, context.state將作為 window.__INITIAL_STATE__狀態,自動嵌入到最終的 HTML中。而在客戶端,在掛載到應用程序之前, store就應該獲取到狀態,最終我們的 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. }

常見問題的解決方案

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

對於舊項目遷移到 SSR肯定會經歷的問題,一般為在項目入口處或是 created、 beforeCreate生命周期使用了 DOM操作,或是獲取了 location對象,通用的解決方案一般為判斷執行環境,通過 typeofwindow是否為 ‘undefined’,如果遇到必須使用 location對象的地方用於獲取 url中的相關參數,在 ctx對象中也可以找到對應參數。

  • vue-router報錯 Uncaught TypeError: _Vue.extend is not _Vue function,沒有找到_Vue實例的問題:

通過查看 Vue–router源碼發現沒有手動調用 Vue.use(Vue–Router);。沒有調用 Vue.use(Vue–Router);在瀏覽器端沒有出現問題,但在服務端就會出現問題。對應的 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路由的參數,會導致 vue–router不起效果,對於使用了 vue–router的前後端同構應用,必須換為 history路由。

由於客戶端每次請求都會對應地把 cookie帶給接口側,而服務端 WebServerFrame作為代理服務器,並不會每次維持 cookie,所以需要我們手動把

cookie透傳給接口側,常用的解決方案是,將 ctx掛載到全局狀態中,當發起異步請求時,手動帶上 cookie,如下代碼所示:

  1. // createStore.js
  2. // 在創建全局狀態的函數`createStore`時,將`ctx`掛載到全局狀態
  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. }

當發起異步請求時,手動帶上 cookie,項目中使用的是 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’: ”,
  11. },
  12. };
  13. // 手動帶上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. // …
  • 接口請求時報 connect ECONNREFUSED 127.0.0.1:80的問題

原因是改造之前,使用客戶端渲染時,使用了 devServer.proxy代理配置來解決跨域問題,而服務端作為代理服務器對接口發起異步請求時,不會讀取對應的 webpack配置,對於服務端而言會對應請求當前域下的對應 path下的接口。

解決方案為去除 webpack的 devServer.proxy配置,對於接口請求帶上對應的 origin即可:

  1. const requestUrlOrigin = requestUrlOrigin = state.ctx.URL.origin;
  2. const res = await Axios.get(`${requestUrlOrigin}${url.GET_A}`, requestParams);
  • 對於 vue-router配置項有 base參數時,初始化時匹配不到對應路由的問題

在官方示例中的 entry–server.js:

  1. // entry-server.js
  2. import { createApp } from ‘./app’;
  3. export default context => {
  4. // 因為有可能會是異步路由鉤子函數或組件,所以我們將返回一個 Promise,
  5. // 以便服務器能夠等待所有的內容在渲染前,
  6. // 就已經準備就緒。
  7. return new Promise((resolve, reject) => {
  8. const { app, router } = createApp();
  9. // 設置服務器端 router 的位置
  10. router.push(context.url);
  11. // …
  12. });
  13. }

原因是設置服務器端 router的位置時, context.url為訪問頁面的 url,並帶上了 base,在 router.push時應該去除 base,如下所示:

  1. router.push(context.url.replace(‘/base’, ”));

小結

本文為筆者通過對現有項目進行改造,給現有項目加上 Vue服務端渲染的實踐過程的總結。

首先闡述了什麼是 Vue服務端渲染,其目的、本質及原理,通過在服務端使用 Vue的虛擬 DOM,形成初始化的 HTML字符串,即應用程序的「快照」。帶來極大的性能優勢,包括 SEO優勢和首屏渲染的極速體驗。之後闡述了 Vue服務端渲染的基本用法,即兩個入口、兩個 webpack配置,分別作用於客戶端和服務端,分別生成 vue–ssr–client–manifest.json與 vue–ssr–server–bundle.json作為打包結果。最後通過對現有項目的改造過程,包括對路由進行改造、數據預獲取和狀態初始化,並解釋了在 Vue服務端渲染項目改造過程中的常見問題,幫助我們進行現有項目往 Vue服務端渲染的遷移。

文章最後,打個廣告:騰訊醫療部門招前端工程師啦,HC無限多,社招、校招均可內推。如果有想來騰訊的小夥伴,可以添加我的微信:xingbofeng001,如果有想交朋友、交流技術的小夥伴也歡迎添加我的微信~

●編號920,輸入編號直達本文

About 尋夢園
尋夢園是台灣最大的聊天室及交友社群網站。 致力於發展能夠讓會員們彼此互動、盡情分享自我的平台。 擁有數百間不同的聊天室 ,讓您隨時隨地都能找到志同道合的好友!