webpack ——提高工程化(實踐篇)

java06051515發表於2019-12-09

wepack ——提高工程化(實踐篇)

webpack 是我們前端工程師必須掌握的一項技能,我們的日常開發已經離不開這個配置。關於這方面的文章已經很多,但還是想把自己的學習過程總結記錄下來。
上一篇文章介紹了webpack 構建原理,這篇文章將基於這個原理之上,講述在我們實際工程配置中可以去優化的2 個方向。

  • 提升構建速度,也就是減少整個打包構建的時間,
  • 優化構建輸出,也就是減小我們最終構建輸出的檔案體積。

1. 提升構建速度

1.1 哪些階段可以提速?

我們先回顧下整個構建過程,首先從入口檔案開始遞迴生成所有檔案的module例項,再針對所有module 例項的依賴關係進行分析優化,劃分為一個或多個 chunk 去生成最終的打包檔案輸出。那哪些階段我們可以去節約時間呢?

module 例項的優化和處理的時間我們並不好做提速,這裡往往涉及到最終輸出檔案的大小,我們做的優化操作越多,輸出的檔案體積越小,這是我們希望看到的,所以只能從生成 module 例項階段去入手提速,在這個階段檔案會經過以下處理:

  • resovle階段: 獲取檔案所在的絕對路徑以及檔案要被哪些loaders編譯轉換
  • run-loader階段:執行對應的 loaders對檔案執行編譯轉換
  • parse 階段:解析檔案是否存在依賴,以及對應的依賴檔案。

在這個過程中,我們可以節約時間的方向:

  • resolve 階段: 減少查詢檔案絕對路徑的時間
  • run-loader階段: 減少要被 loader 執行編譯轉換的檔案數量

1.2 如何配置?

  • resolve 階段
 resolve: {
    // 位於 src 資料夾下常用模組,建立別名,準確匹配
    alias: {
      xyz$: path.resolve(__dirname, 'path/to/file.js')
    },
    modules: ['node_modules'],  // 查詢範圍的減小
    extensions: ['.js', '.json'],  // import 檔案未加檔案字尾是,webpack 根據 extensions 定義的檔案字尾進行依次查詢
    mainFields: ['loader', 'main'],  // 減少入口檔案的搜尋步驟,設定第三方模組的入口檔案位置
  },
  • 充分利用別名,配置別名的路徑,可準確匹配到對應檔案路徑,減少檔案查詢時間
  • 配置 modules,減少檔案查詢範圍
  • extensions 配置項要充分利用,在我們引入的檔案沒有新增字尾資訊時,webpack 會遍歷此配置項,依次加上配置項陣列裡的字尾去查詢匹配檔案,所以在我們日常的開發中最好加上檔案字尾,可以省略新增字尾查詢的步驟,或者我們把高頻的檔案字尾放在陣列前面,這樣通過減少遍歷次數去節約時間
  • run-loader 階段
 module: {
    rules: [
      {
        test: /\.js$/, // 匹配的檔案
        use: 'babel-loader',
        // 在此檔案範圍內去查詢
        include: [],
        // 此檔案範圍內不去查詢
        exclude: file => (
          /node_modules/.test(file) &&
          !/\.vue\.js/.test(file)
        )
      }
    ]
  }

在此階段要充分利用include和exclude配置項,將需要經過 loader 轉換的檔案限定在某個範圍內,或者把不需要經過此 loader 執行的檔案過濾掉。

2. 優化構建輸出

減小最終輸出的包體積可以從如下幾個方向著手:

  • tree-shaking
  • 程式碼壓縮

2.1 Tree-shaking

Tree-shaking的意思是將我們沒有用到的程式碼剔除掉,從而減少總的打包體積。在 webpack4.0 生產模式中,已預設幫我開啟了 tree-shaking,我們先了解下它tree-shaking的原理

// a.js
import {
  add
} from './b.js'
add(1, 2)
// b.js
export function add(n1, n2) {
  return n1 + n2
}
export function square(x) {
  return x * x;
}
export function cube(x) {
  return x * x * x;
}

我們可以看到 b.js中的square和cube方法並沒有被使用到,我們在開發模式下開啟 usedExports的配置,

 mode: 'development',
 optimization: {
   usedExports: true
 }

b.js 最後打包的結果如下:

/***/ "./src/chunk/b.js":
/*!************************!*\
  !*** ./src/chunk/b.js ***!
  \************************/
/*! exports provided: add, square, cube */
/*! exports used: add */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return add; });
/* unused harmony export square */
/* unused harmony export cube */
// import('./d.js').then(mod => mod(100, 11))
function add(n1, n2) {
  return n1 + n2
}
function square(x) {
  return x * x;
}
function cube(x) {
  return x * x * x;
}

可以看到square和cube函式都被標記了/ unused harmony export /, webpack4.0 就是根據此標記集合生產模式下預設開啟的壓縮 minification進行 tree-shaking的。

當然,他有一些需要注意的點,否則很多時候我們可能會發現 treeshaking 無效

  • 使用 ES6 的模組化語法(import/esport)

正是由於 ES6 模組的靜態特性,使我們對依賴的靜態分析有了可能,才有了上面的/ harmony /標記,因此webpack4.0 的 treeshaking 必須基於 ES6 模組化語法。

  • 避免將 ES6 模組化語法經過 babel 編譯轉換為 commonJs 語法
  • 在專案 package.json檔案中,新增一個“sideEffects:false”屬性.

告知 webpack 所有程式碼不包含 side effect,可以安全刪除未用到的 export,當然我們 也可以配置指定檔案無side effect

  "sideEffects": [
    "./src/some-side-effectful-file.js"
  ]
  • 開啟程式碼壓縮,集合 ES6 模組化語法才能 tree-shaking

在生產模式中,預設開啟程式碼壓縮minification,預設載入的壓縮外掛是TerserPlugin,當然也可以使用其他具有刪除未引用程式碼能力的外掛來替換預設外掛,比如UglifyJsPlugin

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
  optimization: {
    minimizer: [new UglifyJsPlugin()],
  },
};

2.2 程式碼壓縮

  • js 程式碼壓縮

在 webpack 4.0的生產環境中 js 程式碼壓縮是預設開啟的,

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        test: /\.js(\?.*)?$/i,
        cache: true,
        parallel: true,
      }),
    ],
  },
};

此處注意webpack 是在 v4.26.0 將預設的壓縮外掛從 uglifyjs-webpack-plugin 改成 teaser-webpack-plugin 的( https://github.com/webpack/webpack/releases?after=v4.26.1 )因為uglifyjs-webpack-plugin 使用的 uglify-es 已經不再被維護,teaser是其一個分支。

  • css 程式碼壓縮

目前 webpack4 並未對 css 有內建任何優化,據說 webpack5 會內建 css 檔案的壓縮。目前可以使用’mini-css-extract-plugin’外掛進行壓縮。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename: "[name].css",
      chunkFilename: "[id].css"
    })
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              // you can specify a publicPath here
              // by default it use publicPath in webpackOptions.output
              publicPath: '../'
            }
          },
          "css-loader"
        ]
      }
    ]
  }
}

再來看一個來自官方文件的demo,集合了 js 和 css 的壓縮。

const TerserJSPlugin = require("terser-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = {
  optimization: {
    minimizer: [
      new TerserJSPlugin({}),
      new OptimizeCSSAssetsPlugin({})
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
      chunkFilename: "[id].css"
    })
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader"
        ]
      }
    ]
  }
}

2.3 拆包

前面介紹了 tree-shaking 和程式碼壓縮,那打包出來的體積是不是已經優化到了極致呢?非同步載入出現了。。。 比如vue-router 支援非同步路由。整體思想是將進入首頁所需要的所有資源打包到一個 chunk 中,其餘的資源非同步引用,在需要時再載入。

// a.js
import('./c').then(del => del(1, 2))

回顧下原理篇我們講的拆包原理,遇到非同步進行分 chunkgroup,在這個 c.js就會被單獨打包成一個 chunk 輸出。最後打包的結果如下:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
/***/ "./src/chunk/c.js":
/*!************************!*\
  !*** ./src/chunk/c.js ***!
  \************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return del; });
/* harmony import */ var _d_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./d.js */ "./src/chunk/d.js");
Object(_d_js__WEBPACK_IMPORTED_MODULE_0__["default"])(100, 11)
Promise.resolve(/*! import() */).then(__webpack_require__.bind(null, /*! ./b.js */ "./src/chunk/b.js")).then(add => add(1, 2))
function del(n1, n2) {
  return n1 - n2
}
/***/ })
}]);

在執行到 c.js 時,webpack 使用jsonp 的原理呼叫非同步引用的檔案c 。通過 script 標籤載入,src 指向 c 檔案的標識來呼叫執行。

涉及到拆包的配置如下,來自官方文件的 demo:

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};

通過對splitChunks的一些配置,可以設定我們拆包的一些規則。
在這裡有一 點需要注意:

  • webpack 預設把第三方庫對應的node_modules裡的檔案抽出單獨作為一個 chunk輸出,當然我們還可以講自己寫的公用工具類的程式碼也這麼抽出作為單獨chunk 輸出,這麼做的原因這些程式碼不太會變動,單純抽出可以充分利用瀏覽起的快取,加快下一次的載入速度。

3. 總結

webpack的配置優化可以從以下 2 個方向入手

  • 提高構建速度
    • Resolve階段提速,加快檔案查詢速度
    • loader 執行階段提速,減少要被 loader 執行的檔案數量
  • 優化構建輸出
    • tree-shaking
    • 程式碼壓縮
      • js 程式碼壓縮
      • css 程式碼壓縮
    • 拆包
      • 非同步載入
      • 抽出第三方庫

作者:吳海元

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559758/viewspace-2667502/,如需轉載,請註明出處,否則將追究法律責任。

相關文章