Vue-Cli3插件實戰一:vue-cli-plugin-dll

前述

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


插件開發背景

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

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

模塊預編譯原理

webpack.dllPlugin本質是將大量複用模塊且不會頻繁更新的庫進行預編譯,且只須要編譯一次,編譯完成後產出指定文件(能夠稱爲動態連接庫)。在以後的構建過程當中不會再對這些模塊進行編譯,而是直接使用DllReferencePlugin來引用動態連接庫的代碼,所以能夠提升構建速度。通常能夠將第三方模塊進行預編譯,如 vue、vue-router、vuex等,只要這些依賴模塊不更新,就不須要再從新編譯。webpack

項目對比

在封裝vue-cli-plugin-dll插件以前,須要探索一下模塊預編譯對前端項目的影響有多大。 這裏實驗對比了兩個項目:ios

  • vue-init:vue-cli3構建的初始化項目。
  • sellgoods:vue-cli3構建且依賴其餘三方庫的工程。

改造前現狀

開發環境,未預先運行dll腳本進行預編譯git

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

生產環境,未預先運行dll腳本進行預編譯github

構建次數 第一次 第二次 第三次 第四次 平均用時 構建包大小
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指定了須要提早預編譯的模塊listweb

// 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-router

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

生產環境,運行dll腳本提早預編譯vuex

構建次數 第一次 第二次 第三次 第四次 平均用時 構建包大小
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定製功能等,這裏僅是一次插件封裝的嘗試。

最後:歡迎大牛或者有經驗的前端從業人員對本文有誤內容不吝指導。

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

相關文章
相關標籤/搜索