基本打包機制 本質上,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~ ^_^