作為先進最為流行的前端構(gòu)建工具之一,webpack成為了前端開發(fā)必須掌握的技能秆吵。其諸多的插件為我們的工作帶來了大大的便利屁桑,本文將對webpack plugin的基本原理以及編寫方式做一個介紹。編寫插件需要對webpack的底層特性有一定的了解索抓,本文中也會對這些內(nèi)容做一些基本介紹碳蛋。
后文的介紹和樣例代碼編寫所對應(yīng)的webpack版本號為4.35.0
創(chuàng)建一個最基礎(chǔ)的Plugin
首先我們不來扯別的原理胚泌,先來看看一個最為基本的webpack plugin結(jié)構(gòu)。
// 聲明一個js函數(shù)
function ExamplePlugin(option) {
this.option = option
}
// 在函數(shù)的原型上聲明一個apply方法
ExamplePlugin.prototype.apply = function(compiler) {}
你也可以采用ES6來進行編寫
// 采用ES6
class ExamplePlugin {
constructor(option) {
this.option = option
}
apply(compiler) {}
}
以上就是一個最為基本的plugin結(jié)構(gòu)肃弟。webpack plugin最為核心的便是這個apply方法玷室。
webpack執(zhí)行時,先生成了插件的實例對象笤受,之后會調(diào)用插件上的apply方法穷缤,并將compiler對象(webpack實例對象,包含了webpack的各種配置信息...)作為參數(shù)傳遞給apply箩兽。
之后我們便可以在apply方法中使用compiler對象去監(jiān)聽webpack在不同時刻觸發(fā)的各種事件來進行我們想要的操作了津肛。
接下來看一個簡單的示例
class plugin1 {
constructor(option) {
this.option = option
console.log(option.name + '初始化')
}
apply(compiler) {
console.log(this.option.name + ' apply被調(diào)用')
//在webpack的emit生命周期上添加一個方法
compiler.hooks.emit.tap('plugin1', (compilation) => {
console.log('生成資源到 output 目錄之前執(zhí)行的生命周期')
})
}
}
class plugin2 {
constructor(option) {
this.option = option
console.log(option.name + '初始化')
}
apply(compiler) {
console.log(this.option.name + ' apply被調(diào)用')
//在webpack的afterPlugins生命周期上添加一個方法
compiler.hooks.afterPlugins.tap('plugin2', (compilation) => {
console.log('webpack設(shè)置完初始插件之后執(zhí)行的生命周期')
})
}
}
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.js'
},
plugins: [
new plugin1({ name: 'plugin1' }),
new plugin2({ name: 'plugin2' })
]
}
//執(zhí)行webpack命令后輸出結(jié)果如下:
/*
plugin1初始化
plugin2初始化
plugin1 apply被調(diào)用
plugin2 apply被調(diào)用
webpack設(shè)置完初始插件之后執(zhí)行的生命周期
生成資源到 output 目錄之前執(zhí)行的生命周期
*/
首先webpack會按順序?qū)嵗痯lugin對象,之后再依次調(diào)用plugin對象上的apply方法汗贫。
也就是對應(yīng)輸出 plugin1初始化身坐、plugin2初始化、 plugin1 apply被調(diào)用芳绩、plugin2 apply被調(diào)用掀亥。
在webpack
源代碼中我們也可以看到這么一行撞反,options.plugins便是配置文件中的被實例化的plugin數(shù)組妥色。
之前我們也提到了遏片,webpack在運行過程中會觸發(fā)各種事件嘹害,而在apply方法中我們能接收一個compiler對象撮竿,我們可以通過這個對象監(jiān)聽到webpack觸發(fā)各種事件的時刻,然后執(zhí)行對應(yīng)的操作函數(shù)笔呀。這套機制類似于Node.js的EventEmitter
幢踏,總的來說就是一個發(fā)布訂閱模式。
compiler.hooks中定義了各式各樣的事件鉤子许师,這些鉤子會在不同的時機被執(zhí)行房蝉。而上文中的compiler.hooks.emit
和compiler.hooks.afterPlugin
這兩個生命周期鉤子,分別對應(yīng)了設(shè)置完初始插件以及生成資源到 output 目錄之前這兩個時間節(jié)點微渠,afterPlugin
是在emit
之前被觸發(fā)的搭幻,所以輸出順序更靠前。
compiler對象上具體的鉤子也可以查看官方文檔 compiler鉤子逞盆。
在繼續(xù)記下來的內(nèi)容之前檀蹋,我們先來對compiler
和compilation
做一個更為詳細的介紹。
compiler和compilation介紹
webpack的compiler模塊是其核心部分云芦。其包含了webpack配置文件傳遞的所有選項俯逾,包含了諸如loader、plugins等信息舅逸。
我們可以看看Compiler
類中定義的一些核心方法桌肴。
//繼承自Tapable類,使得自身擁有發(fā)布訂閱的能力
class Compiler extends Tapable {
//構(gòu)造函數(shù)堡赔,context實際傳入值為process.cwd()识脆,代表當前的工作目錄
constructor(context) {
super();
// 定義了一系列的事件鉤子,分別在不同的時刻觸發(fā)
this.hooks = {
shouldEmit: new SyncBailHook(["compilation"]),
done: new AsyncSeriesHook(["stats"]),
//....更多鉤子
};
this.running = true;
//其他一些變量聲明
}
//調(diào)用該方法之后會監(jiān)聽文件變更善已,一旦變更則重新執(zhí)行編譯
watch(watchOptions, handler) {
this.running = true;
return new Watching(this, watchOptions, handler)
}
//用于觸發(fā)編譯時所有的工作
run(callback) {
//編譯之后的處理灼捂,省略了部分代碼
const onCompiled = (err, compilation) => {
this.emitAssets(compilation, err => {...})
}
}
//負責(zé)將編譯輸出的文件寫入本地
emitAssets(compilation, callback) {}
//創(chuàng)建一個compilation對象,并將compiler自身作為參數(shù)傳遞
createCompilation() {
return new Compilation(this);
}
//觸發(fā)編譯换团,在內(nèi)部創(chuàng)建compilation實例并執(zhí)行相應(yīng)操作
compile() {}
//以上核心方法中很多會通過this.hooks.someHooks.call來觸發(fā)指定的事件
}
可以看到悉稠,compiler
中設(shè)置了一系列的事件鉤子和各種配置參數(shù),并定義了webpack諸如啟動編譯艘包、觀測文件變動的猛、將編譯結(jié)果文件寫入本地等一系列核心方法。在plugin執(zhí)行的相應(yīng)工作中我們肯定會需要通過compiler拿到webpack的各種信息想虎。
接下來看看compilation
如果把compiler
算作是總控制臺卦尊,那么compilation
則專注于編譯處理這件事上。
在啟用Watch模式后舌厨,webpack將會監(jiān)聽文件是否發(fā)生變化岂却,每當檢測到文件發(fā)生變化,將會執(zhí)行一次新的編譯,并同時生成新的編譯資源和新的compilation
對象躏哩。
compilation
對象中包含了模塊資源署浩、編譯生成資源以及變化的文件和被跟蹤依賴的狀態(tài)信息等等,以供插件工作時使用扫尺。如果我們在插件中需要完成一個自定義的編譯過程筋栋,那么必然會用到這個對象。
tips: 在webpack-dev-server和webpack-dev-middleware里Watch模式默認開啟
插件編寫示例
首先看一個插件示例正驻,這個插件在我們構(gòu)建完相關(guān)的文件后弊攘,會輸出一個記錄所有構(gòu)建文件名的filelist.md
文件。
class myPlugin {
constructor(option) {
this.option = option
}
apply(compiler) {
compiler.hooks.emit.tap('myPlugin', compilation => {
let filelist = '構(gòu)建后的文件: \n'
for (var filename in compilation.assets) {
filelist += '- ' + filename + '\n';
}
compilation.assets['filelist.md'] = {
source: function() {
return filelist
},
size: function() {
return filelist.length
}
}
})
}
}
在webpack的emit
事件被觸發(fā)之后姑曙,我們的插件會執(zhí)行指定的工作肴颊,并將包含了編譯生成資源的compilation作為參數(shù)傳入了函數(shù)。我們可以通過compilation.assets拿到生成的文件渣磷,并獲取其中的filename值婿着。
同樣的,我們也可以獲取到構(gòu)建后的文件內(nèi)容醋界。
接下來我們編寫一個插件竟宋,將編譯后的.js
和.css
文件進行g(shù)zip壓縮。
const zlib = require('zlib')
class gzipPlugin {
constructor(option) {
this.option = option
}
apply(compiler) {
compiler.hooks.emit.tap('myPlugin', compilation => {
for (var filename in compilation.assets) {
if (/(.js|.css)/.test(filename)) {
const gzipFile = zlib.gzipSync(compilation.assets[filename]._value, {
//壓縮等級
level: this.option.level || 7
})
compilation.assets[filename + '.gz'] = {
source: function () {
return gzipFile
},
size: function () {
return gzipFile.length
}
}
}
}
})
}
}
//webpack.config.js中調(diào)用
{
...
plugins: [
new gzipPlugin({
//設(shè)置壓縮等級
level: 9
})
]
}
在這個插件中形纺,我們同樣監(jiān)聽compiler的emit事件丘侠,通過compilation.assets[filename]._value
拿到文件內(nèi)容,之后通過node自帶的zlib庫便可生成gzip文件了逐样。
壓縮后結(jié)果如下:
關(guān)于gzip的更多實踐內(nèi)容蜗字,可以去這篇文章查看 gzip壓縮實踐
異步事件鉤子
webpack有些事件鉤子是支持異步的。
具體可以通過tapAsync或者tapPromise來實現(xiàn)脂新,接下來看分別看一個示例挪捕。
class AsyncPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('asyncEmit', (compilation, callback) => {
console.log('asyncEmit')
setTimeout(() => {
//異步完成后調(diào)用callback函數(shù)以繼續(xù)流程
callback()
}, 2000)
})
}
}
class LogPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('log', (compilation, callback) => {
console.log('LogPlugin')
})
compiler.hooks.done.tap('done', () => {
console.log('done')
})
}
}
//webpack.config.js中調(diào)用
{
//...
plugins: [
new AsyncPlugin(),
new LogPlugin()
]
}
以上代碼輸出順序如下:asyncEmit,2秒后輸出LogPlugin争便,緊跟著輸出done级零。
使用tapPromise也同理,只需稍稍改變一下寫法即可:
class AsyncPlugin {
apply(compiler) {
compiler.hooks.emit.tapPromise('asyncEmit', compilation => {
// 返回一個 Promise滞乙,在我們的異步任務(wù)完成時 resolve……
return new Promise((resolve, reject) => {
setTimeout(function() {
console.log('異步工作完成……')
resolve()
}, 1000);
})
})
}
}
結(jié)合Tapable在插件中使用自定義事件
Tapable
是一個小型的庫奏纪,類似于Node.js的EventEmitter
類,負責(zé)自定義事件的注冊和觸發(fā)斩启。
const {SyncHook} = require('tapable')
class MainPlugin {
apply(compiler) {
//在hooks上自定義一個名為mainPlugin的鉤子
compiler.hooks.mainPlugin = new SyncHook(['data'])
//在webpack的environment事件觸發(fā)時序调,廣播自定義的mainPlugin事件,并傳參
compiler.hooks.environment.tap('mainPlugin', (compilation) => {
compiler.hooks.mainPlugin.call({
text: 'MainPlugin Call'
})
})
}
}
class ListenPlugin {
apply(compiler) {
//監(jiān)聽自定義的mainPlugin被觸發(fā)后兔簇,執(zhí)行對應(yīng)的函數(shù)发绢,輸出data.text
compiler.hooks.mainPlugin.tap('listenPlugin', (data) => {
console.log(data.text)
})
}
}
//在webpack.config.js中引用
{
// ...
plugins: [
new MainPlugin(),
new ListenPlugin()
]
}
可以看到荣挨,借助tapable
我們可以在webpack插件中自定義一些事件,用來進行特定的操作朴摊。插件之間也可以通過自定義事件互相調(diào)用部分邏輯恭朗。
webpack自身的compiler
句各、complation
類也是繼承自tapable
來實現(xiàn)自身事件的注冊和觸發(fā)的跷跪。
通過以上的學(xué)習(xí)滩褥,我們接下來對上面的內(nèi)容進行一個小小的總結(jié)伏伐。
1. webpack插件本質(zhì)上是一個函數(shù)糊肠,它的原型上存在一個名為apply函數(shù)皆看。webpack在初始化時 (在最早觸發(fā)的environment事件之前) 會執(zhí)行這個函數(shù)龙誊,并將一個包含了webpack所有配置信息的compiler作為參數(shù)傳遞給apply函數(shù)韭寸。
2. 插件可以通過監(jiān)聽webpack本身觸發(fā)的事件春哨,在不同的時間階段介入進行你想做的操作。
3. 通過獲取到的compiler對象恩伺,我們可以結(jié)合tapable在插件中自定義事件并將其廣播赴背。
4. 在插件中監(jiān)聽一些特定的事件 (thisCompilation到afterEmit這個階段的事件),你可以拿到一個compilation對象晶渠,里面包含了各種編譯資源凰荚,你可以通過操作這個對象對生成的資源進行添加和修改等操作。
通過上面的學(xué)習(xí)褒脯,相信大家插件的編寫和大致原理有了一定的了解和認識便瑟。
webpack執(zhí)行流程
最后我們來對webpack本身的執(zhí)行流程進行一個概述,并將其和compiler事件鉤子的觸發(fā)時機進行一個對照番川。
webpack首先會讀取配置文件到涂,創(chuàng)建compiler對象,之后調(diào)用所有插件中的apply方法颁督,并將參數(shù)傳入其中践啄。
在完成之后會廣播environment
這個事件鉤子。然后讀取配置文件的entry屬性沉御,遍歷所有入口js文件往核。
接下來compiler對象會調(diào)用run方法,正式開始啟動各方面的工作嚷节。
webpack開始為創(chuàng)建compilation對象做準備工作聂儒,首先會調(diào)用一個newCompilationParams
方法,創(chuàng)建compilation對象所需的參數(shù)硫痰,緊接著立刻廣播beforeCompile和compile這兩個事件衩婚。之后compilation對象被創(chuàng)建,并廣播compilation和make事件效斑。
webpack接下來就開始了編譯相關(guān)的工作非春。調(diào)用loader處理各模塊之間的依賴,對每一個require調(diào)用對應(yīng)的loader進行加工,再將加工后的文件處理生成AST抽象語法樹并遍歷這顆抽象語法樹奇昙,構(gòu)建該模塊所依賴的模塊护侮。最后再將所有模塊中的require語法轉(zhuǎn)換成 __webpack_require__
。
以上步驟完成之后webpack會觸發(fā)emit事件储耐,你可以在這個事件中通過compilation.assets拿到生成的各種資源羊初。最后,webpack通過compiler的emitAssets方法將文件輸出到對應(yīng)的構(gòu)建目錄中什湘,操作完成长赞。
本文篇幅有限,對webpack流程只是進行了一個簡單的介紹闽撤,但通過對流程的學(xué)習(xí)和了解得哆,你能夠更合理地運用、編寫插件哟旗。
以上是這篇文章的全部內(nèi)容贩据,希望對您有所幫助。
參考文獻