Webpack原理—編寫Loader和Plugin

編寫 Loader

Loader就像是一個(gè)翻譯員,能把源文件經(jīng)過(guò)轉(zhuǎn)化后輸出新的結(jié)果,并且一個(gè)文件還可以鏈?zhǔn)降慕?jīng)過(guò)多個(gè)翻譯員翻譯。
以處理SCSS文件為例:

  1. SCSS源代碼會(huì)先交給sass-loader把SCSS轉(zhuǎn)換成CSS;
  2. sass-loader輸出的CSS交給css-loader處理,找出CSS中依賴的資源、壓縮CSS等;
  3. css-loader輸出的CSS交給style-loader處理,轉(zhuǎn)換成通過(guò)腳本加載的JavaScript代碼;

可以看出以上的處理過(guò)程需要有順序的鏈?zhǔn)綀?zhí)行,先sass-loadercss-loaderstyle-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的步驟如下:

  1. 確保正在開(kāi)發(fā)的本地Npm模塊(也就是正在開(kāi)發(fā)的Loader)的package.json已經(jīng)正確配置好;
  2. 在本地Npm模塊根目錄下執(zhí)行npm link,把本地模塊注冊(cè)到全局;
  3. 在項(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ì)象就是CompilerCompilation,它們是Plugin和Webpack之間的橋梁。CompilerCompilation的含義如下:

  • 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ì)象。

CompilerCompilation的區(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非常相似。CompilerCompilation都繼承自Tapable,可以直接在CompilerCompilation對(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.applycompilation.plugin使用方法和上面一致。
在開(kāi)發(fā)插件時(shí),你可能會(huì)不知道該如何下手,因?yàn)槟悴恢涝摫O(jiān)聽(tīng)哪個(gè)事件才能完成任務(wù)。
在開(kāi)發(fā)插件時(shí),還需要注意以下兩點(diǎn):

  • 只要能拿到CompilerCompilation對(duì)象,就能廣播出新的事件,所以在新開(kāi)發(fā)的插件中也能廣播出事件,給其它插件監(jiān)聽(tīng)使用。
  • 傳給每個(gè)插件的CompilerCompilation對(duì)象都是同一個(gè)引用。也就是說(shuō)在一個(gè)插件中修改了CompilerCompilation對(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ò)程中廣播出常用事件,你可以從中找到你需要的事件。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 可以結(jié)合慕課網(wǎng)的視頻來(lái)讀這篇文章,地址:http://www.imooc.com/learn/802 Webpac...
    哈哈騰飛閱讀 2,089評(píng)論 0 7
  • 版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載。 webpack介紹和使用 一、webpack介紹 1、由來(lái) ...
    it筱竹閱讀 11,444評(píng)論 0 21
  • GitChat技術(shù)雜談 前言 本文較長(zhǎng),為了節(jié)省你的閱讀時(shí)間,在文前列寫作思路如下: 什么是 webpack,它要...
    蕭玄辭閱讀 12,869評(píng)論 7 110
  • webpack 核心 核心概述 entry 入口文件:js 代碼文件,可執(zhí)行的 node 模塊或打包的入口文件。 ...
    coolheadedY閱讀 3,602評(píng)論 1 6
  • 是累也好,是樂(lè)也好,路,一直在。 路也一直在走,大路小路也好,過(guò)山涉水也好,你只能向前,朝著你的方向,一直走…… ...
    文子灣閱讀 251評(píng)論 2 6

友情鏈接更多精彩內(nèi)容