不可錯過的Webpack核心知識點

本文使用「署名 4.0 國際 (CC BY 4.0)」 許可協議,歡迎轉載、或從新修改使用,但須要註明來源。
css

做者: 百應前端團隊 @雙魚前端

https://juejin.im/user/307518985745895node

1. 核心概念

  • entry:入口。webpack是基於模塊的,使用webpack首先須要指定模塊解析入口(entry),webpack從入口開始根據模塊間依賴關係遞歸解析和處理全部資源文件。
  • output:輸出。源代碼通過webpack處理以後的最終產物。
  • loader:模塊轉換器。本質就是一個函數,在該函數中對接收到的內容進行轉換,返回轉換後的結果。由於 Webpack 只認識 JavaScript,因此 Loader 就成了翻譯官,對其餘類型的資源進行轉譯的預處理工做。
  • plugin:擴展插件。基於事件流框架  Tapable,插件能夠擴展 Webpack 的功能,在 Webpack 運行的生命週期中會廣播出許多事件,Plugin 能夠監聽這些事件,在合適的時機經過 Webpack 提供的 API 改變輸出結果。
  • module:模塊。除了js範疇內的es module、commonJs、AMD等,css @import、url(...)、圖片、字體等在webpack中都被視爲模塊。

另外webpack4開始 mode 變成一個重要概念,webpack爲不一樣 mode提供了一些默認值,附上阮一峯老師的吐槽react

不一樣mode的默認配置以下:jquery

2. 打包流程

  1. 初始化參數:從配置文件和 Shell 語句中讀取與合併參數,得出最終的參數;
  2. 初始化編譯:用上一步獲得的參數初始化 Compiler 對象,註冊插件並傳入 Compiler 實例(掛載了衆多webpack事件api供插件調用);
  3. AST & 依賴圖:從入口文件(entry)出發,調用AST引擎(acorn)生成抽象語法樹AST,根據AST構建模塊的全部依賴;
  4. 遞歸編譯模塊:調用全部配置的 Loader 對模塊進行編譯;
  5. 輸出資源:根據入口和模塊之間的依賴關係,組裝成一個個包含多個模塊的 Chunk,再把每一個 Chunk 轉換成一個單獨的文件加入到輸出列表,這步是能夠修改輸出內容的最後機會;
  6. 輸出完成:在肯定好輸出內容後,根據配置肯定輸出的路徑和文件名,把文件內容寫入到文件系統;

在以上過程當中,Webpack 會在特定的時間點廣播出特定的事件,插件在監聽到相關事件後會執行特定的邏輯,而且插件能夠調用 Webpack 提供的 API 改變 Webpack 的運行結果webpack

構建流程核心概念:
  • Tapable:一個基於發佈訂閱的事件流工具類,Compiler 和 Compilation 對象都繼承於 Tapable
  • Compiler:webpack編譯貫穿始終的核心對象,在編譯初始化階段被建立的全局單例,包含完整配置信息、loaders、plugins以及各類工具方法
  • Compilation:表明一次 webpack 構建和生成編譯資源的的過程,在watch模式下每一次文件變動觸發的從新編譯都會生成新的 Compilation 對象,包含了當前編譯的模塊 module, 編譯生成的資源,變化的文件, 依賴的狀態等

更加細化的構建流程圖:git

看大圖點這裏👈github

流程圖出處:淘系前端團隊-細說 webpack 之流程篇web

3. Loader

loader就像一個翻譯官,將源文件通過轉換後生成目標文件並交由下一流程處理正則表達式

使用方法
  • 每一個loader職責都是單一的,就像流水線上的工人
  • 順序很關鍵(從右往左)
實現準則
  • 簡單【Simple】loader只作單一任務,多個loader > 一個多功能loader
  • 鏈式【Chaining】遵循鏈式調用原則
  • 無狀態【Stateless】即函數式裏的Pure Function,無反作用
  • 使用工具庫【Loader Utilities】充分利用 loader-utils 包
實現一個簡單的loader,功能是替換console.log、去除換行符、在文件結尾處增長一行自定義內容
/** webpack.config.js */  
  
const path = require("path");  
  
module.exports = {  
  entry: {  
    index: path.resolve(__dirname, "src/index.js"),  
  },  
  output: {  
    path: path.resolve(__dirname, "dist"),  
  },  
  module: {  
    rules: [  
      {  
        test/\.js$/,  
        use: [  
          {  
            loader: path.resolve("lib/loader/loader1.js"),  
            options: {  
              message"this is a message",  
            }  
          }  
        ],  
      },  
    ],  
  },  
};  
/** lib/loader/loader1.js */  
  
const loaderUtils = require('loader-utils');  
  
/** 過濾console.log和換行符 */  
module.exports = function (source{  
  
  // 獲取loader配置項  
  const options = loaderUtils.getOptions(this);  
  
  console.log('loader配置項:', options);  
  
  const result = source  
    .replace(/console.log\(.*\);?/g"")  
    .replace(/\n/g"")  
    .concat(`console.log("${options.message || '沒有配置項'}");`);  
  
  return result;  
};  
  
異步loader如何編寫
/** lib/loader/loader1.js */  
  
/** 異步loader */  
module.exports = function (source{  
  
  let count = 1;  
  
  // 1.調用this.async() 告訴webpack這是一個異步loader,須要等待 asyncCallback 回調以後再進行下一個loader處理  
  // 2.this.async 返回異步回調,調用表示異步loader處理結束  
  const asyncCallback = this.async();  
  
  const timer = setInterval(() => {  
    console.log(`時間已通過去${count++}秒`);  
  }, 1000);  
  
  // 異步操做  
  setTimeout(() => {  
    clearInterval(timer);  
    asyncCallback(null, source);  
  }, 3200);  
  
};  
  
  

4. Plugin

在webpack編譯整個生命週期的特定節點執行特定功能

實現要點:
  • 一個命名JS函數或者JS類
  • 在prototype上定義一個apply方法(供webpack調用,而且在調用時注入 compiler 對象)
  • 在 apply 函數中須要有經過 compiler 對象掛載的 webpack 事件鉤子(鉤子函數中能拿到當前編譯的 compilation 對象)
  • 處理 webpack 內部實例的特定數據
  • 功能完成後調用 webpack 提供的回調
基本模型:
// 一、Plugin名稱  
const MY_PLUGIN_NAME = "MyBasicPlugin";  
  
class MyBasicPlugin {  
  // 二、在構造函數中獲取插件配置項  
  constructor(option) {  
    this.option = option;  
  }  
  
  // 三、在原型對象上定義一個apply函數供webpack調用  
  apply(compiler) {  
    // 四、註冊webpack事件監聽函數  
    compiler.hooks.emit.tapAsync(  
      MY_PLUGIN_NAME,  
      (compilation, asyncCallback) => {  
  
        // 五、操做Or改變compilation內部數據  
        console.log(compilation);        
  
        console.log("當前階段 ======> 編譯完成,即將輸出到output目錄");  
  
        // 六、若是是異步鉤子,結束後須要執行異步回調  
        asyncCallback();  
      }  
    );  
  }  
}  
  
// 七、模塊導出  
module.exports = MyBasicPlugin;  
實現一個plugin,功能是在dist目錄自動生成README文件:
const MY_PLUGIN_NAME = "MyReadMePlugin";  
  
// 插件功能:自動生成README文件,標題取自插件option  
class MyReadMePlugin {  
  
  constructor(option) {  
    this.option = option || {};  
  }  
  
  apply(compiler) {  
    compiler.hooks.emit.tapAsync(  
      MY_PLUGIN_NAME,  
      (compilation, asyncCallback) => {  
        compilation.assets["README.md"] = {  
          // 文件內容  
          source: () => {  
            return `# ${this.option.title || '默認標題'}`;  
          },  
          // 文件大小  
          size: () => 30,  
        };  
        asyncCallback();  
      }  
    );  
  }  
}  
  
// 七、模塊導出  
module.exports = MyReadMePlugin;  
  

compiler.hooks 上掛載了不一樣時期觸發的webpack事件函數(相似於React生命週期),能夠在編譯的各個階段執行其餘邏輯或者改變輸出結果,具體支持的事件列表能夠看這裏:compiler-hooks

Tapable:

webpack 的插件架構主要基於 Tapable 實現的,Tapable 是 webpack 項目組的一個內部庫,主要是抽象了一套插件機制。它相似於 NodeJS 的 EventEmitter 類,專一於自定義事件的觸發和操做。

Tapable事件類型分爲同步和異步,內部又以不一樣的規則分爲不一樣類型,上述事件的具體區別能夠看 這篇文章,理解這些事件的區別和應用場景有助於咱們理解webpack源碼和編寫Plugin

Complier對象:

在webpack啓動時被初始化一次,全局惟一,能夠理解爲webpack編譯實例,它包含了webpack原始配置、Loader、Plugin引用、各類鉤子

部分源碼:https://github.com/webpack/webpack/blob/10282ea20648b465caec6448849f24fc34e1ba3e/lib/webpack.js

5. 性能優化

1. 從何開始?
  • 使用 speed-measure-webpack-plugin 測量打包速度

  • 使用 webpack-bundle-analyzer 進行體積分析

    從某項目的分析圖能夠看出一個很明顯的優化空間就是 BizCharts 沒有按需引入,這時候咱們能夠import路徑再執行一次打包分析看效果。

    另外圖中每一個模塊都有三種Size,分別是 Stat Size、Parsed Size、Gzipped Size,這三者的分別表明什麼含義能夠看下插件的github issue

2. 優化Loader配置

思路主要是優化搜索時間、縮小文件搜索範圍、減小沒必要要的編譯工做,具體作法能夠看如下配置文件

module .exports = {   
  module : {   
    rules : [{  
      // 若是項目源碼中只有 文件,就不要寫成/\jsx?$/,以提高正則表達式的性能  
      test: /\.js$/,   
      // babel-loader 支持緩存轉換出的結果,經過 cacheDirectory 選項開啓  
      use: ['babel-loader?cacheDirectory'] ,   
      // 只對項目根目錄下 src 目錄中的文件採用 babel-loader  
      include: path.resolve(__dirname,'src'),  
      // 使用resolve.alias把原導入路徑映射成一個新的導入路徑,減小耗時的遞歸解析操做  
      alias: {  
        'react': path.resolve( __dirname ,'./node_modules/react/dist/react.min.js'),  
      },  
      // 讓 Webpack 忽略對部分沒采用模塊化的文件的遞歸解析處理  
      noParse: '/jquery|lodash/',  
    }],  
  }  
}  
3. DLL Plugin Or Externals

合理使用DLLPlugin將更改頻率較低的代碼(三方庫)移到單獨的編譯中,我理解大部分場景下和配置 externals 做用是差很少的(都不用打包三方庫),可是 externals 在某些場景下會存在失效問題,具體能夠看 這篇文章,另外 DLLPlugin 具體使用 參考這裏

4. 多進程系列

多進程陣營裏有幾位知名選手:

  • thread-loader(v4之後的官方推薦)
  • happypack(不怎麼維護了)
  • parallel-webpack(不怎麼維護了)

這裏只介紹一下 thread-loader ,使用 thread-loader 將開銷較大的 loader(例如babel-loader)放到獨立進程中(官方描述 worker pool)處理,使用上有如下注意事項

  • 將其放在須要單獨加載的loader的前面,順序很關鍵
module.exports = {  
  module: {  
    rules: [  
      {  
        test/\.js$/,  
        include: path.resolve("src"),  
        use: [  
          "thread-loader",  
          // your expensive loader (e.g babel-loader)  
        ]  
      }  
    ]  
  }  
}  
  • worker pool中的loader使用上是有限制的,例如沒法使用自定義 loader api,沒法獲取webpack 配置項
5. 合理利用緩存 縮短非首次構建時間

目前項目在用的插件是 hard-source-webpack-plugin,效果較爲顯著,不過缺點有3

  1. 生成的緩存文件較大,比較佔用磁盤空間(以前還出現過發佈的時候誤把緩存文件上傳到服務器致使發佈特別慢的狀況 =。=,因此最好仍是指定緩存文件路徑爲 node_modules 內部)
  2. 這個倉庫也好久沒更新了
  3. 現有項目偶爾會出現更改代碼不觸發從新編譯的狀況,猜想可能與此插件有關

另外 webpack5 是否有自帶的緩存策略或者官方維護的緩存插件還須要去了解一下

6. 代碼壓縮 減小產物體積
  • webpack3配置optimization.minimize = true會默認啓用 UglifyJsPlugin,其多進程版本爲 ParallelUglifyPlugin
  • webpack4 中 webpack.optimize.UglifyJsPlugin 已被廢棄,默認內置使用 terser-webpack-plugin 插件壓縮優化代碼,原生支持多進程(這裏想起官方文檔 Build Performance 章節中列舉的優化措施第一點:Stay Up to Date,最香的仍是最新的webpack版本)
7. Code Splitting

官方文檔描述的code splitting的3種姿式:

  1. 多entry配置(多entry是自然的code splitting,可是基本沒人會由於性能優化的點去把一個單頁應用改爲多entry)

  2. 使用 SplitChunksPlugin 進行重複數據刪除和提取

  3. 使用 Dynamic Import 指定模塊拆分,而且能夠結合 preload、prefetch作更多用戶體驗上的優化

6. 想的更遠:那些值得深究的問題🤔

  • HMR的原理?
  • Tree shaking原理,爲何須要es module的寫法?
  • webpack5的Module Federation有哪些優點,在與http2.0的結合上有哪些有趣的事情,在微前端上的應用?
  • 爲何說rollup比webpack更適合打包組件庫?

點個『在看』支持下 

本文分享自微信公衆號 - 前端技術江湖(bigerfe)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索