編寫 Loader
Loader就像是一個(gè)翻譯員,能把源文件經(jīng)過(guò)轉(zhuǎn)化后輸出新的結(jié)果,并且一個(gè)文件還可以鏈?zhǔn)降慕?jīng)過(guò)多個(gè)翻譯員翻譯。
以處理SCSS文件為例:
- SCSS源代碼會(huì)先交給
sass-loader把SCSS轉(zhuǎn)換成CSS; - 把
sass-loader輸出的CSS交給css-loader處理,找出CSS中依賴的資源、壓縮CSS等; - 把
css-loader輸出的CSS交給style-loader處理,轉(zhuǎn)換成通過(guò)腳本加載的JavaScript代碼;
可以看出以上的處理過(guò)程需要有順序的鏈?zhǔn)綀?zhí)行,先sass-loader再css-loader再style-loader。 以上處理的Webpack相關(guān)配置如下:
module.exports = {
module: {
rules: [
{
// 增加對(duì) SCSS 文件的支持
test: /\.scss/,
// SCSS 文件的處理順序?yàn)橄?sass-loader 再 css-loader 再 style-loader
use: [
'style-loader',
{
loader:'css-loader',
// 給 css-loader 傳入配置項(xiàng)
options:{
minimize:true,
}
},
'sass-loader'],
},
]
},
};
Loader的職責(zé)
由上面的例子可以看出:一個(gè)Loader的職責(zé)是單一的,只需要完成一種轉(zhuǎn)換。 如果一個(gè)源文件需要經(jīng)歷多步轉(zhuǎn)換才能正常使用,就通過(guò)多個(gè)Loader去轉(zhuǎn)換。 在調(diào)用多個(gè)Loader去轉(zhuǎn)換一個(gè)文件時(shí),每個(gè)Loader會(huì)鏈?zhǔn)降捻樞驁?zhí)行, 第一個(gè)Loader將會(huì)拿到需處理的原內(nèi)容,上一個(gè)Loader處理后的結(jié)果會(huì)傳給下一個(gè)接著處理,最后的Loader將處理后的最終結(jié)果返回給Webpack。
所以,在你開(kāi)發(fā)一個(gè)Loader時(shí),請(qǐng)保持其職責(zé)的單一性,你只需關(guān)心輸入和輸出。
Loader基礎(chǔ)
由于Webpack是運(yùn)行在Node.js之上的,一個(gè)Loader其實(shí)就是一個(gè)Node.js模塊,這個(gè)模塊需要導(dǎo)出一個(gè)函數(shù)。 這個(gè)導(dǎo)出的函數(shù)的工作就是獲得處理前的原內(nèi)容,對(duì)原內(nèi)容執(zhí)行處理后,返回處理后的內(nèi)容。
一個(gè)最簡(jiǎn)單的Loader的源碼如下:
module.exports = function(source) {
// source 為 compiler 傳遞給 Loader 的一個(gè)文件的原內(nèi)容
// 該函數(shù)需要返回處理后的內(nèi)容,這里簡(jiǎn)單起見(jiàn),直接把原內(nèi)容返回了,相當(dāng)于該`Loader`沒(méi)有做任何轉(zhuǎn)換
return source;
};
由于Loader運(yùn)行在Node.js中,你可以調(diào)用任何Node.js自帶的API,或者安裝第三方模塊進(jìn)行調(diào)用:
const sass = require('node-sass');
module.exports = function(source) {
return sass(source);
};
Loader進(jìn)階
Webpack還提供一些API供Loader調(diào)用。
獲得Loader的options
在最上面處理SCSS文件的Webpack配置中,給css-loader傳了options參數(shù),以控制css-loader。要在自己編寫的Loader中獲取到用戶傳入的options,需要這樣做:
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// 獲取到用戶給當(dāng)前 Loader 傳入的 options
const options = loaderUtils.getOptions(this);
return source;
};
返回其它結(jié)果
上面的Loader都只是返回了原內(nèi)容轉(zhuǎn)換后的內(nèi)容,但有些場(chǎng)景下還需要返回除了內(nèi)容之外的東西。
例如以用babel-loader轉(zhuǎn)換ES6代碼為例,它還需要輸出轉(zhuǎn)換后的ES5代碼對(duì)應(yīng)的Source Map,以方便調(diào)試源碼。 為了把Source Map也一起隨著ES5代碼返回給Webpack,可以這樣寫:
module.exports = function(source) {
// 通過(guò) this.callback 告訴 Webpack 返回的結(jié)果
this.callback(null, source, sourceMaps);
// 當(dāng)你使用 this.callback 返回內(nèi)容時(shí),該 Loader 必須返回 undefined,
// 以讓 Webpack 知道該 Loader 返回的結(jié)果在 this.callback 中,而不是 return 中
return;
};
其中的this.callback是Webpack給Loader注入的API,以方便Loader和Webpack之間通信。this.callback的詳細(xì)使用方法如下:
this.callback(
// 當(dāng)無(wú)法轉(zhuǎn)換原內(nèi)容時(shí),給 Webpack 返回一個(gè) Error
err: Error | null,
// 原內(nèi)容轉(zhuǎn)換后的內(nèi)容
content: string | Buffer,
// 用于把轉(zhuǎn)換后的內(nèi)容得出原內(nèi)容的 Source Map,方便調(diào)試
sourceMap?: SourceMap,
// 如果本次轉(zhuǎn)換為原內(nèi)容生成了 AST 語(yǔ)法樹(shù),可以把這個(gè) AST 返回,
// 以方便之后需要 AST 的 Loader 復(fù)用該 AST,以避免重復(fù)生成 AST,提升性能
abstractSyntaxTree?: AST
);
Source Map的生成很耗時(shí),通常在開(kāi)發(fā)環(huán)境下才會(huì)生成Source Map,其它環(huán)境下不用生成,以加速構(gòu)建。 為此Webpack為Loader提供了this.sourceMap API去告訴Loader當(dāng)前構(gòu)建環(huán)境下用戶是否需要Source Map。
同步與異步
Loader有同步和異步之分,上面介紹的Loader都是同步的Loader,因?yàn)樗鼈兊霓D(zhuǎn)換流程都是同步的,轉(zhuǎn)換完成后再返回結(jié)果。 但在有些場(chǎng)景下轉(zhuǎn)換的步驟只能是異步完成的,例如你需要通過(guò)網(wǎng)絡(luò)請(qǐng)求才能得出結(jié)果,如果采用同步的方式網(wǎng)絡(luò)請(qǐng)求就會(huì)阻塞整個(gè)構(gòu)建,導(dǎo)致構(gòu)建非常緩慢。
在轉(zhuǎn)換步驟是異步時(shí),你可以這樣:
module.exports = function(source) {
// 告訴 Webpack 本次轉(zhuǎn)換是異步的,Loader 會(huì)在 callback 中回調(diào)結(jié)果
var callback = this.async();
someAsyncOperation(source, function(err, result, sourceMaps, ast) {
// 通過(guò) callback 返回異步執(zhí)行后的結(jié)果
callback(err, result, sourceMaps, ast);
});
};
處理二進(jìn)制數(shù)據(jù)
在默認(rèn)的情況下,Webpack傳給Loader的原內(nèi)容都是UTF-8格式編碼的字符串。 但有些場(chǎng)景下Loader不是處理文本文件,而是處理二進(jìn)制文件,例如file-loader,就需要Webpack給Loader傳入二進(jìn)制格式的數(shù)據(jù)。 為此,你需要這樣編寫Loader:
module.exports = function(source) {
// 在 exports.raw === true 時(shí),Webpack 傳給 Loader 的 source 是 Buffer 類型的
source instanceof Buffer === true;
// Loader 返回的類型也可以是 Buffer 類型的
// 在 exports.raw !== true 時(shí),Loader 也可以返回 Buffer 類型的結(jié)果
return source;
};
// 通過(guò) exports.raw 屬性告訴 Webpack 該 Loader 是否需要二進(jìn)制數(shù)據(jù)
module.exports.raw = true;
以上代碼中最關(guān)鍵的代碼是最后一行module.exports.raw = true;,沒(méi)有該行Loader只能拿到字符串。
緩存加速
在有些情況下,有些轉(zhuǎn)換操作需要大量計(jì)算非常耗時(shí),如果每次構(gòu)建都重新執(zhí)行重復(fù)的轉(zhuǎn)換操作,構(gòu)建將會(huì)變得非常緩慢。為此,Webpack會(huì)默認(rèn)緩存所有Loader的處理結(jié)果,也就是說(shuō)在需要被處理的文件或者其依賴的文件沒(méi)有發(fā)生變化時(shí), 是不會(huì)重新調(diào)用對(duì)應(yīng)的Loader去執(zhí)行轉(zhuǎn)換操作的。
如果想讓W(xué)ebpack不緩存該Loader的處理結(jié)果,可以這樣:
module.exports = function(source) {
// 關(guān)閉該 Loader 的緩存功能
this.cacheable(false);
return source;
};
其它Loader API
除了以上提到的在Loader中能調(diào)用的Webpack API外,還存在以下常用API:
-
this.context:當(dāng)前處理文件的所在目錄,假如當(dāng)前Loader處理的文件是/src/main.js,則this.context就等于/src。 -
this.resource:當(dāng)前處理文件的完整請(qǐng)求路徑,包括querystring,例如/src/main.js?name=1。 -
this.resourcePath:當(dāng)前處理文件的路徑,例如/src/main.js。 -
this.resourceQuery:當(dāng)前處理文件的querystring。 -
this.target:等于Webpack配置中的Target。 -
this.loadModule:當(dāng)Loader在處理一個(gè)文件時(shí),如果依賴其它文件的處理結(jié)果才能得出當(dāng)前文件的結(jié)果時(shí), 就可以通過(guò)this.loadModule(request: string, callback: function(err, source, sourceMap, module))去獲得request對(duì)應(yīng)文件的處理結(jié)果。 -
this.resolve:像require語(yǔ)句一樣獲得指定文件的完整路徑,使用方法為resolve(context: string, request: string, callback: function(err, result: string))。 -
this.addDependency:給當(dāng)前處理文件添加其依賴的文件,以便再其依賴的文件發(fā)生變化時(shí),會(huì)重新調(diào)用Loader處理該文件。使用方法為addDependency(file: string)。 -
this.addContextDependency:和addDependency類似,但addContextDependency是把整個(gè)目錄加入到當(dāng)前正在處理文件的依賴中。使用方法為addContextDependency(directory: string)。 -
this.clearDependencies:清除當(dāng)前正在處理文件的所有依賴,使用方法為clearDependencies()。 -
this.emitFile:輸出一個(gè)文件,使用方法為emitFile(name: string, content: Buffer|string, sourceMap: {...})。
加載本地Loader
在開(kāi)發(fā)Loader的過(guò)程中,為了測(cè)試編寫的Loader是否能正常工作,需要把它配置到Webpack中后,才可能會(huì)調(diào)用該Loader。使用的Loader都是通過(guò)Npm安裝的,要使用Loader時(shí)會(huì)直接使用Loader的名稱,代碼如下:
module.exports = {
module: {
rules: [
{
test: /\.css/,
use: ['style-loader'],
},
]
},
};
如果還采取以上的方法去使用本地開(kāi)發(fā)的Loader將會(huì)很麻煩,因?yàn)槟阈枰_保編寫的Loader的源碼是在node_modules目錄下。 為此你需要先把編寫的Loader發(fā)布到Npm倉(cāng)庫(kù)后再安裝到本地項(xiàng)目使用。
解決以上問(wèn)題的便捷方法有兩種,分別如下:
Npm link
Npm link專門用于開(kāi)發(fā)和調(diào)試本地Npm模塊,能做到在不發(fā)布模塊的情況下,把本地的一個(gè)正在開(kāi)發(fā)的模塊的源碼鏈接到項(xiàng)目的node_modules目錄下,讓項(xiàng)目可以直接使用本地的Npm模塊。 由于是通過(guò)軟鏈接的方式實(shí)現(xiàn)的,編輯了本地的Npm模塊代碼,在項(xiàng)目中也能使用到編輯后的代碼。
完成Npm link的步驟如下:
- 確保正在開(kāi)發(fā)的本地Npm模塊(也就是正在開(kāi)發(fā)的Loader)的
package.json已經(jīng)正確配置好; - 在本地Npm模塊根目錄下執(zhí)行
npm link,把本地模塊注冊(cè)到全局; - 在項(xiàng)目根目錄下執(zhí)行
npm link loader-name,把第2步注冊(cè)到全局的本地Npm模塊鏈接到項(xiàng)目的node_moduels下,其中的loader-name是指在第1步中的package.json文件中配置的模塊名稱。
鏈接好Loader到項(xiàng)目后你就可以像使用一個(gè)真正的 Npm 模塊一樣使用本地的Loader了。
ResolveLoader
ResolveLoader用于配置Webpack如何尋找Loader。 默認(rèn)情況下只會(huì)去node_modules目錄下尋找,為了讓W(xué)ebpack加載放在本地項(xiàng)目中的Loader需要修改resolveLoader.modules。
假如本地的Loader在項(xiàng)目目錄中的./loaders/loader-name中,則需要如下配置:
module.exports = {
resolveLoader:{
// 去哪些目錄下尋找 Loader,有先后順序之分
modules: ['node_modules','./loaders/'],
}
}
加上以上配置后,Webpack會(huì)先去node_modules項(xiàng)目下尋找Loader,如果找不到,會(huì)再去./loaders/目錄下尋找。
實(shí)戰(zhàn)
接下來(lái)從實(shí)際出發(fā),來(lái)編寫一個(gè)解決實(shí)際問(wèn)題的Loader。
該Loader名叫comment-require-loader,作用是把JavaScript代碼中的注釋語(yǔ)法
// @require '../style/index.css'
轉(zhuǎn)換成
require('../style/index.css');
該Loader的使用方法如下:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['comment-require-loader'],
// 針對(duì)采用了 fis3 CSS 導(dǎo)入語(yǔ)法的 JavaScript 文件通過(guò) comment-require-loader 去轉(zhuǎn)換
include: [path.resolve(__dirname, 'node_modules/imui')]
}
]
}
};
該Loader的實(shí)現(xiàn)非常簡(jiǎn)單,完整代碼如下:
function replace(source) {
// 使用正則把 // @require '../style/index.css' 轉(zhuǎn)換成 require('../style/index.css');
return source.replace(/(\/\/ *@require) +(('|").+('|")).*/, 'require($2);');
}
module.exports = function (content) {
return replace(content);
};
編寫Plugin
Webpack通過(guò)Plugin機(jī)制讓其更加靈活,以適應(yīng)各種應(yīng)用場(chǎng)景。 在Webpack運(yùn)行的生命周期中會(huì)廣播出許多事件,Plugin可以監(jiān)聽(tīng)這些事件,在合適的時(shí)機(jī)通過(guò)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;
在使用這個(gè)Plugin時(shí),相關(guān)配置代碼如下:
const BasicPlugin = require('./BasicPlugin.js');
module.export = {
plugins:[
new BasicPlugin(options),
]
}
Webpack啟動(dòng)后,在讀取配置的過(guò)程中會(huì)先執(zhí)行new BasicPlugin(options)初始化一個(gè)BasicPlugin獲得其實(shí)例。 在初始化compiler對(duì)象后,再調(diào)用basicPlugin.apply(compiler)給插件實(shí)例傳入compiler對(duì)象。 插件實(shí)例在獲取到compiler對(duì)象后,就可以通過(guò)compiler.plugin(事件名稱, 回調(diào)函數(shù))監(jiān)聽(tīng)到Webpack廣播出來(lái)的事件。 并且可以通過(guò)compiler對(duì)象去操作Webpack。
Compiler和Compilation
在開(kāi)發(fā)Plugin時(shí)最常用的兩個(gè)對(duì)象就是Compiler和Compilation,它們是Plugin和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以開(kāi)發(fā)模式運(yùn)行時(shí),每當(dāng)檢測(cè)到一個(gè)文件變化,一次新的Compilation將被創(chuàng)建。Compilation對(duì)象也提供了很多事件回調(diào)供插件做擴(kuò)展。通過(guò)Compilation也能讀取到Compiler對(duì)象。
Compiler和Compilation的區(qū)別在于:Compiler代表了整個(gè)Webpack從啟動(dòng)到關(guān)閉的生命周期,而Compilation只是代表了一次新的編譯。
事件流
Webpack就像一條生產(chǎn)線,要經(jīng)過(guò)一系列處理流程后才能將源文件轉(zhuǎn)換成輸出結(jié)果。 這條生產(chǎn)線上的每個(gè)處理流程的職責(zé)都是單一的,多個(gè)流程之間有存在依賴關(guān)系,只有完成當(dāng)前處理后才能交給下一個(gè)流程去處理。 插件就像是一個(gè)插入到生產(chǎn)線中的一個(gè)功能,在特定的時(shí)機(jī)對(duì)生產(chǎn)線上的資源做處理。
Webpack通過(guò)Tapable來(lái)組織這條復(fù)雜的生產(chǎn)線。 Webpack在運(yùn)行過(guò)程中會(huì)廣播事件,插件只需要監(jiān)聽(tīng)它所關(guān)心的事件,就能加入到這條生產(chǎn)線中,去改變生產(chǎn)線的運(yùn)作。 Webpack的事件流機(jī)制保證了插件的有序性,使得整個(gè)系統(tǒng)擴(kuò)展性很好。
Webpack的事件流機(jī)制應(yīng)用了觀察者模式,和Node.js中的EventEmitter非常相似。Compiler和Compilation都繼承自Tapable,可以直接在Compiler和Compilation對(duì)象上廣播和監(jiān)聽(tīng)事件,方法如下:
/**
* 廣播出事件
* event-name 為事件名稱,注意不要和現(xiàn)有的事件重名
* params 為附帶的參數(shù)
*/
compiler.apply('event-name',params);
/**
* 監(jiān)聽(tīng)名稱為 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使用方法和上面一致。
在開(kāi)發(fā)插件時(shí),你可能會(huì)不知道該如何下手,因?yàn)槟悴恢涝摫O(jiān)聽(tīng)哪個(gè)事件才能完成任務(wù)。
在開(kāi)發(fā)插件時(shí),還需要注意以下兩點(diǎn):
- 只要能拿到
Compiler或Compilation對(duì)象,就能廣播出新的事件,所以在新開(kāi)發(fā)的插件中也能廣播出事件,給其它插件監(jiān)聽(tīng)使用。 - 傳給每個(gè)插件的
Compiler和Compilation對(duì)象都是同一個(gè)引用。也就是說(shuō)在一個(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
插件可以用來(lái)修改輸出文件、增加輸出文件、甚至可以提升Webpack性能、等等,總之插件通過(guò)調(diào)用 Webpack提供的API能完成很多事情。 由于Webpack提供的API非常多,有很多API很少用的上,又加上篇幅有限,下面來(lái)介紹一些常用的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è)模塊組成,通過(guò) 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)聽(tīng)處理結(jié)束。
// 如果忘記了調(diào)用 callback,Webpack 將一直卡在這里而不會(huì)往后執(zhí)行。
callback();
})
}
}
監(jiān)聽(tīng)文件變化
Webpack會(huì)從配置的入口模塊出發(fā),依次找出所有的依賴模塊,當(dāng)入口模塊或者其依賴的模塊發(fā)生變化時(shí), 就會(huì)觸發(fā)一次新的Compilation。
在開(kāi)發(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)聽(tīng)HTML文件的變化,編輯HTML文件時(shí)就不會(huì)重新觸發(fā)新的Compilation。 為了監(jiān)聽(tīng)HTML文件的變化,我們需要把HTML文件加入到依賴列表中,為此可以使用如下代碼:
compiler.plugin('after-compile', (compilation, callback) => {
// 把 HTML 文件添加到文件依賴列表,好讓 Webpack 去監(jiān)聽(tīng) HTML 模塊文件,在 HTML 模版文件發(fā)生變化時(shí)重新啟動(dòng)一次編譯
compilation.fileDependencies.push(filePath);
callback();
});
修改輸出資源
有些場(chǎng)景下插件需要修改、增加、刪除輸出的資源,要做到這點(diǎn)需要監(jiān)聽(tīng)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è)置compilation.assets的代碼如下:
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();
});
讀取compilation.assets的代碼如下:
compiler.plugin('emit', (compilation, callback) => {
// 讀取名稱為 fileName 的輸出資源
const asset = compilation.assets[fileName];
// 獲取輸出資源的內(nèi)容
asset.source();
// 獲取輸出資源的文件大小
asset.size();
callback();
});
判斷Webpack使用了哪些插件
在開(kāi)發(fā)一個(gè)插件時(shí)可能需要根據(jù)當(dāng)前配置是否使用了其它某個(gè)插件而做下一步?jīng)Q定,因此需要讀取Webpack當(dāng)前的插件配置情況。 以判斷當(dāng)前是否使用了ExtractTextPlugin為例,可以使用如下代碼:
// 判斷當(dāng)前配置使用使用了 ExtractTextPlugin,
// compiler 參數(shù)即為 Webpack 在 apply(compiler) 中傳入的參數(shù)
function hasExtractTextPlugin(compiler) {
// 當(dāng)前配置所有使用的插件列表
const plugins = compiler.options.plugins;
// 去 plugins 中尋找有沒(méi)有 ExtractTextPlugin 的實(shí)例
return plugins.find(plugin=>plugin.__proto__.constructor === ExtractTextPlugin) != null;
}
實(shí)戰(zhàn)
下面我們?nèi)?shí)現(xiàn)一個(gè)插件。
該插件的名稱取名叫EndWebpackPlugin,作用是在Webpack即將退出時(shí)再附加一些額外的操作,例如在Webpack成功編譯和輸出了文件后執(zhí)行發(fā)布操作把輸出的文件上傳到服務(wù)器。 同時(shí)該插件還能區(qū)分Webpack構(gòu)建是否執(zhí)行成功。使用該插件時(shí)方法如下:
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);
})
]
}
要實(shí)現(xiàn)該插件,需要借助兩個(gè)事件:
-
done:在成功構(gòu)建并且輸出了文件后,Webpack即將退出時(shí)發(fā)生; -
failed:在構(gòu)建出現(xiàn)異常導(dǎo)致構(gòu)建失敗,Webpack即將退出時(shí)發(fā)生;
實(shí)現(xiàn)該插件非常簡(jiǎn)單,完整代碼如下:
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;
從開(kāi)發(fā)這個(gè)插件可以看出,找到合適的事件點(diǎn)去完成功能在開(kāi)發(fā)插件時(shí)顯得尤為重要。Webpack在運(yùn)行過(guò)程中廣播出常用事件,你可以從中找到你需要的事件。