Webpack相關原理淺析

小蚊發表於2019-06-20

基本打包機制

本質上,webpack 是一個現代 JavaScript 應用程式的靜態模組打包器(module bundler)。當 webpack 處理應用程式時,它會遞迴地構建一個依賴關係圖(dependency graph),其中包含應用程式需要的每個模組,然後將所有這些模組打包成一個或多個 bundle。

打包過程可以拆分為四步:

1、利用babel完成程式碼轉換,並生成單個檔案的依賴

2、從入口開始遞迴分析,並生成依賴圖譜

3、將各個引用模組打包為一個立即執行函式

4、將最終的bundle檔案寫入bundle.js中

 

小解讀:

1.1 利用@babel/parser解析程式碼,識別module

1.2 利用@babel/traverse遍歷AST,獲取通過import引入的模組並儲存所依賴的模組

1.3 通過@babel/core和@babel/preset-env進行程式碼的轉換,就是轉化ES6/7/8程式碼等

1.4 輸出單個檔案的依賴

return{
        filename,//該檔名
        dependencies,//該檔案所依賴的模組集合(鍵值對儲存)
        code//轉換後的程式碼
    }

2.1 從入口開始,廣度遍歷所有依賴,並輸出整個專案的依賴圖譜

graphArray.forEach(item => {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
    })
    return graph

3.1 生成程式碼字串

4.1 寫入檔案

 

完整程式碼見:https://github.com/LuckyWinty/blog/tree/master/code/bundleBuild

 

以上是打包的基本機制,而webpack的打包過程,會基於這些基本步驟進行擴充套件,主要有以下步驟:

1. 初始化引數 從配置檔案和 Shell 語句中讀取與合併引數,得出最終的引數

2. 開始編譯 用上一步得到的引數初始Compiler物件,載入所有配置的外掛,通 過執行物件的run方法開始執行編譯

3. 確定入口 根據配置中的 Entry 找出所有入口檔案

4. 編譯模組 從入口檔案出發,呼叫所有配置的 Loader 對模組進行編譯,再找出該模組依賴的模組,再遞迴本步驟直到所有入口依賴的檔案都經過了本步驟的處理

5. 完成模組編譯 在經過第4步使用 Loader 翻譯完所有模組後, 得到了每個模組被編譯後的最終內容及它們之間的依賴關係

6. 輸出資源:根據入口和模組之間的依賴關係,組裝成一個個包含多個模組的 Chunk,再將每個 Chunk 轉換成一個單獨的檔案加入輸出列表中,這是可以修改輸出內容的最後機會

7. 輸出完成:在確定好輸出內容後,根據配置確定輸出的路徑和檔名,將檔案的內容寫入檔案系統中。

整個流程概括為3個階段,初始化、編譯、輸出。而在每個階段中又會發生很多事件,Webpack會將這些事件廣播出來供Plugin使用。具體鉤子,可以看官方文件:https://webpack.js.org/api/loaders/

Webpack Loader

Loader 就像一個翻譯員,能將原始檔經過轉化後輸出新的結果,並且一個檔案還可以鏈式地經過多個翻譯員翻譯。

概念:

  • 一個Loader 的職責是單一的,只需要完成一種轉換
  • 一個Loader 其實就是一個Node.js 模組,這個模組需要匯出一個函式

開發Loader形式

1.基本形式

module.exports = function (source ) { 
      return source; 
}

 

2.呼叫第三方模組

const sass= require('node-sass'); 
module.exports = function (source) { 
  return sass(source);
}

由於 Loader 執行在 Node.js 中,所以我們可以呼叫任意 Node.js 自帶的 API ,或者安裝第三方模組進行呼叫

 

3、呼叫Webpack的Api

//獲取使用者為 Loader 傳入的 options
const loaderUtils =require ('loader-utils'); 
module.exports = (source) => {
    const options= loaderUtils.getOptions(this); 
    return source; 
}
//返回sourceMap
module.exports = (source)=> { 
    this.callback(null, source, sourceMaps); 
    //當我們使用 this.callback 返回內容時 ,該 Loader 必須返回 undefined,
    //以讓 Webpack 知道該 Loader 返回的結果在 this.callback 中,而不是 return中
    return; 
}
// 非同步
module.exports = (source) => {
    const callback = this.async()
    someAsyncOperation(source, (err, result, sourceMaps, ast) => {
        // 通過 callback 返回非同步執行後的結果
        callback(err, result, sourceMaps, ast)
    })
}
//快取加速
module.exports = (source) => { 
    //關閉該 Loader 的快取功能
    this.cacheable(false)
    return source 
}

 

source引數是compiler 傳遞給 Loader 的一個檔案的原內容,這個函式需要返回處理後的內容,這裡為了簡單起見,直接將原內容返回了,相當於該Loader 有做任何轉換.這裡結合了webpack的api和第三方模組之後,可以說loader可以做的事情真的非常非常多了...

更多的webpack Api可以看官方文件:https://webpack.js.org/api/loaders

Webpack Plugin

專注處理 webpack 在編譯過程中的某個特定的任務的功能模組,可以稱為外掛

 

概念:

  • 是一個獨立的模組
  • 模組對外暴露一個 js 函式
  • 函式的原型 (prototype) 上定義了一個注入 compiler 物件的 apply 方法 apply 函式中需要有通過 compiler 物件掛載的 webpack 事件鉤子,鉤子的回撥中能拿到當前編譯的 compilation 物件,如果是非同步編譯外掛的話可以拿到回撥 callback
  • 完成自定義子編譯流程並處理 complition 物件的內部資料
  • 如果非同步編譯外掛的話,資料處理完成後執行 callback 回撥。

 

開發基本形式

    // 1、BasicPlugin.js 檔案(獨立模組)
    // 2、模組對外暴露的 js 函式
    class BasicPlugin{ 
        //在建構函式中獲取使用者為該外掛傳入的配置
        constructor(pluginOptions) {
            this.options = pluginOptions;
        } 
        //3、原型定義一個 apply 函式,並注入了 compiler 物件
        apply(compiler) { 
            //4、掛載 webpack 事件鉤子(這裡掛載的是 emit 事件)
            compiler.plugin('emit', function (compilation, callback) {
                // ... 內部進行自定義的編譯操作
                // 5、操作 compilation 物件的內部資料
                console.log(compilation);
                // 6、執行 callback 回撥
                callback();
            });
        }
    } 
    // 7、暴露 js 函式
    module.exports = BasicPlugin;

 

Webpack 啟動後,在讀取配置的過程中會先執行 new BasicPlugin(options )初始化一個 BasicPlugin 並獲得其例項。在初始化 Compiler 物件後,再呼叫 basicPlugin.apply (compiler )為外掛例項傳入 compiler 物件。外掛例項在獲取到 compiler 物件後,就可以通過 compiler. plugin (事件名稱 ,回撥函式)監聽到 Webpack 廣播的事件,並且可以通過 compiler 物件去操作 Webpack。

Compiler物件

 

compiler 物件是 webpack 的編譯器物件,compiler 物件會在啟動 webpack 的時候被一次性地初始化,compiler 物件中包含了所有 webpack 可自定義操作的配置,例如 loader 的配置,plugin 的配置,entry 的配置等各種原始 webpack 配置等

webpack部分原始碼:https://github.com/webpack/webpack/blob/10282ea20648b465caec6448849f24fc34e1ba3e/lib/webpack.js#L30

 

Compilation 物件

 compilation 例項繼承於 compiler,compilation 物件代表了一次單一的版本 webpack 構建和生成編譯資源的過程。當執行 webpack 開發環境中介軟體時,每當檢測到一個檔案變化,一次新的編譯將被建立,從而生成一組新的編譯資源以及新的 compilation 物件。一個 compilation 物件包含了 當前的模組資源、編譯生成資源、變化的檔案、以及 被跟蹤依賴的狀態資訊。編譯物件也提供了很多關鍵點回撥供外掛做自定義處理時選擇使用。

Compiler 和 Compilation 的區別在於: Compiler 代表了整個 Webpack 從啟動到關閉的生命週期,而 Compilation 只代表一次新的編譯。

 

Tapable & Tapable 例項

webpack 的外掛架構主要基於 Tapable 實現的,Tapable 是 webpack 專案組的一個內部庫,主要是抽象了一套外掛機制。它類似於 NodeJS 的 EventEmitter 類,專注於自定義事件的觸發和操作。 除此之外, Tapable 允許你通過回撥函式的引數訪問事件的生產者。

 

webpack本質上是一種事件流的機制,它的工作流程就是將各個外掛串聯起來,而實現這一切的核心就是Tapable,webpack中最核心的負責編譯的Compiler和負責建立bundles的Compilation都是Tapable的例項,Tapable 能夠讓我們為 javaScript 模組新增並應用外掛。 它可以被其它模組繼承或混合。

 

一些鉤子的含義:

  • SyncBailHook:只要監聽函式中有一個函式的返回值不為 null,則跳過剩下所有的邏輯。
  • SyncWaterfallHook:上一個監聽函式的返回值可以傳給下一個監聽函式。
  • SyncLoopHook:當監聽函式被觸發的時候,如果該監聽函式返回true時則這個監聽函式會反覆執行,如果返回 undefined 則表示退出迴圈。
  • AsyncParallelHook:非同步併發,不關心監聽函式的返回值
  • AsyncParallelBailHook:只要監聽函式的返回值不為 null,就會忽略後面的監聽函式執行,直接跳躍到callAsync等觸發函式繫結的回撥函式,然後執行這個被繫結的回撥函式
  • AsyncSeriesHook:非同步序列,不關心callback()的引數
  • AsyncSeriesBailHook:callback()的引數不為null,就會直接執行callAsync等觸發函式繫結的回撥函式
  • AsyncSeriesWaterfallHook:上一個監聽函式的中的callback(err, data)的第二個引數,可以作為下一個監聽函式的引數

同步鉤子,用tap方式註冊。非同步鉤子,有三種註冊/釋出的模式,tap、tapAsync、tapPromise。

 

Tapable 簡化後的模型,就是我們熟悉的釋出訂閱者模式

class SyncHook{
   constructor(){
      this.hooks = {}
   }
   
   tap(name,fn){
    if(!this.hooks[name])this.hooks[name] = []
     this.hooks[name].push(fn) 
   }      

   call(name){
     this.hooks[name].forEach(hook=>hook(...arguments))
   }
}

Loader & Plugin 開發除錯

npm link

1. 確保正在開發的本地 Loader 模組的 package.json 已經配置好(最主要的main欄位的入口檔案指向要正確)

2. 在本地的 Npm 模組根目錄下執行 npm link,將本地模組註冊到全域性

3. 在專案根目錄下執行 npm link loader-name ,將第 2 步註冊到全域性的本地 Npm 模組連結到專案的 node moduels 下,其中的 loader-name 是指在第 1 步的package.json 檔案中配置的模組名稱

 

Npm link 專門用於開發和除錯本地的 Npm 模組,能做到在不釋出模組的情況下, 將本地的一個正在開發的模組的原始碼連結到專案的 node_modules 目錄下,讓專案可以直接使 用本地的 Npm 模組。由於是通過軟連結的方式實現的,編輯了本地的 Npm 模組的程式碼,所以在專案中也能使用到編輯後的程式碼。

 

Resolveloader

ResolveLoader 用於配置 Webpack 如何尋找 Loader ,它在預設情況下只會去 node_modules 目錄下尋找。為了讓 Webpack 載入放在本地專案中的 Loader,需要修改 resolveLoader.modules。

構建工具選擇

針對不同的場景,選擇最合適的工具

通過對比,不難看出,Webpack和Rollup在不同場景下,都能發揮自身優勢作用。webpack作為打包工具,但是在定義模組輸出的時候,webpack確不支援ESM,webpack外掛系統龐大,確實有支援模組級的Tree-Shacking的外掛,如webpack-deep-scope-analysis-plugin。但是粒度更細化的,一個模組裡面的某個方法,本來如果沒有被引用的話也可以去掉的,就不行了....這個時候,就要上rollup了。rollup它支援程式流分析,能更加正確的判斷專案本身的程式碼是否有副作用,其實就是rollup的tree-shaking更乾淨。所以我們的結論是rollup 比較適合打包 js 的 sdk 或者封裝的框架等,例如,vue 原始碼就是 rollup 打包的。而 webpack 比較適合打包一些應用,例如 SPA 或者同構專案等等。

結論:在開發應用時使用 Webpack,開發庫時使用 Rollup

資料推薦

補充學習資料:https://github.com/LuckyWinty/blog/issues/1

更多學習資料推薦:

Loader: https://juejin.im/post/5a698a316fb9a01c9f5b9ca0

Tapable: https://juejin.im/post/5abf33f16fb9a028e46ec352

webpack:

  • ebook:webpack深入淺出
  • 極客時間:玩轉webpack

 

 

更多:

想來深圳Shopee(外企,不加班,福利好,假期多)發展的。歡迎找我內推,前端、後臺、測試、產品等各種崗~^_^

其他:如果方便的話,可以關注一下我的github,並給我剛開始的部落格專案點個start~ ^_^

 

相關文章