webpack4 深刻了解

編譯打包相關知識

  • webpack 基本工做流程

    webpack 提供一個方法 webpack(options, callback) 用於 編譯打包源文件javascript

    webpack 方法 在執行過程當中, 經歷的 主要流程 以下:css

    1. 數據校驗,判斷傳入的 配置項 - configOption數據格式 是否符合 webpack 要求,不符合會直接拋錯vue

    2. 將用戶傳入的 配置項 - configOption 標準化, 根據 工做模式(development / production)entryoptimizationresolveoutput 等添加 相應的默認屬性java

      例如: 若是 configOption未指定 entry 屬性webpack 會向 configOption 添加 entry 屬性默認值'./src/index'node

    3. 構建一個 編譯器 - compiler, 用於 實際的編譯打包react

    4. 遍歷 configOption 中的 plugins 屬性, 給 compiler 安裝 用戶指定插件webpack

    5. 根據 webpack工做模式(development/ production) 以及 options配置項, 安裝 相應的插件web

    6. 執行 編譯器 - compilerrun 方法, 開始編譯打包正則表達式

  • 編譯器 - compiler

    編譯器 - compilerwebpack 編譯打包的核心npm

    webpack 會在 工做過程當中 構建一個 compiler, 而後執行 compiler.run 方法進行 編譯打包。 整個 編譯打包基本過程 以下:

    1. 構建一個 compilation params 對象, 用於 構建 compilation 對象。

      compilation params 對象, 能夠理解爲 編譯所需參數params 對象 中包含一個屬性: normalModuleFactory, 對應的 屬性值 是一個 對象, 能夠理解爲 模塊工廠。 這個 對象 提供了一個 create 方法, 執行這個方法, 能夠生成一個 module 對象

    2. 使用 compilation params 對象, 構建一個 compilation 對象。

      一個 compilation 對象,能夠理解爲 一個 編譯過程compilation 直接負責整個編譯打包過程

    3. compilation 開始 編譯打包 工做。

      整個 編譯打包工做流程 以下:

      1. 構建模塊依賴圖

        compilation 會以 入口文件(entry)起點, 找到 整個項目所須要的全部文件,爲 每個文件 都生成相應的 module 對象, 並 創建模塊之間的依賴關係, 生成一個 模塊依賴圖

      2. 模塊依賴圖 中的 模塊 分離生成 chunk

      3. 將生成的 chunks 轉化爲 bundles 並輸出到 指定位置(output)

    整個 編譯打包 過程, 簡單來說, 就是 webpack 先構建一個 編譯器 - compiler, 而後 compiler 根據一個 參數 - params, 生成一個 compilationcompilation入口文件起點,構建一個 模塊依賴圖, 將 模塊依賴圖 分離成 chunks, 而後 將 chunks 轉化爲 bundles 並輸出到指定位置

  • 模塊依賴圖

    使用 webpack 編譯打包源文件 時,每個 源文件 均可以當作一個 module 來處理,這個 源文件 能夠是 .js 文件.css 文件.json 文件 以及 .png 文件 等等。

    源文件 之間的 相互引用,使得 對應的 module 之間存在 依賴關係。若是一個 module A 使用的時候須要先引入 module B, 那麼 module A 就依賴於 module B

    webpack 會以 入口文件起點,找到 入口文件的依賴文件 以及 依賴文件的依賴文件直到最後一個依賴文件沒有依賴文件爲止。 在這個過程當中,會爲 每個文件 建立一個 module 對象, 而後 創建 module 對象以前的依賴關係, 最後造成一個 模塊依賴圖

    生成 模塊依賴圖 經歷的 過程 大體以下:

    1. 解析(resolve) entry 配置項 提供的 入口文件路徑,以獲取入口文件在磁盤中的位置(絕對路徑)

      解析過程中,會同時獲得 處理文件內容須要的 loaders(eslint-loader、babel-loader)解析(parse)文件內容須要的 parser構建最後輸出內容須要的 generator

    2. 構建一個 module 對象module 對象userRequestloadersparsergenerator 屬性分別指向步驟一輩子成的 文件絕對路徑處理文件內容須要的loaders解析文件內容須要的parser構建最後輸出內容須要的 generator

    3. 讀取 源文件的內容,使用收集的 loaders 處理 源文件的內容

      如: 使用 babel-loader 處理 js 內容, 使用 sass-loadercss-loader 等處理 css 內容

      loader 處理之後的 源文件內容, 會保存在 module 對象_source 屬性中。

    4. 使用 parser 解析 loader 處理過的文件內容獲取當前文件所依賴的文件 的 請求路徑

      parser 會將 文件內容 解析爲 ast - 抽象語法樹對象, 從 ast 中能夠獲取到 依賴文件的請求路徑(通常爲相對路徑)

      分析 ast 的時候, 會爲每個 依賴文件 生成一個 dep 對象dep 對象request 屬性 的值爲 依賴文件的請求路徑module 屬性的值爲 依賴對文件對應的 module,此時爲 null

      文件中 依賴的靜態文件對應的 dep 對象 會收集到 當前文件 對應的 moduledependencies 列表中,依賴的須要懶加載的文件對應的 dep 對象 會收集到 moduleblocks 列表中, 依賴的全局變量 會收集到 modulevariables 列表中。

    5. 解析 dependenciesblocksvariablesdep 對象 中的 請求路徑,獲取 依賴文件在磁盤中的位置 以及 解析依賴文件內容的loaders、parser,建立 依賴文件的 module 對象, 併爲 dep 對象module屬性 賦值。

      這樣, 經過 模塊dependenciesblocksvariables,即可創建 模塊之間的依賴關係

    6. 重複步驟2到步驟5, 直到模塊的 dependencies、blocks 列表中的值爲空爲止

    綜上, 一個 模塊依賴圖 便生成,而後用於 打包分離生成chunk

  • 模塊

    module 是構成 模塊依賴圖關鍵,在使用 webpack編譯打包源文件 時, 每個 源文件 都對應一個 module 對象module 對象 會中包含 源文件的請求路徑絕對路徑依賴文件文件輸出 等信息。

    一個 模塊 的生成,要經歷 resolvecreatebuild 三個階段,即 解析文件請求路徑生成 module 對象使用 loader 處理文件內容和使用 parser 解析文件內容

    • resolve

      源文件 構建一個 module 對象,首先要作的就是 解析源文件請求路徑, 獲 取源文件在本地磁盤的位置(絕對路徑)

      源文件請求路徑 能夠是 相對路徑絕對路徑模塊路徑別名路徑(resolve.alias)webpack 會根據 配置項 中的 context(基礎目錄)resolve 來解析 文件的請求路徑

      文件的請求路徑解析完成 之後, 會使用 解析生成的絕對路徑校驗文件是否存在。若是 不存在拋出 file no exist 異常

      若是 容許文件不須要擴展名(resolve.enforceExtension : false)請求路徑沒有擴展名,解析時會根據 配置項提供的自動解析的擴展(resolve.extensions - 默認值爲[".wasm", ".mjs", ".js", ".json"]), 依次補全絕對路徑,而後 校驗文件是否存在。 當 全部的擴展使用之後文件仍是不存在拋出 file no exist 異常

      文件的 絕對路徑生成 之後, 會根據 配置項 - modules 中提供的 rules, 收集 處理源文件 須要的 loader。 依據 loader名稱, 解析 loader絕對路徑, 如: babel-loader 解析之後的 絕對路徑 以下:

      // babel-loader 的絕對路徑
      D:\study\demo\webpack\webpack-4-demo\node_modules\_babel-loader@7.1.5@babel-loader\lib\index.js
      複製代碼

      此外, 還會生成一個 解析器 - parser, 用於 解析源文件內容,一個 生成器 - generator,構建 module 對象 對應的 輸出內容(js代碼字符串)

      綜上, 在 resolve 階段, webpack 會找到 源文件的絕對路徑(在本地磁盤中的位置)處理源文件內容須要的 loaders 的絕對路徑(在本地磁盤中的位置)解析源文件內容須要的 parser、以及 構建輸出內容的 generator, 而後使用這些信息構建一個 module 對象

    • create

      create 階段, webpack 會根據 resolve 階段 返回的 源文件絕對路徑loadersparsergenerator 等信息生成一個 module 對象

      生成的 module 對象 會添加到一個 緩存-cache 中,防止 相同文件的重複 resolve、build

      另外,module 對象 還會被添加到 compilation 對象modules 列表中(compilation.modules 會在構建 chunks 的時候使用)。

      生成的 module 對象 會添加到一個 緩存-cache 中,防止 相同文件的重複 resolve、build

      另外,module 對象 還會被添加到 compilation 對象modules 列表中(compilation.modules 會在構建 chunks 的時候使用)。

    • build

      build 階段webpack 作了 兩件事情: 使用 resolve 階段收集的 loaders 處理源文件內容, 而後 使用 parser 解析 loader 處理之後的源文件內容

      具體的 build 流程 以下:

      1. 根據 收集的 loaders 的絕對路徑, 經過 require(path) 的方式, 獲取 每個 loader 提供的 方法

      2. 根據 源文件絕對路徑讀取源文件的內容(是一個字符串)

      3. 使用 步驟一loader 提供的方法源文件的內容 進行 預處理

        如: 使用 css-loader 處理 css 內容字符串babel-loader 處理 js 內容字符串vue-loader 處理 .vue 文件內容字符串

        loader 返回的都是 js 格式的內容字符串

      4. 使用 parser 解析 步驟三 返回的 內容字符串

        parser 會將 輸入的代碼內容字符串 解析爲一個 ast 對象ast 對象 中包含一系列 節點- nodenode 節點類型 爲: ImportDeclarationVariableDeclarationSwitchStatementIfStatementWhileStatementForOfStatementExportDefaultDeclaration 等, 分別對應文件內容中的 import 聲明語句變量聲明語句switch語句if語句while語句for-ofexport 聲明語句, 如:

        import {func} from 'util.1.js'
        
        // 對應的 ast對象 節點
        {
            "type": "ImportDeclaration", // import 申明
            "specifiers": [
                {
                    "type": "ImportDefaultSpecifier",
                    "local": {
                        "type": "Identifier",
                        "name": "func"
                    }
                }
            ],
            "source": {
                "type": "Literal",
                "value": "util.1.js",
                "raw": "'util.1.js'"
            }
        }
        
        複製代碼

        webpack 會遍歷 ast 對象 中的 節點, 若是節點與文件的輸入語句相關, 那麼會建立一個 dep 對象dep 對象request 屬性 會從 ast 對象節點source.raw 獲取。

        若是 節點對應的語句中使用了全局變量, 也會建立一個 dep對象

        若是是 普通依賴dep 對象 會添加到 module 對象dependencies 列表中; 若是是 懶加載依賴dep 對象 會添加到 module 對象blocks 列表中; 若是是 全局變量dep 對象 會添加到 module 對象variables 列表中。

      5. 處理模塊的依賴

        遍歷模塊的 dependenciesblocksvariables 列表, 根據列表中 dep 對象重複步驟一到步驟五, 生成 依賴模塊, 並 處理依賴模塊的依賴, 直到模塊的 dependenciesblocksvariables 列表爲 爲止。

    這樣, 通過 resolvecreatebuild 階段, 一個模塊就 構建 完畢。

  • chunk

    概述

    在構建 模塊依賴圖 階段, 每一個源文件對應的 module 對象, 都會被 收集compilation 對象modules 列表 中。 等 模塊依賴圖構建完成 之後, webpack 會使用 compilation.modules 中的 module 對象 進行 代碼分離 來生成 chunks, 每一個 chunk 包含各自對應的 module 對象

    代碼分離webpack 中最引人注目的特性之一。此特性可以把代碼分離到不一樣的 chunk 中,而後能夠 按需加載並行加載 這些文件。代碼分離能夠用於獲取更小的 chunk,以及控制資源加載優先級,若是使用合理,會極大影響加載時間

    分類

    代碼分離 生成的 chunk, 根據 包含模塊的特性 以及 chunk 的用途,能夠分爲以下幾類(我的意見):

    • initial chunk

      入口文件 對應的 module 所在的 chunk 稱之爲 initial chunk

    • async chunk

      懶加載文件 對應的 module 所在的 chunk 稱之爲 async chunk

    • runtime chunk

      客戶端 負責 安裝chunk安裝module加載lazy chunkchunk

      runtime chunk 能夠經過 optimization.runtimeChunk: trueinitial chunk 中分離出來。

    • normal chunk

      經過 optimization.splitChunks 策略 分離生成的 chunk 均可以稱之爲 normal chunk

    不一樣的 chunk,在 客戶端加載順序不一樣runtime chunk 加載順序最前, 而後 initial chunk、normal chunk 次之, async chunk 最後normal chunk 加載順序不固定, 可能先於 initial chunk, 也可能後於 initial chunk, 得看具體狀況。

    split chunks

    compilation.modules 中收集的 全部 module 分離成 chunks, 要經歷三個階段: 構建 initial chunk分離 async chunk使用 optimization.splitChunks 策略進行優化造成 normal chunk

    1. 構建 initial chunk

      根據 入口文件(main.js) 對應的 模塊(main module), webpack 會建立一個 chunk 對象。 這個 chunk 對象 會用 入口文件的名稱 來命名,通常爲 'main', 所以 chunk 也稱之爲 main chunkinitial chunk 會將 main module 收集到 _modules 列表中。

      若是是 多頁面應用, 有 多個入口文件, 會生成 多個 initial chunk

    2. 以 main module 爲起點,遍歷 模塊依賴圖,分離 async chunk

      模塊依賴圖 中, 每個 module 依賴的 normal modules 都會收集到 dependencies 列表中, 依賴的 全局變量 會收集到 variables 列表 中, 依賴的 async modules 會收集到 blocks 列表 中。

      分離 的時候, 會 依次遍歷各個模塊dependenciesvariablesblocks列表。

      具體的 分離過程 以下:

      1. 遍歷 main moduledependenciesvariables 列表, 將 dependenciesvariables 中收集的 module對象 添加到 mian chunk_modules 列表中。

        繼續遍歷 dependenciesvariables 列表中的 module 對象dependenciesvariables 列表, 直到 module 對象dependenciesvariables 爲止。 將 遍歷過程當中遇到到全部 module 都添加到 initial chunk_modules 列表 中。

      2. 遍歷 main moduleblocks 列表, 爲 blocks 中收集的 module,建立一個 新的 chunk, 即 async chunk。 將 block 列表 中的 async module,分別添加到對應的 async chunk_modules 列表 中。

      3. async chunk 中, 以 async module起點,遍歷 async moduledependenciesvariables 列表, 將 dependenciesvariables 中收集的 modules 添加到 async chunk_modules 列表 中。

        繼續遍歷 dependenciesvariables 列表中的 module 對象dependenciesvariables 列表, 直到 module 對象dependenciesvariables 爲止。 將 遍歷過程當中遇到到全部module 都添加到 async chunk_modules 列表中。

      4. 遍歷 async moduleblocks 列表, 爲 blocks 中收集的 module,建立一個 新的 async chunk, 將 block 列表 中的 async module,分別添加到對應的 async chunk_modules 列表中。

      5. 重複 步驟3步驟4,直到 blocks 爲止。

      通過上述過程, 咱們能夠獲得一個 initial chunk 和 多個 async chunk

      若是一個 chunk A(initial chunk 或者 async chunk) 中可分離出一個 chunkB(async chunk), 那麼 chunkAchunkBparentchunkBchunkAchild。 一個 chunk 能夠有 多個 parent, 也能夠有 多個 child

      async chunk 分離之後, 會進行 優化。 若是 async chunk(child) 中收集的 moduleparent chunk 中已經存在, 那麼移除 async chunk(child) 中對應的 modules

    3. optimization.splitChunks 優化

      initial chunksasync chunks 生成之後, 咱們還須要對它們作一些 優化。 好比, 將第三方庫(如vue、react)對應的 module 分離多個 chunk 中公共 module 分離 等。

      經過 optimization.splitChunks.cacheGroups 配置項, 咱們能夠將上述 moduleinitial chunksasync chunks 分離出來。

      webpack4optimization.splitChunks.cacheGroups 提供了 默認值, 以下:

      {
          ...
          optimization: {
              splitChunks: {
                  cacheGroups: {
                      // 將被至少2個chunk共享的module分離成新的chunk
                      default: {
                          automaticNamePrefix: '',
                          reuseExistingChunk: true,
                          minChunks: 2,
                          priority: -20
                      },
                      // 將引用的第三方庫分離成新的chunk
                      vendors: {
                          automaticNamePrefix: 'vendors',
                          test: /[\\/]node_modules[\\/]/,
                          priority: -10
                      }
                  }
              }
          }
      }
      複製代碼

      經過上述配置, 咱們就能夠將 第三方庫多個chunk共享的module 分離成 新的 chunk。這些 新的 chunk,能夠稱爲 normal chunk。( optimization.splitChunks.cacheGroups 的用法詳見 官網 - splitChunksPlugin )。

      另外, optimization.splitChunks 還提供了一些 配置項,對 代碼分離 進行了 限制。具體的 限制 以下:

      • maxAsyncRequests

        按需加載 時的 最大容許並行請求數production 模式 下爲 默認值5developmen t模式 下爲 默認值Infinity(不作限制)

        若是不知足, 代碼分離失敗

      • maxInitialRequests

        入口點處最大並行請求數production模式 下爲 3development模式 下爲 Infinity(不作限制)

        若是不知足, 代碼分離失敗

      • minSize

        指定生成塊的最小大小, 以字節爲單位production 模式 下默認 30KB.

        若是不知足, 代碼分離失敗

      • maxSize

        webpack 會嘗試將大於 maxSize 的塊拆分紅 更小的部分

      • minChunks

        代碼分離前必須共享模塊的最小塊數

      • chunks

        代表能夠選擇哪些 chunks 中的 modules 進行分離。

      上述配置項的具體用法及說明詳見: 官網 - splitChunksPluginwebpack4 經常使用配置項使用整理

    綜上, 咱們即可以將 源文件 根據咱們的實際須要分離爲多個 chunks

  • 構建 bundle

    chunks 構建完成之後,接下來要作的是根據 chunk 中收集的 modules, 構建可輸出的 bundle 文件

    構建過程以下:

    1. 根據 chunk 的類型, 獲取對應的 template

      webpack 提供了兩種 templatemainTemplatechunkTemplate

      template 用於構建 chunk 對應的 輸出內容

      在前面 分離 chunk 的部分, 咱們瞭解到 initial chunk 中能夠分離出 runtime chunkruntime chunk 在應用中是 第一個加載 的,用於 安裝全部的 chunks 以及 chunk 中 modules

      initial chunk, 若是 沒有分離出 runtime chunk, 使用 mainTemplate 構建 輸出文件內容; 若是 分離了 runtime chunk, 使用 chunkTemplate 構建 輸出文件內容

      async chunknormal chunk 使用 chunkTemplate 構建 輸出文件內容

    2. 根據 output.filename 構建 bundle 的文件名

    3. 遍歷 chunk 收集的 modules, 構建每一個 module 對應的 輸出內容

      在這個階段, 會用到 構建 module 階段 生成的 生成器 - generator

      generator 會 從 module對象_source 屬性中獲取 經 loader 處理之後的源文件的內容字符串, 而後根據 module 對象dependenciesblocksvariables 中存儲的 dep 對象替換源文件內容字符串中的 引入、輸出語句、全局變量語句

      // 源文件 - example1.js, 是一個懶加載文件
      import {func3} from '../utils/util.3'
      
      export const example1 = () => {
          func3()
          alert('example1.1.9')
      }
      
      // 輸出文件
      {
      
      /***/ "./src/examples/example.1.js":
      /***/ (function(module, __webpack_exports__, __webpack_require__) {
      
              "use strict";
              __webpack_require__.r(__webpack_exports__);
              /* harmony export (binding) */
              __webpack_require__.d(__webpack_exports__, "example1", function() { return example1; });
              /* harmony import */ 
              var _utils_util_3__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/utils/util.3.js");
              
              var example1 = function example1() {
                  Object(_utils_util_3__WEBPACK_IMPORTED_MODULE_0__[/* func3 */ "a"])();
                  alert('example1.1.9');
              };
          
          /***/ })
      
      }
      複製代碼
    4. 利用步驟3返回的 模塊輸出內容,構建 chunk 對應 bundle

      步驟三中 example1 對應的 chunk 生成的 bundle 以下:

      (window["webpackJsonp"] = window["webpackJsonp"] || []).push([["example1"],{
      
      /***/ "./src/examples/example.1.js":
      /***/ (function(module, __webpack_exports__, __webpack_require__) {
      
              "use strict";
              __webpack_require__.r(__webpack_exports__);
              /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "example1", function() { return example1; });
              /* harmony import */ var _utils_util_3__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/utils/util.3.js");
              
              var example1 = function example1() {
                  Object(_utils_util_3__WEBPACK_IMPORTED_MODULE_0__[/* func3 */ "a"])();
                  alert('example1.1.9');
              };
      
      /***/ })
      
      }]);
      複製代碼

    經過上述過程, 一個 chunk 對應的 bundle 文件 即可生成, 最後利用 node提供的文件輸出功能 即可 將生成的bundle文件輸出到 output 配置項指定的位置

    另外, runtime chunk 對應的 bundle 文件 格式以下:

    // 用於安裝chunk和chunk中的module
    (function(modules) { // webpackBootstrap
        // install a JSONP callback for chunk loading
        // 安裝 chunk
        function webpackJsonpCallback(data) {
     		...
     	};
     	
     	...
     	
     	// 安裝chunk中的每個module, 獲取 module 的輸出
     	// The require function
     	function __webpack_require__(moduleId) {
     	    ...
     	}
     	
     	...
    })([]);
    複製代碼
  • hash

    webpack編譯打包 過程當中, 會根據 output 配置項 提供的 hashFunction(默認md4)hashDigest(默認hex)hashDigestLength(hash前綴長度, 默認20) 等生成 hashhash 有幾種類型, 以下:

    • module hash

      每個 module標識,根據 module 對應的 源文件的內容、文件名、路徑 等信息生成, 長度爲 32

      修改 源文件的內容文件名 以及 位置, 都會致使 module hash 值的變化。

    • module renderedHash

      根據 hashDigestLength, 截取 module hash前20位

    • chunk hash

      每個 chunk 的標識, 根據 chunk的namechunk 中收集的全部 module 對象的 module hash 等信息生成, 長度爲 32

      只要修改 chun某一個 module 對象對應的源文件的內容、文件名、位置, 都會致使 chunk hash 的 變化。

    • chunk renderedHash

      根據 hashDigestLength, 截取 module hash前20位

      output.filename = '[name].[chunkhash].js' 中使用的 chunkhash 就是 chunk renderedHash

    • chunk contentHash

      chunkcontentHash 是一個 對象, 裏面有 兩個 屬性:css/mini-extractjavascript, 屬性值是 hash 值

      chunk.contentHash["css/mini-extract"] 的值爲 chunk 中全部 css 內容 生成的 hash 值的 前20位

      chunk.contentHash.javascript 屬性值是 chunk 中全部 js 內容 生成的 hash 值的 前20位

      修改 chunk 中的 css 內容chunk.contentHash["css/mini-extract"] 的值 會變化chunk.contentHash.javascript 的值 不會變化

      同理, 修改 chunk中 js 內容chunk.contentHash.javascript 的值 會變化chunk.contentHash["css/mini-extract"] 的值 不會變化

    • compilation fullhash

      每個 compilation標識, 根據 compilation 生成的全部 chunk 對象的 chunk hash 信息生成, 長度爲 32

      修改任意一個源文件, 都會致使 compilation fullhash 變化

    • compilation hash

      根據 hashDigestLength, 截取 compilation fullhash前20位

      output.filename = '[name].[hash].js' 中使用的 hash 就是 compilation hash。 即 全部的 bundle 文件名中的 hash相同

未完待續...

loader 相關知識

  • loader的本質

    webpack 中的 loader 本質上是一個 函數 - function函數輸入值 能夠是 源文件中源代碼字符串內容, 也能夠是 格式化之後的文件請求路徑,還能夠是 上一個 loader 返回結果返回值處理之後生成的 js 代碼字符串

  • css-loader

    css-loader 用於處理 css 格式 的內容, 即 css-loader輸入值css內容字符串(如: ".class1 { height: 100px; }")。 這個 輸入值, 能夠 直接從 .css 文件 中直接獲取, 也能夠是 上一個 loader 提供(如 sass-loader 處理 .scss 文件,返回的 css 內容字符串 做爲 css-loader 的 輸入值)

    css-loader輸出值 是一段 js代碼字符串。執行這一段 js 代碼,會返回 css 模塊 對應的 module 對象module.exports 是一個 數組, 存儲着 css 模塊 及 子css模塊idcss內容字符串

    // 返回一個數組, 用於收集 css module對象
    exports = module.exports = require("../../node_modules/_css-loader@3.1.0@css-loader/dist/runtime/api.js")(false);
    // 收集css中引入的子css module對象
    exports.i(require("-!../../node_modules/_css-loader@3.1.0@css-loader/dist/cjs.js!./style.1.css"), "");
    // 收集 css module對象
    exports.push([module.id, ".egoo9WLFwSoCC0hGalRN3 {\r\n width: 100px;\r\n height: 100px;\r\n background-color: aqua;\r\n}", ""]);
    // 若是css-loader的modules屬性爲true, 會將類名、id名等轉化爲惟一名稱
    // locals 收集 css類名、id名 和 生成的惟一名稱
    exports.locals = {
        // class 爲類型名, 對應的值爲 css-loader根據class生成的惟一名稱
    	"class": "egoo9WLFwSoCC0hGalRN3"
    };
    複製代碼

    必須使用 css-loader 處理 css 內容字符串, 將 css內容 轉化爲 js代碼字符串, 不然 webpack 會報錯

    緣由: webpack 在構建 依賴關係圖 時, 經過 acornParser 解析模塊源代碼字符串,從中 收集依賴的子模塊acornParser 只能 處理js代碼, 若是 輸入值css代碼 或者 其餘,會 拋出異常

    使用 css-loader 的時候, 能夠經過傳入一個 配置項-options, 來控制 css-loader 的行爲, 具體使用詳見 官網

  • style-loader

    style-loader 會經過 動態添加 style標籤 到文檔 head 的方式, 使 css樣式 生效。

    style-loader輸入值 是一個 內聯 css-loader 的文件請求路徑輸出值 是一段 js 代碼字符串

    輸入值格式 以下:

    // 內聯 css-loader 的文件請求路徑
    !!../../node_modules/_css-loader@3.2.0@css-loader/dist/cjs.js!./style.css" 複製代碼

    輸出值(代碼字符串)格式 以下:

    // 獲取通過css-loader處理的css內容
    // content 是一個數組, 存儲着樣式內容
    "var content = require("!!../../node_modules/_css-loader@3.2.0@css-loader/dist/cjs.js!./style.css"); if(typeof content === 'string') content = [[module.id, content, '']]; ... // 獲取addStyles方法, 而後將content中的css樣式經過添加style的方式添加到head中 // 返回一個update方法, 用於熱更新 var update = require("!../../node_modules/_style-loader@0.23.1@style-loader/lib/addStyles.js")(content, options); if(content.locals) module.exports = content.locals; // 熱更新代碼 if(module.hot) { module.hot.accept("!!../../node_modules/_css-loader@3.2.0@css-loader/dist/cjs.js!./style.css", function() { var newContent = require("!!../../node_modules/_css-loader@3.2.0@css-loader/dist/cjs.js!./style.css"); ... // 使用update方法進行熱更新, 使用新的css內容更新style標籤中原來的樣式內容 update(newContent); }); }"
    複製代碼

    項目運行 時, 會 執行上述的代碼,將 css樣式 添加到 文檔 中,步驟以下:

    1. 執行 css-loader 返回的 js代碼, 獲得一個 數組對象 - contentcontent 中存儲着 css樣式內容字符串

    2. 獲取 style-loader 提供的 addStyle 方法, 將 content 中的 css樣式內容 添加到文檔的head 中。

      addStyle 方法執行過程當中, 會遍歷 content數組。 針對 content 中的 每個元素,都會經過 document.createElement('style') 方式建立一個 style 元素 添加到 head中, 而後利用 元素中css樣式內容字符串 經過 document.createTextNode(css) 方法建立一個 文本節點, 再將這個 文本節點 經過 style.appendChild(textNode) 的方式添加到 style 標籤 中。

    開發模式 下,style-loader 輸出的代碼 中會包含 熱更新 邏輯(hmr: true)。 若是咱們修改了 源文件中的樣式內容瀏覽器端 會獲取到 修改之後的css樣式內容, 而後經過 textNode = document.createTextNode(newCss)style.appendChild(textNode) 的方式 更新樣式

    使用 style-loader 的時候, 能夠經過傳入一個 配置項-options, 來控制 style-loader 的行爲, 具體使用詳見 官網

    style-loader 必須需配合 css-loader 使用,沒法單獨使用,不然會 報錯, 對應的 配置項 以下:

    rules: [{
        test: /.css$/,
        loaders: ['style-loader', 'css-loader']
    }]
    複製代碼

    在使用 webpack 處理 css內容 時, style-loader 先工做, css-loader 後工做。

  • sass-loader

    sass-loader 能夠將 sass/scss 樣式代碼 經過 編譯 生成 css 樣式代碼

    sass-loader輸入值sass/scss 樣式代碼字符串輸出值css樣式代碼字符串

    sass-loader輸出值 通常做爲 css-loader輸入值 使用。

    webpack打包過程 中, 若是未使用 sass-loader 處理, 最後 輸出的樣式內容sass/scss 格式瀏覽器沒法識別

    sass-loader 必須配合 css-loader 處理, 不然沒有意義, 對應的配置項以下:

    rules: [{
        test: /.scss$/,
        loaders: ['style-loader', 'css-loader', 'sass-loader']
    }]
    複製代碼

    在使用 webpack 在處理 scss/sass 內容 時, style-loader 先工做, sass-loader 後工做, 最後 css-loader 工做。

  • less-loader

    工做模式和sass-loader相同

未完待續...

plugin 相關知識

  • miniCssExtractPlugin

    miniCssExtractPlugin 能夠將項目中使用到的全部 css 內容 打包合併,生成一個新的 .css 文件, 具體使用詳見 官網

    使用 miniCssExtractPlugin 時, 須要構建一個 plugin 實例, 添加到 webpack 配置項plugins 列表 中。 構建 實例 的時候, 能夠傳入一個 配置項配置項 中的屬性爲: filenamechunkFilenamemoduleFilenameignoreOrder

    其中, filename 用於指定從 initial chunk 中分離出的 .css 文件 對應的 文件名, 默認值 爲 '[name].css'。 chunkFilename 用於指定從 非 initial chunk(async chunk、normal chunk等) 中分離出的 .css 文件 對應的 文件名默認值 爲 '[id].[contenthash].css'。moduleFilename用戶自定義函數, 用於 動態生成 filenameignoreOrder 指示 是否忽略引用的 .css 文件 之間的順序, 默認爲 false

    miniCssExtractPlugin 工做時,會將 chunk 中的 css modules 分離出來生成一個 .css 文件。 極端狀況下, 有多少個 chunk, 就會生成多少個 .css 文件。 此時, 咱們能夠先經過 optimization.splitChunkschunk 中的 css modules 分離出來生成一個 新的 chunk, 而後再根據這個 chunk 生成 .css 文件, 這樣最後只會生成 一個 .css 文件

    .css 文件 的命名規則通常爲 '[id].[contentHash].css'。 從 chunk 中分離 .css 文件 生成 文件名 時, id 對應 chunk.id, contentHash 對應 chunk.contentHash["css/mini-extract"]

    不要使用 [id].[chunkhash].css

    miniCssExtractPlugin 的工做過程主要是 兩個部分:

    1. 經過 miniCssExtractPlugin.loadercss 內容 所有轉化爲 css module 添加到 模塊依賴圖 中。

      在處理過程當中, 會構建一個 child compiler

    2. 利用 chunk 中的 css modules, 生成 bundle, 並輸出到 指定位置

未完待續...

optimization 相關知識

  • tree shaking

    概述

    tree shaking 能夠將 未使用的代碼(dead code)打包代碼刪除優化打包代碼的體積

    使用 tree shaking 前提 : 模塊的輸入和輸入,必須使用ES2015模塊語法,即 import / export

    緣由: tree shaking 是對代碼的靜態分析,在編譯階段就須要確認模塊的使用狀況ES2015模塊語法編譯時加載支持靜態分析,使得 編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量

    require 是運行時加載, 在編譯階段沒法知道模塊的內部使用狀況

    配置說明

    咱們能夠經過設定 optimization.sideEffects 的值來決定 是否開啓 tree shaking。若是 optimization.sideEffects 的值爲 ture啓用 tree shaking; 若是值爲 false不啓用 tree shaking

    production 模式下, optimization.sideEffects 的值爲 true,默認 開啓 tree shaking

    optimization.sideEffects 的值爲 true 時,webpack 會安裝 SideEffectsFlagPlugin 來幫助咱們實現 tree shaking

    每一個 模塊生成 之後,都會經過 SideEffectsFlagPlugin 添加一個 標誌屬性: sideEffectFree。 這個 標誌屬性 表明 tree shaking 是否能夠做用於模塊若是爲 true, 表明 當前模塊 但是使用 tree shaking,沒有反作用;若是爲 false,表明 當前模塊 不可使用 tree shaking

    項目根目錄 - package.json 中的 sideEffects 屬性的值 會影響 模塊 sideEffectFree 屬性的值:

    • 沒有 sideEffects 屬性(即值爲undefined) 或者 sideEffects 的值爲 true, 聲明使用 tree shaking 會有 反作用。此時 全部模塊 sideEffectFree 屬性值會置爲 false不可以使用 tree shaking

    • sideEffects 的值爲 false,聲明使用 tree shaking 不會有 反作用。此時 全部模塊 sideEffectFree 的屬性值爲 true,可使用 tree shaking沒有反作用

    • sideEffects 的值爲 字符串 或者 數組,聲明 知足條件的模塊使用 tree shaking 會有反作用。 若是 模塊的請求路徑匹配 sideEffects 的值,則 sideEffectFree 的屬性值爲 false不可以使用tree shaking; 若是 不匹配,則 sideEffectFree 的屬性值爲 true可使用 tree shaking

    另外,module 配置項rule.sideEffects 的值也會影響 模塊sideEffectFree 屬性的值:

    • 沒有sideEffects屬性模塊 sideEffectFree 的值受項目根目錄 - package.json 中的 sideEffects 屬性值 的影響;

    • sideEffects的值爲true對匹配規則的模塊使用tree shaking有反作用模塊 sideEffectFree 的值會置爲 false不可以使用tree shaking

    • sideEffects 的值爲 false對匹配規則的模塊使用 tree shaking 不會有反作用模塊 sideEffectFree 的屬性值置爲 true可使用 tree shaking

    module 配置項 中 rule.sideEffects 的 優先級 會高於 項目根目錄 - package.json 中的 sideEffects

    分類

    tree shaking 能夠分爲兩類: 總體 tree shaking局部 tree shaking

    總體 tree shaking,即 若是模塊只被引用但沒有使用,當前模塊會從打包代碼中刪除

    局部 tree shaking, 即 一個模塊有多個 export,未使用的 export 會從打包代碼中刪除

    模塊 sideEffectFree 屬性會在 打包階段 起做用。

    打包階段,會將 compilation 收集的modules 分離成 chunks每個chunk 都經過 _modules 屬性 來收集 屬於它的 module。若是 modulesideEffectFree 屬性值爲 true,且 module 的輸出徹底沒有被使用, 那麼該 module不會添加到 _modules 列表中,最後 輸出的打包文件中不會包含該module,這樣就達到了 tree shaking 的目的。

    實現 局部 tree shaking,還須要將配置項 optimization.usedExportsoptimization.minimize 的值設置爲 trueproduction 模式下, 這兩個配置項的值默認爲true。在 打包階段,會爲 每個module 對象 添加一個 usedExports 屬性, 該屬性是一個 數組數組元素是模塊被使用的 輸出(export) 的名稱。 若是 模塊的輸出未被使用, 則 usedExports 的值爲 nullwebpack最後的構建輸出文件代碼 時, 會根據 moduleusedExports 中的值, 肯定已使用的export, 如:

    // 最後的輸出代碼
    (function(module, __webpack_exports__, __webpack_require__) {
    // cube 有被使用, 則 module 的 usedExports 的值爲 ["cube"]
    // 肯定已使用的 export 爲 cube, square 未被使用
    /* unused harmony export square */
    /* harmony export (immutable) */ __webpack_exports__["a"] = cube;
    function square(x) {
      return x * x;
    }
    
    function cube(x) {
      return x * x * x;
    }
    複製代碼

    輸出文件代碼 生成之後, webpack 會根據 optimization.minimize: true 啓用 TerserPlugin(Uglify)輸出文件進行壓縮、混淆未使用的代碼(如square)會被刪除, 達到 tree shaking 的目的。

webpack 打包分析、優化

webpack 打包分析

分析 webpack打包速度、打包體積 經常使用手段:

  • 使用 webpack 內置的 stats

    顆粒度比較粗,只能知道 構建時間打包文件體積沒法知道哪一個階段耗時長、具體哪一個模塊體積大

  • 使用 speed-measure-webpack-plugin

    顆粒度比較細, 可用於分析 打包總耗時 以及 每一個插件loader耗時狀況

  • 使用 webpack-bundle-analyzer

    可視化 分析 各個bundle的體積各個bundle中包含的module及體積, 經過分析可進行 針對性的優化

webpack 打包優化

webpack 打包能夠從 兩個方面 進行 優化 - 打包速度打包文件體積

  • 打包速度優化

    可使用如下措施 優化打包速度:

    1. 使用 高版本webpacknode提高打包速度;

    2. 多進程/多實例構建

      使用 happypack 或者 thread-loader(不要濫用)。

      module: {
          rules: [{
              test: /\.js$/,
              loaders: [{
                  loader: 'thread-loader',
                  options: {
                      workers: 3
                  }
              }, 'babel-loader']
          }]
      }
      複製代碼
    3. 多進程並行壓縮代碼

      經過 optimization.minimizer 來配置, 使用 teser-webpack-plugin 進行 多線程並行壓縮代碼

      optimization: {
          minimizer: [new TerserWebpackPlugin({
              parallel: 4
          })],
          ...
      }
      複製代碼

      不要濫用,需根據實際狀況酌情使用,不然會起到反效果。

    4. 預編譯資源文件(第三方庫、業務庫等)

      使用 DllPlugin 提早將 第三方類庫業務庫 打包成 一個文件一 個mainfest.json 文件, 而後銅鼓 DllReferencePlugin 使用 預編譯生成的打包文件

    5. 充分利用 緩存 提高 二次構建速度

      使用緩存的一些思路:

      • babel-loader 開啓緩存

      • terser-webpack-plugin 開啓緩存

      • 使用 cache-loader

      開啓緩存後,上次編譯產生的文件 會保存在 node_modules 目錄 下的 .cache 文件夾中,供下次編譯使用。

    6. 縮小構建目標

      • 縮小 loader 的使用範圍 - module 配置項中使用 excludeinclude

      • 減小文件的 搜索範圍

        • 優化 resolve.modules, 只在根目錄下的 node_modules 下尋找 npm 包,減小模塊搜索層級;

        • 優化 resolve.mainFileds, 設置爲 main, 在npm包package.json 文件中 經過 main 字段 查找入口文件;

        • 合理使用 alias

  • 打包文件體積優化

    可使用如下措施 優化打包文件體積

    1. 使用 懶加載 以及 合理的代碼分離策略(optimization.splitChunks);

    2. tree shaking 刪除 無用 js、css 代碼

      • 開啓 tree shaking, 將 未使用的依賴模塊bundle 中刪除;

      • 設置 optimization.minimize: true, 將 模塊中未使用的js代碼 刪除;

      • 使用 purgecss-webpack-plugin, 將模塊中 未使用的css代碼 刪除。

        需配合 mini-css-extract-plugin 使用

    3. 經過 image-webpack-loader 壓縮圖片

    4. 使用 動態 polyfill 服務

      不使用 babel-polyfill,使用 polyfill-service。根據 user agent,返回 瀏覽器 對應的 polyfill

其餘

wepack 魔法註釋

經過 import() 的方式 動態導入模塊 時,咱們能夠添加以下注釋:

  • webpackChunkName

    指定 lazy chunkname

  • webpackPrefetch

    import(/* webpackChunkName: "example3", webpackPrefetch: true */ './examples/example.3')
    複製代碼

    預提取懶加載chunk,具體實現方式以下:

    <link rel="prefetch" as="script" href="example3.js">
    複製代碼

    瀏覽器一般會在 空閒狀態 取得這些資源,在取得資源以後擱在 HTTP緩存 以便於實現未來的請求。

  • webpackPreload

    import(/* webpackChunkName: "example3", webpackPreload: true */ './examples/example.3')
    複製代碼

    預加載懶加載chunk,具體實現方式以下:

    <link rel="preload" as="script" href="example3.js">
    複製代碼

    preload 的塊與 父塊 並行加載

  • webpackInclude

    正則表達式匹配到的模塊能夠打包

  • webpackExclude

    正則表達式匹配到的模塊不能夠打包

  • webpackMode

    可指定不一樣的模式 解析動態導入

    默認爲 lazy, 分離 lazy chunk; 值爲 eager, 不分離 chunk

相關文章
相關標籤/搜索