經過插件咱們能夠擴展webpack
,在合適的時機經過Webpack
提供的 API 改變輸出結果,使webpack
能夠執行更普遍的任務,擁有更強的構建能力。 本文將嘗試探索 webpack
插件的工做流程,進而去揭祕它的工做原理。同時須要你對webpack
底層和構建流程的一些東西有必定的瞭解。php
想要了解 webpack 的插件的機制,須要弄明白如下幾個知識點:css
webpack
構建流程
Tapable
是如何把各個插件串聯到一塊兒的
compiler
以及
compilation
對象的使用以及它們對應的事件鉤子。
plugins
是能夠用自身原型方法apply
來實例化的對象。apply
只在安裝插件被Webpack compiler
執行一次。apply
方法傳入一個webpck compiler
的引用,來訪問編譯器回調。html
class HelloPlugin{
// 在構造函數中獲取用戶給該插件傳入的配置 constructor(options){ } // Webpack 會調用 HelloPlugin 實例的 apply 方法給插件實例傳入 compiler 對象 apply(compiler) { // 在emit階段插入鉤子函數,用於特定時機處理額外的邏輯; compiler.hooks.emit.tap('HelloPlugin', (compilation) => { // 在功能流程完成後能夠調用 webpack 提供的回調函數; }); // 若是事件是異步的,會帶兩個參數,第二個參數爲回調函數,在插件處理完任務時須要調用回調函數通知webpack,纔會進入下一個處理流程。 compiler.plugin('emit',function(compilation, callback) { // 支持處理邏輯 // 處理完畢後執行 callback 以通知 Webpack // 若是不執行 callback,運行流程將會一直卡在這不往下執行 callback(); }); } } module.exports = HelloPlugin; 複製代碼
安裝插件時, 只須要將它的一個實例放到Webpack config plugins
數組裏面:node
const HelloPlugin = require('./hello-plugin.js');
var webpackConfig = { plugins: [ new HelloPlugin({options: true}) ] }; 複製代碼
先來分析一下webpack Plugin的工做原理webpack
new HelloPlugin(options)
初始化一個
HelloPlugin
得到其實例。
compiler
對象後調用
HelloPlugin.apply(compiler)
給插件實例傳入
compiler
對象。
compiler
對象後,就能夠經過
compiler.plugin(事件名稱, 回調函數)
監聽到 Webpack 廣播出來的事件。 而且能夠經過
compiler
對象去操做
Webpack
。
在編寫插件以前,還須要瞭解一下Webpack
的構建流程,以便在合適的時機插入合適的插件邏輯。git
Webpack的基本構建流程以下:github
webpack.config.js
文件,初始化本次構建的配置參數
Compiler
對象:執行配置文件中的插件實例化語句
new MyWebpackPlugin()
,爲
webpack
事件流掛上自定義
hooks
entryOption
階段:
webpack
開始讀取配置的
Entries
,遞歸遍歷全部的入口文件
run/watch
:若是運行在
watch
模式則執行
watch
方法,不然執行
run
方法
compilation
:建立
Compilation
對象回調
compilation
相關鉤子,依次進入每個入口文件(
entry
),使用loader對文件進行編譯。經過
compilation
我能夠能夠讀取到
module
的
resource
(資源路徑)、
loaders
(使用的loader)等信息。再將編譯好的文件內容使用
acorn
解析生成AST靜態語法樹。而後遞歸、重複的執行這個過程, 全部模塊和和依賴分析完成後,執行
compilation
的
seal
方法對每一個 chunk 進行整理、優化、封裝
__webpack_require__
來模擬模塊化操做.
emit
:全部文件的編譯及轉化都已經完成,包含了最終輸出的資源,咱們能夠在傳入事件回調的
compilation.assets
上拿到所需數據,其中包括即將輸出的資源、代碼塊Chunk等等信息。
// 修改或添加資源
compilation.assets['new-file.js'] = { source() { return 'var a=1'; }, size() { return this.source().length; } }; 複製代碼
afterEmit
:文件已經寫入磁盤完成
done
:完成編譯
奉上一張滴滴雲博客的WebPack
編譯流程圖,不喜歡看文字講解的能夠看流程圖理解記憶web
WebPack 編譯流程圖 原圖出自:blog.didiyun.com/index.php/2…chrome
看完以後,若是仍是看不懂或者對縷不清webpack構建流程的話,建議通讀一下全文,再回來看這段話,相信必定會對webpack構建流程有很更加深入的理解。npm
webpack
本質上是一種事件流的機制,它的工做流程就是將各個插件串聯起來,而實現這一切的核心就是Tapable。
Webpack
的 Tapable
事件流機制保證了插件的有序性,將各個插件串聯起來, Webpack 在運行過程當中會廣播事件,插件只須要監聽它所關心的事件,就能加入到這條webapck機制中,去改變webapck的運做,使得整個系統擴展性良好。
Tapable
也是一個小型的 library,是Webpack
的一個核心工具。相似於node
中的events
庫,核心原理就是一個訂閱發佈模式。做用是提供相似的插件接口。
webpack中最核心的負責編譯的Compiler
和負責建立bundles的Compilation
都是Tapable的實例,能夠直接在 Compiler
和 Compilation
對象上廣播和監聽事件,方法以下:
/** * 廣播事件 * event-name 爲事件名稱,注意不要和現有的事件重名 */ compiler.apply('event-name',params); compilation.apply('event-name',params); /** * 監聽事件 */ compiler.plugin('event-name',function(params){}); compilation.plugin('event-name', function(params){}); 複製代碼
Tapable
類暴露了tap
、tapAsync
和tapPromise
方法,能夠根據鉤子的同步/異步方式來選擇一個函數注入邏輯。
tap
同步鉤子
compiler.hooks.compile.tap('MyPlugin', params => {
console.log('以同步方式觸及 compile 鉤子。') }) 複製代碼
tapAsync
異步鉤子,經過callback
回調告訴Webpack
異步執行完畢 tapPromise
異步鉤子,返回一個Promise
告訴Webpack
異步執行完畢
compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {
console.log('以異步方式觸及 run 鉤子。') callback() }) compiler.hooks.run.tapPromise('MyPlugin', compiler => { return new Promise(resolve => setTimeout(resolve, 1000)).then(() => { console.log('以具備延遲的異步方式觸及 run 鉤子') }) }) 複製代碼
const {
SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable"); 複製代碼
class Hook{
constructor(args){ this.taps = [] this.interceptors = [] // 這個放在後面用 this._args = args } tap(name,fn){ this.taps.push({name,fn}) } } class SyncHook extends Hook{ call(name,fn){ try { this.taps.forEach(tap => tap.fn(name)) fn(null,name) } catch (error) { fn(error) } } } 複製代碼
tapable
是如何將webapck/webpack
插件關聯的?Compiler.js
const { AsyncSeriesHook ,SyncHook } = require("tapable");
//建立類 class Compiler { constructor() { this.hooks = { run: new AsyncSeriesHook(["compiler"]), //異步鉤子 compile: new SyncHook(["params"]),//同步鉤子 }; }, run(){ //執行異步鉤子 this.hooks.run.callAsync(this, err => { this.compile(onCompiled); }); }, compile(){ //執行同步鉤子 並傳參 this.hooks.compile.call(params); } } module.exports = Compiler 複製代碼
MyPlugin.js
const Compiler = require('./Compiler')
class MyPlugin{ apply(compiler){//接受 compiler參數 compiler.hooks.run.tap("MyPlugin", () => console.log('開始編譯...')); compiler.hooks.complier.tapAsync('MyPlugin', (name, age) => { setTimeout(() => { console.log('編譯中...') }, 1000) }); } } //這裏相似於webpack.config.js的plugins配置 //向 plugins 屬性傳入 new 實例 const myPlugin = new MyPlugin(); const options = { plugins: [myPlugin] } let compiler = new Compiler(options) compiler.run() 複製代碼
想要深刻了解tapable
的文章能夠看看這篇文章:
webpack4
核心模塊tapable
源碼解析: https://www.cnblogs.com/tugenhua0707/p/11317557.html
開發插件首先要知道compiler
和 compilation
對象是作什麼的
Compiler
對象包含了當前運行Webpack
的配置,包括entry、output、loaders
等配置,這個對象在啓動Webpack
時被實例化,並且是全局惟一的。Plugin
能夠經過該對象獲取到Webpack的配置信息進行處理。
若是看完這段話,你仍是沒理解compiler
是作啥的,不要怕,接着看。 運行npm run build
,把compiler
的所有信息輸出到控制檯上console.log(Compiler)
。
// 爲了能更直觀的讓你們看清楚compiler的結構,裏面的大量代碼使用省略號(...)代替。
Compiler { _pluginCompat: SyncBailHook { ... }, hooks: { shouldEmit: SyncBailHook { ... }, done: AsyncSeriesHook { ... }, additionalPass: AsyncSeriesHook { ... }, beforeRun: AsyncSeriesHook { ... }, run: AsyncSeriesHook { ... }, emit: AsyncSeriesHook { ... }, assetEmitted: AsyncSeriesHook { ... }, afterEmit: AsyncSeriesHook { ... }, thisCompilation: SyncHook { ... }, compilation: SyncHook { ... }, normalModuleFactory: SyncHook { ... }, contextModuleFactory: SyncHook { ... }, beforeCompile: AsyncSeriesHook { ... }, compile: SyncHook { ... }, make: AsyncParallelHook { ... }, afterCompile: AsyncSeriesHook { ... }, watchRun: AsyncSeriesHook { ... }, failed: SyncHook { ... }, invalid: SyncHook { ... }, watchClose: SyncHook { ... }, infrastructureLog: SyncBailHook { ... }, environment: SyncHook { ... }, afterEnvironment: SyncHook { ... }, afterPlugins: SyncHook { ... }, afterResolvers: SyncHook { ... }, entryOption: SyncBailHook { ... }, infrastructurelog: SyncBailHook { ... } }, ... outputPath: '',//輸出目錄 outputFileSystem: NodeOutputFileSystem { ... }, inputFileSystem: CachedInputFileSystem { ... }, ... options: { //Compiler對象包含了webpack的全部配置信息,entry、module、output、resolve等信息 entry: [ 'babel-polyfill', '/Users/frank/Desktop/fe/fe-blog/webpack-plugin/src/index.js' ], devServer: { port: 3000 }, output: { ... }, module: { ... }, plugins: [ MyWebpackPlugin {} ], mode: 'production', context: '/Users/frank/Desktop/fe/fe-blog/webpack-plugin', devtool: false, ... performance: { maxAssetSize: 250000, maxEntrypointSize: 250000, hints: 'warning' }, optimization: { ... }, resolve: { ... }, resolveLoader: { ... }, infrastructureLogging: { level: 'info', debug: false } }, context: '/Users/frank/Desktop/fe/fe-blog/webpack-plugin',//上下文,文件目錄 requestShortener: RequestShortener { ... }, ... watchFileSystem: NodeWatchFileSystem { //監聽文件變化列表信息 ... } } 複製代碼
源碼地址(948行):https://github.com/webpack/webpack/blob/master/lib/Compiler.js
const { SyncHook, SyncBailHook, AsyncSeriesHook } = require("tapable");
class Compiler { constructor() { // 1. 定義生命週期鉤子 this.hooks = Object.freeze({ // ...只列舉幾個經常使用的常見鉤子,更多hook就不列舉了,有興趣看源碼 done: new AsyncSeriesHook(["stats"]),//一次編譯完成後執行,回調參數:stats beforeRun: new AsyncSeriesHook(["compiler"]), run: new AsyncSeriesHook(["compiler"]),//在編譯器開始讀取記錄前執行 emit: new AsyncSeriesHook(["compilation"]),//在生成文件到output目錄以前執行,回調參數: compilation afterEmit: new AsyncSeriesHook(["compilation"]),//在生成文件到output目錄以後執行 compilation: new SyncHook(["compilation", "params"]),//在一次compilation建立後執行插件 beforeCompile: new AsyncSeriesHook(["params"]), compile: new SyncHook(["params"]),//在一個新的compilation建立以前執行 make:new AsyncParallelHook(["compilation"]),//完成一次編譯以前執行 afterCompile: new AsyncSeriesHook(["compilation"]), watchRun: new AsyncSeriesHook(["compiler"]), failed: new SyncHook(["error"]), watchClose: new SyncHook([]), afterPlugins: new SyncHook(["compiler"]), entryOption: new SyncBailHook(["context", "entry"]) }); // ...省略代碼 } newCompilation() { // 建立Compilation對象回調compilation相關鉤子 const compilation = new Compilation(this); //...一系列操做 this.hooks.compilation.call(compilation, params); //compilation對象建立完成 return compilation } watch() { //若是運行在watch模式則執行watch方法,不然執行run方法 if (this.running) { return handler(new ConcurrentCompilationError()); } this.running = true; this.watchMode = true; return new Watching(this, watchOptions, handler); } run(callback) { if (this.running) { return callback(new ConcurrentCompilationError()); } this.running = true; process.nextTick(() => { this.emitAssets(compilation, err => { if (err) { // 在編譯和輸出的流程中遇到異常時,會觸發 failed 事件 this.hooks.failed.call(err) }; if (compilation.hooks.needAdditionalPass.call()) { // ... // done:完成編譯 this.hooks.done.callAsync(stats, err => { // 建立compilation對象以前 this.compile(onCompiled); }); } this.emitRecords(err => { this.hooks.done.callAsync(stats, err => { }); }); }); }); this.hooks.beforeRun.callAsync(this, err => { this.hooks.run.callAsync(this, err => { this.readRecords(err => { this.compile(onCompiled); }); }); }); } compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { this.hooks.compile.call(params); const compilation = this.newCompilation(params); //觸發make事件並調用addEntry,找到入口js,進行下一步 this.hooks.make.callAsync(compilation, err => { process.nextTick(() => { compilation.finish(err => { // 封裝構建結果(seal),逐次對每一個module和chunk進行整理,每一個chunk對應一個入口文件 compilation.seal(err => { this.hooks.afterCompile.callAsync(compilation, err => { // 異步的事件須要在插件處理完任務時調用回調函數通知 Webpack 進入下一個流程, // 否則運行流程將會一直卡在這不往下執行 return callback(null, compilation); }); }); }); }); }); }); } emitAssets(compilation, callback) { const emitFiles = (err) => { //...省略一系列代碼 // afterEmit:文件已經寫入磁盤完成 this.hooks.afterEmit.callAsync(compilation, err => { if (err) return callback(err); return callback(); }); } // emit 事件發生時,能夠讀取到最終輸出的資源、代碼塊、模塊及其依賴,並進行修改(這是最後一次修改最終文件的機會) this.hooks.emit.callAsync(compilation, err => { if (err) return callback(err); outputPath = compilation.getPath(this.outputPath, {}); mkdirp(this.outputFileSystem, outputPath, emitFiles); }); } // ...省略代碼 } 複製代碼
apply
方法中插入鉤子的通常形式以下:
// compiler提供了compiler.hooks,能夠根據這些不一樣的時刻去讓插件作不一樣的事情。
compiler.hooks.階段.tap函數('插件名稱', (階段回調參數) => { }); compiler.run(callback) 複製代碼
Compilation
對象表明了一次資源版本構建。當運行 webpack
開發環境中間件時,每當檢測到一個文件變化,就會建立一個新的 compilation
,從而生成一組新的編譯資源。一個 Compilation
對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息,簡單來說就是把本次打包編譯的內容存到內存裏。Compilation
對象也提供了插件須要自定義功能的回調,以供插件作自定義處理時選擇使用拓展。
簡單來講,Compilation
的職責就是構建模塊和Chunk,並利用插件優化構建過程。
和 Compiler
用法相同,鉤子類型不一樣,也能夠在某些鉤子上訪問 tapAsync
和 tapPromise。
控制檯輸出console.log(compilation)
經過 Compilation
也能讀取到 Compiler
對象。
源碼2000多行,看不動了- -,有興趣的能夠本身看看。 https://github.com/webpack/webpack/blob/master/lib/Compilation.js
鉤子 | 類型 | 何時調用 |
---|---|---|
buildModule | SyncHook | 在模塊開始編譯以前觸發,能夠用於修改模塊 |
succeedModule | SyncHook | 當一個模塊被成功編譯,會執行這個鉤子 |
finishModules | AsyncSeriesHook | 當全部模塊都編譯成功後被調用 |
seal | SyncHook | 當一次compilation 中止接收新模塊時觸發 |
optimizeDependencies | SyncBailHook | 在依賴優化的開始執行 |
optimize | SyncHook | 在優化階段的開始執行 |
optimizeModules | SyncBailHook | 在模塊優化階段開始時執行,插件能夠在這個鉤子裏執行對模塊的優化,回調參數:modules |
optimizeChunks | SyncBailHook | 在代碼塊優化階段開始時執行,插件能夠在這個鉤子裏執行對代碼塊的優化,回調參數:chunks |
optimizeChunkAssets | AsyncSeriesHook | 優化任何代碼塊資源,這些資源存放在compilation.assets 上。一個 chunk 有一個 files 屬性,它指向由一個chunk建立的全部文件。任何額外的 chunk 資源都存放在 compilation.additionalChunkAssets 上。回調參數:chunks |
optimizeAssets | AsyncSeriesHook | 優化全部存放在 compilation.assets 的全部資源。回調參數:assets |
Compiler
表明了整個 Webpack
從啓動到關閉的生命週期,而 Compilation
只是表明了一次新的編譯,只要文件有改動,compilation
就會被從新建立。
插件能夠用來修改輸出文件、增長輸出文件、甚至能夠提高 Webpack
性能、等等,總之插件經過調用Webpack
提供的 API
能完成不少事情。 因爲 Webpack
提供的 API
很是多,有不少 API
不多用的上,又加上篇幅有限,下面來介紹一些經常使用的 API。
有些插件可能須要讀取 Webpack
的處理結果,例如輸出資源、代碼塊、模塊及其依賴,以便作下一步處理。 在 emit 事件發生時,表明源文件的轉換和組裝已經完成,在這裏能夠讀取到最終將輸出的資源、代碼塊、模塊及其依賴,而且能夠修改輸出資源的內容。 插件代碼以下:
class Plugin {
apply(compiler) { compiler.plugin('emit', function (compilation, callback) { // compilation.chunks 存放全部代碼塊,是一個數組 compilation.chunks.forEach(function (chunk) { // chunk 表明一個代碼塊 // 代碼塊由多個模塊組成,經過 chunk.forEachModule 能讀取組成代碼塊的每一個模塊 chunk.forEachModule(function (module) { // module 表明一個模塊 // module.fileDependencies 存放當前模塊的全部依賴的文件路徑,是一個數組 module.fileDependencies.forEach(function (filepath) { }); }); // Webpack 會根據 Chunk 去生成輸出的文件資源,每一個 Chunk 都對應一個及其以上的輸出文件 // 例如在 Chunk 中包含了 CSS 模塊而且使用了 ExtractTextPlugin 時, // 該 Chunk 就會生成 .js 和 .css 兩個文件 chunk.files.forEach(function (filename) { // compilation.assets 存放當前全部即將輸出的資源 // 調用一個輸出資源的 source() 方法能獲取到輸出資源的內容 let source = compilation.assets[filename].source(); }); }); // 這是一個異步事件,要記得調用 callback 通知 Webpack 本次事件監聽處理結束。 // 若是忘記了調用 callback,Webpack 將一直卡在這裏而不會日後執行。 callback(); }) } } 複製代碼
Webpack
會從配置的入口模塊出發,依次找出全部的依賴模塊,當入口模塊或者其依賴的模塊發生變化時, 就會觸發一次新的 Compilation
。
在開發插件時常常須要知道是哪一個文件發生變化致使了新的 Compilation
,爲此可使用以下代碼:
// 當依賴的文件發生變化時會觸發 watch-run 事件
compiler.hooks.watchRun.tap('MyPlugin', (watching, callback) => { // 獲取發生變化的文件列表 const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes; // changedFiles 格式爲鍵值對,鍵爲發生變化的文件路徑。 if (changedFiles[filePath] !== undefined) { // filePath 對應的文件發生了變化 } callback(); }); 複製代碼
默認狀況下 Webpack
只會監視入口和其依賴的模塊是否發生變化,在有些狀況下項目可能須要引入新的文件,例如引入一個 HTML
文件。 因爲 JavaScript
文件不會去導入 HTML
文件,Webpack
就不會監聽 HTML
文件的變化,編輯 HTML
文件時就不會從新觸發新的 Compilation
。 爲了監聽 HTML
文件的變化,咱們須要把 HTML
文件加入到依賴列表中,爲此可使用以下代碼:
compiler.hooks.afterCompile.tap('MyPlugin', (compilation, callback) => {
// 把 HTML 文件添加到文件依賴列表,好讓 Webpack 去監聽 HTML 模塊文件,在 HTML 模版文件發生變化時從新啓動一次編譯 compilation.fileDependencies.push(filePath); callback(); }); 複製代碼
有些場景下插件須要修改、增長、刪除輸出的資源,要作到這點須要監聽 emit
事件,由於發生 emit
事件時全部模塊的轉換和代碼塊對應的文件已經生成好, 須要輸出的資源即將輸出,所以emit事件是修改 Webpack 輸出資源的最後時機。
全部須要輸出的資源會存放在 compilation.assets
中,compilation.assets
是一個鍵值對,鍵爲須要輸出的文件名稱,值爲文件對應的內容。
設置 compilation.assets
的代碼以下:
// 設置名稱爲 fileName 的輸出資源
compilation.assets[fileName] = { // 返回文件內容 source: () => { // fileContent 既能夠是表明文本文件的字符串,也能夠是表明二進制文件的 Buffer return fileContent; }, // 返回文件大小 size: () => { return Buffer.byteLength(fileContent, 'utf8'); } }; callback(); 複製代碼
// 判斷當前配置使用使用了 ExtractTextPlugin,
// compiler 參數即爲 Webpack 在 apply(compiler) 中傳入的參數 function hasExtractTextPlugin(compiler) { // 當前配置全部使用的插件列表 const plugins = compiler.options.plugins; // 去 plugins 中尋找有沒有 ExtractTextPlugin 的實例 return plugins.find(plugin=>plugin.__proto__.constructor === ExtractTextPlugin) != null; } 複製代碼
以上4種方法來源於文章: [Webpack學習-Plugin] :http://wushaobin.top/2019/03/15/webpackPlugin/
作一個實驗,若是你在 apply
函數內插入 throw new Error("Message")
,會發生什麼,終端會打印出 Unhandled rejection Error: Message
。而後 webpack 中斷執行。 爲了避免影響 webpack
的執行,要在編譯期間向用戶發出警告或錯誤消息,則應使用 compilation.warnings 和 compilation.errors。
compilation.warnings.push("warning");
compilation.errors.push("error"); 複製代碼
node --inspect-brk ./node_modules/webpack/bin/webpack.js --inline --progress
複製代碼
其中參數--inspect-brk就是以調試模式啓動node:
終端會輸出:
Debugger listening on ws://127.0.0.1:9229/1018c03f-7473-4d60-b62c-949a6404c81d
For help, see: https://nodejs.org/en/docs/inspector
複製代碼