中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Vue服務端渲染之Web應用首屏耗時最優化方法

發布時間:2021-09-10 18:32:46 來源:億速云 閱讀:150 作者:柒染 欄目:web開發

這期內容當中小編將會給大家帶來有關Vue服務端渲染之Web應用首屏耗時最優化方法,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

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

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字符串。之后,交由客戶端托管。實現前后端同構應用。

如何在基于Koa的Web 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服務端渲染之Web應用首屏耗時最優化方法了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

黔江区| 北安市| 和硕县| 怀宁县| 丰宁| 神木县| 洛隆县| 巴塘县| 通许县| 和平区| 申扎县| 临江市| 壤塘县| 横峰县| 旬阳县| 晋城| 南宁市| 涿鹿县| 江陵县| 扶风县| 辽阳市| 庐江县| 巢湖市| 阳东县| 汉川市| 江门市| 昌邑市| 凤冈县| 德兴市| 芜湖市| 承德市| 芷江| 连平县| 泗洪县| 宜兰县| 安岳县| 鹤壁市| 平遥县| 临高县| 额尔古纳市| 洛宁县|