Vue-Cli3外掛實戰一:vue-cli-plugin-dll

SuperFe發表於2019-04-26

Author: 微笑向暖

前述

vue-cli3版本的釋出距今已經過了大半年,前後迭代了50多個版本,終於趨於穩定;這裡不得不得感嘆vue開源團隊對vue技術棧的傾力貢獻,使得vue社群的前端工程化實踐又向前邁了一大步。相比vue-cli2版本的'大鍋混',三版本的外掛系統卓識令人驚豔了一把,因此組內也在第一時間遷移了vue-cli3,本文算是對外掛系統的一次探索與學習,也算是一次拋磚引玉,期待後面繼續更新推出優秀的外掛並將開發外掛的經驗總結開源出來。


外掛開發背景

關於模組預編譯,網上的教程及webpack配置攻略非常多,沒有經驗的讀者可參考webpack dllPlugin。在前端專案迭代到中後期或者依賴第三方模組體積較大時,模組預編譯可有效提升webpack構建速度,但不同專案需要預編譯的模組不同,以及配置細節也不同,所以藉助vue-cli3封裝成vue-plugin-dll外掛,將構建邏輯封裝在外掛內部,對外開放預編譯的配置項,這樣可以使前端開發更專注於業務。

注意:本文封裝的vue-cli-plugin-dll未釋出到npm中,僅提供了開發外掛的思路和總結。

模組預編譯原理

webpack.dllPlugin本質是將大量複用模組且不會頻繁更新的庫進行預編譯,且只需要編譯一次,編譯完成後產出指定檔案(可以稱為動態連結庫)。在之後的構建過程中不會再對這些模組進行編譯,而是直接使用DllReferencePlugin來引用動態連結庫的程式碼,因此可以提高構建速度。一般可以將第三方模組進行預編譯,如 vue、vue-router、vuex等,只要這些依賴模組不更新,就不需要再重新編譯。

專案對比

在封裝vue-cli-plugin-dll外掛之前,需要探索一下模組預編譯對前端專案的影響有多大。 這裡實驗對比了兩個專案:

  • vue-init:vue-cli3構建的初始化專案。
  • sellgoods:vue-cli3構建且依賴其他三方庫的工程。

改造前現狀

開發環境,未預先執行dll指令碼進行預編譯

構建次數 第一次 第二次 第三次 第四次 平均用時
vue-init 2997ms 3561ms 2867ms 2935ms 3078ms
sellgoods 21449ms 16601ms 22480ms 22600ms 20782ms

生產環境,未預先執行dll指令碼進行預編譯

構建次數 第一次 第二次 第三次 第四次 平均用時 構建包大小
vue-init 3736ms 3713ms 3647ms 3800ms 3724ms 122.99 KB
sellgoods 52.09s 38.77s 39.78s 47.82s 44.615s 2.54 MB

改造後現狀

其中files指定了需要提前預編譯的模組list

// vue-init 預編譯列表
files: [
    'vue/dist/vue.runtime.esm.js',
    'vue-router',
    'vuex'
]

// sellgoods 預編譯列表
files: [
    'vue/dist/vue.runtime.esm.js',
    'vue-router',
    'vuex',
    'axios',
    'element-ui',
    'nprogress',
    'qs',
    'resize-observer-polyfill',
    'lodash'
]
複製程式碼

開發環境,執行dll指令碼提前預編譯

構建次數 第一次 第二次 第三次 第四次 平均用時
vue-init 2723ms 2849ms 2799ms 2774ms 2786ms
sellgoods 16115ms 16432ms 16479ms 15131ms 16039ms

生產環境,執行dll指令碼提前預編譯

構建次數 第一次 第二次 第三次 第四次 平均用時 構建包大小
vue-init 3057ms 2936ms 3708ms 2877ms 3144ms 25.06 KB
sellgoods 27.93s 27.60s 27.72s 27.10s 27.58s 1.51 MB

結果分析

實際上,影響webpack構建速度的因素存在很多,比如硬體設施、webpack配置是否合理、程式碼分割策略等等。這裡只針對預編譯(建立動態軟鏈)這一種情況的優化做了分析。

同時為了結果的可行性分析,這裡剔除了異常資料,僅對優化與未優化兩種結果的資料進行對比來進行討論。

從生產環境的構建時間可以看到:

  • vue-init:開發環境下構建平均耗時 3078ms;優化後平均耗時2786ms,速度提升10%左右;
  • sellgoods:開發環境下構建平均耗時20782ms;優化後平均耗時16039ms,速度提升30%左右。

從生產環境的構建時間可以看到:

  • vue-init:生產環境下構建平均耗時3724ms,優化後平均耗時3144ms,構建產出包大小由122.99 KB縮減到25.06 KB,速度提升18%左右。
  • sellgoods:生產環境下構建平均耗時44.615s,優化後平均耗時27.58s,構建產出包大小由2.54 MB縮減到1.51 MB,速度提升60%左右。

注:構建產物減少不意味著瀏覽器載入資源變少,而是減少的部分被提前預編譯,以script標籤形式在index.html中引入。

結論:

針對同一工程的不同環境下而言,預編譯對生產環境的構建提升速度明顯

從vue-init和sellgoods二者的生產環境與開發環境進行對比可以看到,不考慮硬體設施和其它因素影響的情況下,生產環境下的效率提升要比開發環境提升效率高出一倍左右。

預編譯的模組體積越大,構建提升效率越高

將sellgoods與vue-init進行橫向比較,vue-init專案是腳手架的初始專案,只新增了vue、vue-router、vuex等依賴庫;而sellgoods專案已進行到中後期,相對於vue-init而言,程式碼量及依賴的庫要多很多,其中以element-ui最為明顯。從結果可以看到,sellgoods無論是生產環境還是開發環境下,預編譯對構建效率的提升都要比vue-init明顯。

通用化方案

實際上,webpack.dllPlugin配置門檻很低,但沒有必要在每個工程中配置一遍,或者將底層配置開放給業務人員。這裡選擇了封裝vue-cli-plugin-dll外掛併發布到內網npm源中,供其他專案自由引用,下面詳細介紹如果一步步開放vue-cli3外掛。

外掛開發文件可見:vue外掛開發指南

1.構建外掛目錄
├── generator
├    └── index.js
├── service
├    ├── base.js
├    └── dll.js
├── index.js
└── package.json
複製程式碼
2.開發generator
const { red, green } = require('chalk');

module.exports = (api, options, rootOptions) => {
  api.extendPackage({
    scripts: {
      dll: 'vue-cli-service dll'
    },
    vue: {
      pluginOptions: {
        dll: {
          // 檔名
          entry: 'vendor',
          // 檔案輸出路徑
          filePath: './public/vendor',
          // 預編譯包
          files: ['vue/dist/vue.runtime.esm.js', 'vue-router', 'vuex'],
          // 是否保留歷史編譯記錄
          noCache: true
        }
      }
    }
  });
};
複製程式碼

generator對外暴露一個函式,對內接受一個api工具類(GeneratorAPI)負責對工程做偏好設定。這裡我們藉助extendPackage方法向package.json檔案注入dll指令,以及dll外掛的初始化配置。如果建立專案的時候勾選了useConfigFiles,那麼vue屬性下的配置將會被注入到vue.config.js檔案中。

3.開發service(index.js)
module.exports = (api, ops) => {
  require('./service/base')(api, ops);
  require('./service/dll')(api, ops);
};

module.exports.defaultModes = {
  dll: 'production'
};
複製程式碼

service也對外暴露一個函式,並接受api工具類(PluginAPI)負責對webpack作更新配置。 這裡我們將webpack配置進行解耦,base配置公共webpack邏輯,建立動態軟鏈;而dll負責預編譯模組邏輯。

4.開發dll指令
const { red, green } = require('chalk');

module.exports = (api, ops = {}) => {
  api.registerCommand(
    'dll',
    {
      description: '第三方模組預編譯',
      usage: 'vue-cli-service dll'
    },
    async args => {
      const Config = require('webpack-chain');
      const webpack = require('webpack');
      const fs = require('fs-extra');
      const path = require('path');
      const {
        log,
        done,
        logWithSpinner,
        stopSpinner
      } = require('@vue/cli-shared-utils');

      logWithSpinner(green('Building dll files to public vendor'));

      const config = new Config();
      const pluginOptions = ops.pluginOptions || {};
      const root = api.getCwd();
      const dllConfig = pluginOptions.dll;

      if (!dllConfig) {
        log();
        log(red('缺失dll檔案配置'));
        log();
        process.exit(0);
      }

      function resolve(dir) {
        return path.resolve(root, dir);
      }
      function hasVendor(filePath) {
        return fs.existsSync(resolve(filePath));
      }

      // 預設打到public/vendor資料夾裡
      const {
        entry = 'vendor',
        filePath = `./public/${entry}`,
        files,
        noCache = true
      } = dllConfig;

      if (files.length) {
        files.forEach(oneOf => config.entry(entry).add(oneOf));
      }

      config.output
        .path(resolve(filePath))
        .filename('[name].dll.[hash:8].js')
        .library('[name]_[hash]')
        .end();

      if (noCache) {
        // 清空vendor快取
        config.when(hasVendor(filePath), () => {
          fs.removeSync(resolve(filePath));
        });
      }

      config
        .plugin('DllPlugin')
        .use(require('webpack/lib/DllPlugin'), [
          {
            name: '[name]_[hash]',
            path: path.join(root, filePath, '[name]-manifest.json'),
            context: root
          }
        ])
        .end();

      const result = config.toConfig();
      webpack(result, (err, stats) => {
        stopSpinner(false);
        if (err) {
          log();
          log(red(err));
          log();
          return false;
        }
        done(green('Build complete'));
      });
    }
  );
};

複製程式碼

這裡藉助registerCommand方法註冊dll指令,與generator中擴充套件的指令碼前後呼應,在dll方法中,核心使用webpack/lib/DllPlugin外掛預編譯模組,併產生快取檔案,供其他環境配置使用。

5.開發base.js
module.exports = (api, ops) => {
  const webpack = require('webpack');
  const path = require('path');
  const fs = require('fs');
  const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
  const root = api.getCwd();

  function resolve(dir) {
    return path.resolve(root, dir);
  }

  if (ops && ops.pluginOptions) {
    const { entry = 'vendor', filePath = `./public/${entry}` } =
      ops.pluginOptions.dll || {};
    const outputPath = path.basename(filePath) || entry;
    if (fs.existsSync(path.join(filePath, `${entry}-manifest.json`))) {
      api.configureWebpack(config => {
        config.plugins.push(
          new webpack.DllReferencePlugin({
            context: root,
            manifest: require(resolve(`${filePath}/${entry}-manifest.json`))
          }),
          new AddAssetHtmlPlugin({
            filepath: resolve(`${filePath}/*.js`),
            publicPath: `./${outputPath}`,
            outputPath: `./${outputPath}`
          })
        );
      });
    }
  }
};
複製程式碼

在外掛安裝完畢之後,執行yarn dll指令,即可將預編譯的包及快取打到public/vendor目錄下,這時還需為其他環境(如開發和生產環境)配置動態軟鏈,忽略預編譯模組的構建。在base.js中藉助configureWebpack方法將建立動態軟鏈的配置更新到最終版的webpack配置中(也可使用chainWebpack)。

總結

至此,一個初步的vue-cli-plugin-dll外掛開發完畢,具備了預編譯模組的功能,但仍有很多的不足,比如未開放預編譯模組的loader或者plugin定製功能等,這裡僅是一次外掛封裝的嘗試。

轉載請註明出處,十分感謝!

相關文章