Vue 服務端渲染實踐 ——Web應用首屏耗時最優化方案

counterxing發表於2019-02-20

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

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

概述

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

  • 什麼是服務端渲染?服務端渲染的原理是什麼?
  • 如何在基於KoaWeb Server Frame上配置服務端渲染?
    • 基本用法
    • Webpack配置
    • 開發環境搭建
      • 渲染中介軟體配置
  • 如何對現有專案進行改造?
    • 基本目錄改造;
    • 在服務端用vue-router分割程式碼;
      • 在服務端預拉取資料;
      • 客戶端託管全域性狀態;
      • 常見問題的解決方案;

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

Vue.js是構建客戶端應用程式的框架。預設情況下,可以在瀏覽器中輸出Vue元件,進行生成DOM和操作DOM。然而,也可以將同一個元件渲染為伺服器端的HTML字串,將它們直接傳送到瀏覽器,最後將這些靜態標記"啟用"為客戶端上完全可互動的應用程式。

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

  • 服務端渲染的目的是:效能優勢。 在服務端生成對應的HTML字串,客戶端接收到對應的HTML字串,能立即渲染DOM,最高效的首屏耗時。此外,由於服務端直接生成了對應的HTML字串,對SEO也非常友好;
  • 服務端渲染的本質是:生成應用程式的“快照”。將Vue及對應庫執行在服務端,此時,Web Server Frame實際上是作為代理伺服器去訪問介面伺服器來預拉取資料,從而將拉取到的資料作為Vue元件的初始狀態。
  • 服務端渲染的原理是:虛擬DOM。在Web Server Frame作為代理伺服器去訪問介面伺服器來預拉取資料後,這是服務端初始化元件需要用到的資料,此後,元件的beforeCreatecreated生命週期會在服務端呼叫,初始化對應的元件後,Vue啟用虛擬DOM形成初始化的HTML字串。之後,交由客戶端託管。實現前後端同構應用。

如何在基於KoaWeb Server Frame上配置服務端渲染?

基本用法

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

npm install vue vue-server-renderer --save
複製程式碼

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


// 第 1 步:建立一個 Vue 例項
const Vue = require('vue');

const app = new Vue({
  template: `<div>Hello World</div>`
});

// 第 2 步:建立一個 renderer
const renderer = require('vue-server-renderer').createRenderer();

// 第 3 步:將 Vue 例項渲染為 HTML
renderer.renderToString(app, (err, html) => {
  if (err) {
  	throw err;
  }
  console.log(html);
  // => <div data-server-rendered="true">Hello World</div>
});
複製程式碼

與伺服器整合:


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); // 渲染出錯的頁面
	}
	

	ctx.body = html;
}
複製程式碼

使用頁面模板:

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

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

<!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配置

然而在實際專案中,不止上述例子那麼簡單,需要考慮很多方面:路由、資料預取、元件化、全域性狀態等,所以服務端渲染不是隻用一個簡單的模板,然後加上使用vue-server-renderer完成的,如下面的示意圖所示:

Vue 服務端渲染實踐 ——Web應用首屏耗時最優化方案

如示意圖所示,一般的Vue服務端渲染專案,有兩個專案入口檔案,分別為entry-client.jsentry-server.js,一個僅執行在客戶端,一個僅執行在服務端,經過Webpack打包後,會生成兩個Bundle,服務端的Bundle會用於在服務端使用虛擬DOM生成應用程式的“快照”,客戶端的Bundle會在瀏覽器執行。

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

開發環境搭建

我所在的專案使用Koa作為Web Server Frame,專案使用koa-webpack進行開發環境的構建。如果是在產品環境下,會生成vue-ssr-client-manifest.jsonvue-ssr-server-bundle.json,包含對應的Bundle,提供客戶端和服務端引用,而在開發環境下,一般情況下放在記憶體中。使用memory-fs模組進行讀取。

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;

// 生成服務端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);

  // 服務端渲染生成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模組,應用程式可以直接讀取
    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();
  });

  // 服務端渲染的server bundle 儲存到記憶體裡
  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模組,應用程式可以直接讀取
    bundle = JSON.parse(mfs.readFileSync(path.resolve(cwd,'./dist/vue-ssr-server-bundle.json'), 'utf-8'));
    appSSR.bundle = bundle;
  });
}
複製程式碼

渲染中介軟體配置

產品環境下,打包後的客戶端和服務端的Bundle會儲存為vue-ssr-client-manifest.jsonvue-ssr-server-bundle.json,通過檔案流模組fs讀取即可,但在開發環境下,我建立了一個appSSR模組,在發生程式碼更改時,會觸發Webpack熱更新,appSSR對應的bundle也會更新,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;
複製程式碼

通過引入appSSR模組,在開發環境下,就可以拿到clientManifestssrBundle,專案的渲染中介軟體如下:

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') {
  // 開發環境下,通過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('進入SSR,context為: ', JSON.stringify(context));
  const tpl = fs.readFileSync(path.resolve(__dirname, './newTemplate.html'), 'utf-8');
  const renderer = createBundleRenderer(bundle, {
    runInNewContext: false,
    template: tpl, // (可選)頁面模板
    clientManifest: clientManifest // (可選)客戶端構建 manifest
  });
  ctx.logger('createBundleRenderer  renderer:', JSON.stringify(renderer));
  try {
    html = await renderer.renderToString({
      ...context,
      url: context.CTX.url,
    });
  } catch(err) {
    ctx.logger('SSR renderToString 失敗: ', JSON.stringify(err));
    console.error(err);
  }

  ctx.body = html;
};

複製程式碼

如何對現有專案進行改造?

基本目錄改造

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

一個基本專案可能像是這樣:

src
├── components
│   ├── Foo.vue
│   ├── Bar.vue
│   └── Baz.vue
├── frame
│   ├── app.js # 通用 entry(universal entry)
│   ├── entry-client.js # 僅執行於瀏覽器
│   ├── entry-server.js # 僅執行於伺服器
│   └── index.vue # 專案入口元件
├── pages
├── routers
└── store
複製程式碼

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

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); 
}
複製程式碼

注:在我所在的專案中,需要動態判斷是否需要註冊DicomView,只有在客戶端才初始化DicomView,由於Node.js環境沒有window物件,對於程式碼執行環境的判斷,可以通過typeof window === 'undefined'來進行判斷。

避免建立單例

Vue SSR文件所述:

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

如上程式碼所述,createApp方法通過返回一個返回值建立Vue例項的物件的函式呼叫,在函式createVueInstance中,為每一個請求建立了VueVue RouterVuex例項。並暴露給entry-cliententry-server模組。

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

import { createApp } from './app';

// 客戶端特定引導邏輯……

const { app } = createApp();

// 這裡假定 App.vue 模板中根元素具有 `id="app"`
app.$mount('#app');
複製程式碼

服務端entry-server.js使用default export 匯出函式,並在每次渲染中重複呼叫此函式。此時,除了建立和返回應用程式例項之外,它不會做太多事情 - 但是稍後我們將在此執行伺服器端路由匹配和資料預取邏輯:

import { createApp } from './app';

export default context => {
  const { app } = createApp();
  return app;
}
複製程式碼

在服務端用vue-router分割程式碼

Vue例項一樣,也需要建立單例的vueRouter物件。對於每個請求,都需要建立一個新的vueRouter例項:

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 };
}
複製程式碼

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

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

module.exports = context => {
  return new Promise((resolve, reject) => {
    // ...
    // 設定伺服器端 router 的位置
    router.push(context.url);
    // 等到 router 將可能的非同步元件和鉤子函式解析完
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();
      // 匹配不到的路由,執行 reject 函式,並返回 404
      if (!matchedComponents.length) {
        return reject('匹配不到的路由,執行 reject 函式,並返回 404');
      }
      // Promise 應該 resolve 應用程式例項,以便它可以渲染
      resolve(app);
    }, reject);
  });

}
複製程式碼

在服務端預拉取資料

Vue服務端渲染,本質上是在渲染我們應用程式的"快照",所以如果應用程式依賴於一些非同步資料,那麼在開始渲染過程之前,需要先預取和解析好這些資料。服務端Web Server Frame作為代理伺服器,在服務端對介面服務發起請求,並將資料拼裝到全域性Vuex狀態中。

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

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

舉個例子:

<!-- 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中,我們可以通過路由獲得與router.getMatchedComponents()相匹配的元件,如果元件暴露出asyncData,我們就呼叫這個方法。然後我們需要將解析完成的狀態,附加到渲染上下文中。

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

module.exports = context => {
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp(context);
    // 針對沒有Vue router 的Vue例項,在專案中為列表頁,直接resolve app
    if (!router) {
      resolve(app);
    }
    // 設定伺服器端 router 的位置
      router.push(context.url.replace('/base', ''));
    // 等到 router 將可能的非同步元件和鉤子函式解析完
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();
      // 匹配不到的路由,執行 reject 函式,並返回 404
      if (!matchedComponents.length) {
        return reject('匹配不到的路由,執行 reject 函式,並返回 404');
      }
      Promise.all(matchedComponents.map(Component => {
        if (Component.asyncData) {
          return Component.asyncData({
            store,
            route: router.currentRoute,
          });
        }
      })).then(() => {
        // 在所有預取鉤子(preFetch hook) resolve 後,
        // 我們的 store 現在已經填充入渲染應用程式所需的狀態。
        // 當我們將狀態附加到上下文,並且 `template` 選項用於 renderer 時,
        // 狀態將自動序列化為 `window.__INITIAL_STATE__`,並注入 HTML。
        context.state = store.state;
        resolve(app);
      }).catch(reject);
    }, reject);
  });
}
複製程式碼

客戶端託管全域性狀態

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

常見問題的解決方案

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

  • 在服務端沒有windowlocation物件:

對於舊專案遷移到SSR肯定會經歷的問題,一般為在專案入口處或是createdbeforeCreate生命週期使用了DOM操作,或是獲取了location物件,通用的解決方案一般為判斷執行環境,通過typeof window是否為'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原始碼所示:

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."
  );
  // ...
}
複製程式碼
  • 服務端無法獲取hash路由的引數

由於hash路由的引數,會導致vue-router不起效果,對於使用了vue-router的前後端同構應用,必須換為history路由。

  • 介面處獲取不到cookie的問題:

由於客戶端每次請求都會對應地把cookie帶給介面側,而服務端Web Server Frame作為代理伺服器,並不會每次維持cookie,所以需要我們手動把 cookie透傳給介面側,常用的解決方案是,將ctx掛載到全域性狀態中,當發起非同步請求時,手動帶上cookie,如下程式碼所示:

// createStore.js
// 在建立全域性狀態的函式`createStore`時,將`ctx`掛載到全域性狀態
export function createStore({ ctx }) {
    return new Vuex.Store({
        state: {
            ...state,
            ctx,
        },
        getters,
        actions,
        mutations,
        modules: {
            // ...
        },
        plugins: debug ? [createLogger()] : [],
    });
}
複製程式碼

當發起非同步請求時,手動帶上cookie,專案中使用的是Axios

// actions.js

// ...
const actions = {
  async getUserInfo({ commit, state }) {
    let requestParams = {
      params: {
        random: tool.createRandomString(8, true),
      },
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
      },
    };

    // 手動帶上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,
    });
  }
};

// ...
複製程式碼
  • 介面請求時報connect ECONNREFUSED 127.0.0.1:80的問題

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

解決方案為去除webpackdevServer.proxy配置,對於介面請求帶上對應的origin即可:

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

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

// entry-server.js
import { createApp } from './app';

export default context => {
  // 因為有可能會是非同步路由鉤子函式或元件,所以我們將返回一個 Promise,
  // 以便伺服器能夠等待所有的內容在渲染前,
  // 就已經準備就緒。
  return new Promise((resolve, reject) => {
    const { app, router } = createApp();

    // 設定伺服器端 router 的位置
    router.push(context.url);

    // ...
  });
}
複製程式碼

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

router.push(context.url.replace('/base', ''));
複製程式碼

小結

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

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

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

相關文章