先附上原文鏈接
原文
Webpack 通過 Plugin 機(jī)制讓其更加靈活剃斧。 在 Webpack 運(yùn)行的生命周期中會(huì)廣播出許多事件,Plugin 可以監(jiān)聽這些事件昔字,在合適的時(shí)機(jī)通過 Webpack 提供的 API 改變輸出結(jié)果。
先來看一個(gè)基礎(chǔ)的plugin
class BasicPlugin{
// 在構(gòu)造函數(shù)中獲取用戶給該插件傳入的配置
constructor(options){
}
// Webpack 會(huì)調(diào)用 BasicPlugin 實(shí)例的 apply 方法給插件實(shí)例傳入 compiler 對(duì)象
apply(compiler){
compiler.plugin('compilation',function(compilation) {
})
}
}
// 導(dǎo)出 Plugin
module.exports = BasicPlugin;
使用
const BasicPlugin = require('./BasicPlugin.js');
module.export = {
plugins:[
new BasicPlugin(options),
]
}
Webpack 啟動(dòng)后岂傲,在讀取配置的過程中會(huì)先執(zhí)行 new BasicPlugin(options) 初始化一個(gè) BasicPlugin 獲得其實(shí)例痰洒。
在初始化 compiler 對(duì)象后,再調(diào)用 basicPlugin.apply(compiler) 給插件實(shí)例傳入 compiler 對(duì)象瘟裸。
插件實(shí)例在獲取到 compiler 對(duì)象后客叉,就可以通過 compiler.plugin(事件名稱, 回調(diào)函數(shù)) 監(jiān)聽到 Webpack 廣播出來的事件。
Compiler 和 Compilation
Compiler 對(duì)象包含了 Webpack 環(huán)境所有的的配置信息话告,包含 options兼搏,loaders,plugins 這些信息沙郭,這個(gè)對(duì)象在 Webpack 啟動(dòng)時(shí)候被實(shí)例化佛呻,它是全局唯一的,可以簡(jiǎn)單地把它理解為 Webpack 實(shí)例病线;
Compilation 對(duì)象包含了當(dāng)前的模塊資源吓著、編譯生成資源、變化的文件等送挑。當(dāng) Webpack 以開發(fā)模式運(yùn)行時(shí)绑莺,每當(dāng)檢測(cè)到一個(gè)文件變化,一次新的 Compilation 將被創(chuàng)建惕耕。Compilation 對(duì)象也提供了很多事件回調(diào)供插件做擴(kuò)展纺裁。通過 Compilation 也能讀取到 Compiler 對(duì)象。
Compiler 和 Compilation 的區(qū)別在于:Compiler 代表了整個(gè) Webpack 從啟動(dòng)到關(guān)閉的生命周期司澎,而 Compilation 只是代表了一次新的編譯对扶。
事件流
Webpack 就像一條生產(chǎn)線区赵,要經(jīng)過一系列處理流程后才能將源文件轉(zhuǎn)換成輸出結(jié)果。
這條生產(chǎn)線上的每個(gè)處理流程的職責(zé)都是單一的浪南,多個(gè)流程之間有存在依賴關(guān)系笼才,只有完成當(dāng)前處理后才能交給下一個(gè)流程去處理。
插件就像是一個(gè)插入到生產(chǎn)線中的一個(gè)功能络凿,在特定的時(shí)機(jī)對(duì)生產(chǎn)線上的資源做處理骡送。
Webpack 在運(yùn)行過程中會(huì)廣播事件,插件只需要監(jiān)聽它所關(guān)心的事件絮记,就能加入到這條生產(chǎn)線中摔踱,去改變生產(chǎn)線的運(yùn)作。 Webpack 的事件流機(jī)制保證了插件的有序性怨愤,使得整個(gè)系統(tǒng)擴(kuò)展性很好派敷。
Webpack 的事件流機(jī)制應(yīng)用了觀察者模式
可以直接在 Compiler 和 Compilation 對(duì)象上廣播和監(jiān)聽事件,方法如下:
/**
* 廣播出事件
* event-name 為事件名稱撰洗,注意不要和現(xiàn)有的事件重名
* params 為附帶的參數(shù)
*/
compiler.apply('event-name',params);
/**
* 監(jiān)聽名稱為 event-name 的事件篮愉,當(dāng) event-name 事件發(fā)生時(shí),函數(shù)就會(huì)被執(zhí)行差导。
* 同時(shí)函數(shù)中的 params 參數(shù)為廣播事件時(shí)附帶的參數(shù)试躏。
*/
compiler.plugin('event-name',function(params) {
});
同理设褐,compilation.apply 和 compilation.plugin 使用方法和上面一致颠蕴。
在開發(fā)插件時(shí),你可能會(huì)不知道該如何下手助析,因?yàn)槟悴恢涝摫O(jiān)聽哪個(gè)事件才能完成任務(wù)犀被。
在開發(fā)插件時(shí),還需要注意以下兩點(diǎn):
只要能拿到 Compiler 或 Compilation 對(duì)象外冀,就能廣播出新的事件弱判,所以在新開發(fā)的插件中也能廣播出事件,給其它插件監(jiān)聽使用锥惋。
傳給每個(gè)插件的 Compiler 和 Compilation 對(duì)象都是同一個(gè)引用昌腰。也就是說在一個(gè)插件中修改了 Compiler 或 Compilation 對(duì)象上的屬性,會(huì)影響到后面的插件膀跌。
有些事件是異步的遭商,這些異步的事件會(huì)附帶兩個(gè)參數(shù),第二個(gè)參數(shù)為回調(diào)函數(shù)捅伤,在插件處理完任務(wù)時(shí)需要調(diào)用回調(diào)函數(shù)通知 Webpack劫流,才會(huì)進(jìn)入下一處理流程。例如
compiler.plugin('emit',function(compilation, callback) {
// 支持處理邏輯
// 處理完畢后執(zhí)行 callback 以通知 Webpack
// 如果不執(zhí)行 callback,運(yùn)行流程將會(huì)一直卡在這不往下執(zhí)行
callback();
});
常用 API
讀取輸出資源祠汇、代碼塊仍秤、模塊及其依賴
有些插件可能需要讀取 Webpack 的處理結(jié)果,例如輸出資源可很、代碼塊诗力、模塊及其依賴,以便做下一步處理我抠。
在 emit
事件發(fā)生時(shí)苇本,代表源文件的轉(zhuǎn)換和組裝已經(jīng)完成,在這里可以讀取到最終將輸出的資源菜拓、代碼塊瓣窄、模塊及其依賴,并且可以修改輸出資源的內(nèi)容纳鼎。
class Plugin {
apply(compiler) {
compiler.plugin('emit', function (compilation, callback) {
// compilation.chunks 存放所有代碼塊俺夕,是一個(gè)數(shù)組
compilation.chunks.forEach(function (chunk) {
// chunk 代表一個(gè)代碼塊
// 代碼塊由多個(gè)模塊組成,通過 chunk.forEachModule 能讀取組成代碼塊的每個(gè)模塊
chunk.forEachModule(function (module) {
// module 代表一個(gè)模塊
// module.fileDependencies 存放當(dāng)前模塊的所有依賴的文件路徑贱鄙,是一個(gè)數(shù)組
module.fileDependencies.forEach(function (filepath) {
});
});
// Webpack 會(huì)根據(jù) Chunk 去生成輸出的文件資源劝贸,每個(gè) Chunk 都對(duì)應(yīng)一個(gè)及其以上的輸出文件
// 例如在 Chunk 中包含了 CSS 模塊并且使用了 ExtractTextPlugin 時(shí),
// 該 Chunk 就會(huì)生成 .js 和 .css 兩個(gè)文件
chunk.files.forEach(function (filename) {
// compilation.assets 存放當(dāng)前所有即將輸出的資源
// 調(diào)用一個(gè)輸出資源的 source() 方法能獲取到輸出資源的內(nèi)容
let source = compilation.assets[filename].source();
});
});
// 這是一個(gè)異步事件贰逾,要記得調(diào)用 callback 通知 Webpack 本次事件監(jiān)聽處理結(jié)束悬荣。
// 如果忘記了調(diào)用 callback菠秒,Webpack 將一直卡在這里而不會(huì)往后執(zhí)行疙剑。
callback();
})
}
}
### 監(jiān)聽文件變化
Webpack 會(huì)從配置的入口模塊出發(fā),依次找出所有的依賴模塊践叠,當(dāng)入口模塊或者其依賴的模塊發(fā)生變化時(shí)言缤, 就會(huì)觸發(fā)一次新的 Compilation。
在開發(fā)插件時(shí)經(jīng)常需要知道是哪個(gè)文件發(fā)生變化導(dǎo)致了新的 Compilation禁灼,為此可以使用如下代碼:
// 當(dāng)依賴的文件發(fā)生變化時(shí)會(huì)觸發(fā) watch-run 事件
compiler.plugin('watch-run', (watching, callback) => {
// 獲取發(fā)生變化的文件列表
const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
// changedFiles 格式為鍵值對(duì)管挟,鍵為發(fā)生變化的文件路徑。
if (changedFiles[filePath] !== undefined) {
// filePath 對(duì)應(yīng)的文件發(fā)生了變化
}
callback();
});
默認(rèn)情況下 Webpack 只會(huì)監(jiān)視入口和其依賴的模塊是否發(fā)生變化弄捕,在有些情況下項(xiàng)目可能需要引入新的文件僻孝,例如引入一個(gè) HTML 文件。
由于 JavaScript 文件不會(huì)去導(dǎo)入 HTML 文件守谓,Webpack 就不會(huì)監(jiān)聽 HTML 文件的變化穿铆,編輯 HTML 文件時(shí)就不會(huì)重新觸發(fā)新的 Compilation。
為了監(jiān)聽 HTML 文件的變化斋荞,我們需要把 HTML 文件加入到依賴列表中荞雏,為此可以使用如下代碼:
compiler.plugin('after-compile', (compilation, callback) => {
// 把 HTML 文件添加到文件依賴列表,好讓 Webpack 去監(jiān)聽 HTML 模塊文件,在 HTML 模版文件發(fā)生變化時(shí)重新啟動(dòng)一次編譯
compilation.fileDependencies.push(filePath);
callback();
});
修改輸出資源
需要監(jiān)聽 emit 事件凤优,因?yàn)榘l(fā)生 emit 事件時(shí)所有模塊的轉(zhuǎn)換和代碼塊對(duì)應(yīng)的文件已經(jīng)生成好悦陋, 需要輸出的資源即將輸出,因此 emit 事件是修改 Webpack 輸出資源的最后時(shí)機(jī)筑辨。
所有需要輸出的資源會(huì)存放在 compilation.assets 中俺驶,compilation.assets 是一個(gè)鍵值對(duì),鍵為需要輸出的文件名稱挖垛,值為文件對(duì)應(yīng)的內(nèi)容痒钝。
設(shè)置:
compiler.plugin('emit', (compilation, callback) => {
// 設(shè)置名稱為 fileName 的輸出資源
compilation.assets[fileName] = {
// 返回文件內(nèi)容
source: () => {
// fileContent 既可以是代表文本文件的字符串,也可以是代表二進(jìn)制文件的 Buffer
return fileContent;
},
// 返回文件大小
size: () => {
return Buffer.byteLength(fileContent, 'utf8');
}
};
callback();
});
讀攘《尽:
compiler.plugin('emit', (compilation, callback) => {
// 讀取名稱為 fileName 的輸出資源
const asset = compilation.assets[fileName];
// 獲取輸出資源的內(nèi)容
asset.source();
// 獲取輸出資源的文件大小
asset.size();
callback();
});
判斷 Webpack 使用了哪些插件
// 判斷當(dāng)前配置使用使用了 ExtractTextPlugin送矩,
// compiler 參數(shù)即為 Webpack 在 apply(compiler) 中傳入的參數(shù)
function hasExtractTextPlugin(compiler) {
// 當(dāng)前配置所有使用的插件列表
const plugins = compiler.options.plugins;
// 去 plugins 中尋找有沒有 ExtractTextPlugin 的實(shí)例
return plugins.find(plugin=>plugin instanceof ExtractTextPlugin) != null;
}
demo
EndWebpackPlugin,作用是在 Webpack 即將退出時(shí)再附加一些額外的操作哪替,例如在 Webpack 成功編譯和輸出了文件后執(zhí)行發(fā)布操作把輸出的文件上傳到服務(wù)器栋荸。 同時(shí)該插件還能區(qū)分 Webpack 構(gòu)建是否執(zhí)行成功。
module.exports = {
plugins:[
// 在初始化 EndWebpackPlugin 時(shí)傳入了兩個(gè)參數(shù)凭舶,分別是在成功時(shí)的回調(diào)函數(shù)和失敗時(shí)的回調(diào)函數(shù)晌块;
new EndWebpackPlugin(() => {
// Webpack 構(gòu)建成功,并且文件輸出了后會(huì)執(zhí)行到這里帅霜,在這里可以做發(fā)布文件操作
}, (err) => {
// Webpack 構(gòu)建失敗匆背,err 是導(dǎo)致錯(cuò)誤的原因
console.error(err);
})
]
}
需要借助兩個(gè)事件:
done:在成功構(gòu)建并且輸出了文件后,Webpack 即將退出時(shí)發(fā)生身冀;
failed:在構(gòu)建出現(xiàn)異常導(dǎo)致構(gòu)建失敗钝尸,Webpack 即將退出時(shí)發(fā)生;
class EndWebpackPlugin {
constructor(doneCallback, failCallback) {
// 存下在構(gòu)造函數(shù)中傳入的回調(diào)函數(shù)
this.doneCallback = doneCallback;
this.failCallback = failCallback;
}
apply(compiler) {
compiler.plugin('done', (stats) => {
// 在 done 事件中回調(diào) doneCallback
this.doneCallback(stats);
});
compiler.plugin('failed', (err) => {
// 在 failed 事件中回調(diào) failCallback
this.failCallback(err);
});
}
}
// 導(dǎo)出插件
module.exports = EndWebpackPlugin;