Webpack相關原理淺析

来源:https://www.cnblogs.com/LuckyWinty/archive/2019/06/20/11060213.html
-Advertisement-
Play Games

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


基本打包機制

本質上,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~ ^_^

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在前一個階段的工作中,項目組要開發一個平臺,為了做出更好的用戶體驗,實現快速、高質量的交互,從而更快得到用戶的反饋,要求在前端把數據存儲起來,之後我去研究了下現在比較流行的前端存儲資料庫,找到了indexedDB,於是便對indexedDB做了一個較為深入的探索,此文就是記錄探索過程的一些心得體會。 ...
  • 題目 編寫一個 SQL 查詢,獲取 表中第二高的薪水(Salary) 。 例如上述 表,SQL查詢應該返回 作為第二高的薪水。如果不存在第二高的薪水,那麼查詢應返回 。 解決方案 方法一 :使用 和 子句 將不同的薪資按降序排序,然後使用 子句獲得第二高的薪資。 然而,如果沒有這樣的第二最高工資,這 ...
  • html代碼: ...
  • 效果圖: 代碼: ...
  • 一、項目概況 基於react+react-dom+react-router-dom+redux+react-redux+webpack2.0+react-photoswipe+swiper等技術混合開發的手機端仿微信界面聊天室——reactChatRoom,實現了聊天記錄下拉刷新、發送消息、表情(動 ...
  • // ready 在DOM載入完成時運行的代碼 $(document).ready(function(){ // 在這裡寫代碼... }) // 可以簡寫為 $(function(){ // 在這裡繼續使用$作為別名... }) // load 頁面載入完畢後運行代碼 $(document).loa ...
  • 說到Tab切換,你可能首先想到的就是使用jQuery,短短幾行代碼就可以輕鬆搞定一個Tab切換。 而今天所要分享的,是使用 0 行JS代碼來實現Tab切換! 具體效果如下: Tab切換 方法一:模擬單選框原理 關於模擬單選框,在我之前文章中有講到,詳情請戳→純CSS模擬單選框和覆選框 該方法大致原理 ...
  • css等比例分割父級容器(完美三等分) 父級容器的寬度一定,要實現子元素等比例完美均分父級寬度,實現方式有哪些? html部分代碼: 方法一: 浮動佈局+百分比 (將子元素依次左浮動,根據子元素的個數,設定每個子元素的寬度百分比) 方法二:行內元素(inline-block)+百分比 方法三: 父元 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...