寫一個 Webpack 插件

通過前面章節(jié)內(nèi)容的講解蛉拙,對于 Webpack 的插件應(yīng)該已經(jīng)不陌生了岛请,而且對于 Webpack 很多高級的知識點應(yīng)該都有了一定的了解压怠,包括 Webpack 中的 Compiler 和 Compilation 對象掐场,以及 Webpack 的插件原理。

在本章節(jié)中旋膳,主要以官網(wǎng)提供的例子 FileListPlugin/HelloWorldPlugin 來說明如何寫一個插件澎语,而這部分內(nèi)容在前面應(yīng)該已經(jīng)都有了深入的了解。同時验懊,在本章節(jié)中也會給出 Webpack 中不同插件的類型與區(qū)別擅羞。但是,如果想要寫一個自己的 Webpack 的復(fù)雜插件义图,那么除了前面的內(nèi)容以外减俏,也要注意日常的積累??。

如何寫一個 Webpack 的插件

Webpack 的插件機制將 Webpack 引擎的能力暴露給了開發(fā)者歌溉,使用 Webpack 內(nèi)置的各種打包階段鉤子函數(shù)使得開發(fā)者能夠引入他們自己的打包流程垄懂。寫一個 Webpack 插件往往比寫一個 Loader 復(fù)雜骑晶,因為需要了解 Webpack 內(nèi)部很多細節(jié)的部分。

如何創(chuàng)建一個 Webpack 的插件

通過前面的章節(jié)內(nèi)容應(yīng)該有所了解草慧,一個 Webpack 的插件其實包含以下幾個條件:

1桶蛔、一個 js 命名函數(shù)。
2漫谷、在原型鏈上存在一個 apply 方法仔雷。
3、為該插件指定一個 Webpack 的事件鉤子函數(shù)舔示。
4碟婆、使用 Webpack 內(nèi)部的實例對象(Compiler 或者 Compilation)具有的屬性或者方法。
5惕稻、當(dāng)功能完成以后竖共,需要執(zhí)行 Webpack 的回調(diào)函數(shù)。

比如下面的函數(shù)就具備了上面的條件俺祠,所以它是可以作為一個 Webpack 插件的:

function MyExampleWebpackPlugin() {
};
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
  //我們主要關(guān)注 compilation 階段公给,即 webpack 打包階段
  compiler.plugin('compilation', function(compilation , callback) {
    console.log("This is an example plugin!!!");
    //當(dāng)該插件功能完成以后一定要注意回調(diào) callback 函數(shù)
    callback();
  });
};
Compiler 和 Compilation 實例

在前面的章節(jié)中已經(jīng)深入講解了這部分的內(nèi)容,我們下面總結(jié)性的給出兩個對象的作用蜘渣。

Compiler 對象: Compiler 對象代表了 Webpack 完整的可配置的環(huán)境淌铐。該對象在 Webpack 啟動的時候會被創(chuàng)建,同時該對象也會被傳入一些可控的配置蔫缸,如 Options腿准、Loaders、Plugins拾碌。當(dāng)插件被實例化的時候吐葱,會收到一個 Compiler 對象,通過這個對象可以訪問 Webpack 的內(nèi)部環(huán)境倦沧。
Compilation 對象: Compilation 對象在每次文件變化的時候都會被創(chuàng)建唇撬,因此會重新產(chǎn)生新的打包資源它匕。該對象表示本次打包的模塊展融、編譯的資源、文件改變和監(jiān)聽的依賴文件的狀態(tài)豫柬。而且該對象也會提供很多的回調(diào)點告希,我們的插件可以使用它來完成特定的功能,而提供的鉤子函數(shù)在前面的章節(jié)已經(jīng)講過了烧给,此處不再贅述

Hello World 插件

比如下面是我們寫的一個插件:

//插件內(nèi)部可以接受到該插件的配置參數(shù)
function HelloWorldPlugin(options) {
}
HelloWorldPlugin.prototype.apply = function(compiler) {
  //此處利用了 Compiler 提供的 done 鉤子函數(shù)燕偶,作用前面已經(jīng)說過
  compiler.plugin('done', function() {
    console.log('Hello World!');
  });
};
module.exports = HelloWorldPlugin;

那么在 Webpack 配置文件中就可以通過下面的方式來進行配置:

var HelloWorldPlugin = require('hello-world');
//已經(jīng)發(fā)布到 NPM
var webpackConfig = {
  plugins: [
    new HelloWorldPlugin({options: true})
  ]
};

前面已經(jīng)說過,Webpack 插件最重要的就是 Compilation 和 Compiler 對象础嫡。先來看看在插件里面如何使用 Compilation 對象:

function HelloCompilationPlugin(options) {}

HelloCompilationPlugin.prototype.apply = function(compiler) {
  //使用 Compiler 對象的 compilation 鉤子函數(shù)就可以獲取 Compilation 對象
  compiler.plugin("compilation", function(compilation) {
   //使用 Compilation 注冊回調(diào)
    compilation.plugin("optimize", function() {
      console.log("Assets are being optimized.");
    });
  });
};
module.exports = HelloCompilationPlugin;
異步插件

上面看到的 HelloWorld 插件是同步的指么,還有一種插件是異步的酝惧,來看看異步插件如何編寫:

function HelloAsyncPlugin(options) {}

HelloAsyncPlugin.prototype.apply = function(compiler) {
  compiler.plugin("emit", function(compilation, callback) {
    // Do something async...
    setTimeout(function() {
      console.log("Done with async work...");
      callback();
    }, 1000);

  });
};
module.exports = HelloAsyncPlugin;

從這里可看出,異步插件和同步插件最大的不同在于伯诬,異步插件會傳入一個 callback 參數(shù)晚唇,當(dāng)插件完成相應(yīng)的功能以后,必須回調(diào) callback() 函數(shù)盗似。
當(dāng)訪問到 Webpack 的 Compiler 和每次產(chǎn)生的 Compilation 對象的時候哩陕,可以使用 Webpack 的引擎來完成任何事情『帐妫可以重新處理已經(jīng)存在的文件悍及,創(chuàng)建自己的派生文件(想要多產(chǎn)生的文件),或者對將要產(chǎn)生的資源進行修改(HtmlWebpackPlugin)等等接癌。例如心赶,在前面章節(jié)就已經(jīng)講述的下面的實例,該實例就是有效的利用了 Compiler 的文件輸出 emit 階段產(chǎn)生我們自己需要的文件:

function FileListPlugin(options) {}
FileListPlugin.prototype.apply = function(compiler) {
  compiler.plugin('emit', function(compilation, callback) {
    var filelist = 'In this build:\n\n';
    //compilation.assets 和 compilation.chunks 前面已經(jīng)說過
    for (var filename in compilation.assets) {
      filelist += ('- '+ filename +'\n');
    }
   //在 compilation.assets 中添加需要的資源
    compilation.assets['filelist.md'] = {
      source: function() {
        return filelist;
      },
      size: function() {
        return filelist.length;
      }
    };
    callback();
  });
};
module.exports = FileListPlugin;
Webpack 的插件類型

插件可以根據(jù)它注冊的事件分成不同的類型缺猛。每一個特定的鉤子函數(shù)決定了它會被如何執(zhí)行园担,比如插件可以分為如下的類型。

同步插件

此時 Tapable 實例通過下面的方式來執(zhí)行插件:

applyPlugins(name: string, args: any...)
//或者
applyPluginsBailResult(name: string, args: any...)

這意味著每一個插件的回調(diào)函數(shù)將會被按照順序依次執(zhí)行(觀察者模式)枯夜,并傳入特定的參數(shù) args弯汰,這是插件的最簡單的格式。很多有用的鉤子函數(shù)如"compile"湖雹、"this-compilation"都期望每一個插件同步執(zhí)行咏闪。下面給出 Webpack 對于 compile 這個鉤子函數(shù)的執(zhí)行方式:

Compiler.prototype.compile = function(callback) {
  self.applyPluginsAsync("before-compile", params, function(err) {
    self.applyPlugins("compile", params);
    //執(zhí)行 compile 階段,同步執(zhí)行插件的方式
    var compilation = self.newCompilation(params);
    self.applyPluginsParallel("make", compilation, function(err) {
      compilation.finish();
      compilation.seal(function(err) {
        self.applyPluginsAsync("after-compile", compilation, function(err) {
        });
      });
    });
  });
};
瀑布流插件

這種類型的插件通過下面的方法來執(zhí)行:

applyPluginsWaterfall(name: string, init: any, args: any...)

此時摔吏,每一個插件都會將前一個插件的返回值作為參數(shù)輸入鸽嫂,并傳入自己的參數(shù),這種插件必須考慮插件的執(zhí)行順序征讲。第一個插件傳入的第二個參數(shù)值為 init据某,而最后一個插件的返回值作為 applyPluginsWaterfall 的返回值。這種插件的模式常用于 Webpack 的模板诗箍,如 ModuleTemplate癣籽、ChunkTemplate仅淑。比如 ModuleTemplate 下就使用了如下的內(nèi)容:

const Template = require("./Template");
module.exports = class ModuleTemplate extends Template {
    constructor(outputOptions) {
        super(outputOptions);
    }
    render(module, dependencyTemplates, chunk) {
        const moduleSource = module.source(dependencyTemplates, this.outputOptions, this.requestShortener);
        const moduleSourcePostModule = this.applyPluginsWaterfall("module", moduleSource, module, chunk, dependencyTemplates);
        const moduleSourcePostRender = this.applyPluginsWaterfall("render", moduleSourcePostModule, module, chunk, dependencyTemplates);
    //1.必須考慮插件的執(zhí)行順序
        return this.applyPluginsWaterfall("package", moduleSourcePostRender, module, chunk, dependencyTemplates);
    }
    updateHash(hash) {
        hash.update("1");
        this.applyPlugins("hash", hash);
    }
};
異步插件

如果插件會被異步執(zhí)行雪标,那么應(yīng)該使用下面的方式來完成:

applyPluginsAsync(name: string, args: any..., callback: (err?: Error) -> void)

此時插件處理函數(shù)調(diào)用的時候會傳入 args 和簽名為 (err?: Error) -> void 的回調(diào)函數(shù)。我們的處理函數(shù)將會按照注冊時候的順序被執(zhí)行恃鞋。而回調(diào)函數(shù) callback() 將會在所有的處理函數(shù)被調(diào)用以后調(diào)用匠童。這種模式常常用于如 "emit"埂材、"run"等鉤子函數(shù)。比如下面的 Compiler 的 run 方法的具體邏輯汤求。

self.applyPluginsAsync("run", self, function(err) {
      if(err) return callback(err);
      self.readRecords(function(err) {
        if(err) return callback(err);
        //2.調(diào)用compile的回調(diào)函數(shù)
        self.compile(function onCompiled(err, compilation) {
         //其他代碼邏輯
        });
      });
    });
異步瀑布流插件

此時所有的插件將會被異步執(zhí)行俏险,同時遵循瀑布流的方式严拒。此時以下面的方式來調(diào)用:

applyPluginsAsyncWaterfall(name: string, init: any, callback: (err: Error, result: any) -> void)

此時插件的回調(diào)函數(shù)在調(diào)用的時候傳入當(dāng)前的值,回調(diào)函數(shù)被調(diào)用的時候會有如下的簽名 (err: Error, nextValue: any) -> void竖独。如果回調(diào)函數(shù)被調(diào)用了糙俗,那么 nextValue 就會成為下一個處理函數(shù)的當(dāng)前值。第一個處理函數(shù)的當(dāng)前值為 init预鬓。當(dāng)所有的處理函數(shù)都執(zhí)行以后巧骚,回調(diào)函數(shù)會傳入最后一個插件的返回值。如果任何一個處理函數(shù)傳入了一個 err格二,那么回調(diào)函數(shù)將會傳入錯誤參數(shù) err劈彪,此時余下的所有的處理函數(shù)都不會被執(zhí)行。這種模式常常用于如 "before-resolve" 或者 "after-resolve"顶猜。

Webpack 插件調(diào)用順序

Webpack 的源碼中經(jīng)常會看到上面說的執(zhí)行插件注冊的方法沧奴,我們給出下面的 seal 方法的部分代碼:

seal(callback) {
  self.applyPlugins0("seal");
  self.applyPlugins0("optimize");
  while(self.applyPluginsBailResult1("optimize-modules-basic", self.modules) ||
    self.applyPluginsBailResult1("optimize-modules", self.modules) ||
    self.applyPluginsBailResult1("optimize-modules-advanced", self.modules));
  self.applyPlugins1("after-optimize-modules", self.modules);
  //這里是 optimize module
  while(self.applyPluginsBailResult1("optimize-chunks-basic", self.chunks) ||
    self.applyPluginsBailResult1("optimize-chunks", self.chunks) ||
    self.applyPluginsBailResult1("optimize-chunks-advanced", self.chunks));
    //這里是 optimize chunk
  self.applyPlugins1("after-optimize-chunks", self.chunks);
  //這里是 optimize tree
  self.applyPluginsAsyncSeries("optimize-tree", self.chunks, self.modules, function sealPart2(err) {
    self.applyPlugins2("after-optimize-tree", self.chunks, self.modules);
    const shouldRecord = self.applyPluginsBailResult("should-record") !== false;
    self.applyPlugins2("revive-modules", self.modules, self.records);
    self.applyPlugins1("optimize-module-order", self.modules);
    self.applyPlugins1("advanced-optimize-module-order", self.modules);
    self.applyPlugins1("before-module-ids", self.modules);
    self.applyPlugins1("module-ids", self.modules);
    self.applyModuleIds();
    self.applyPlugins1("optimize-module-ids", self.modules);
    self.applyPlugins1("after-optimize-module-ids", self.modules);
    self.sortItemsWithModuleIds();
    self.applyPlugins2("revive-chunks", self.chunks, self.records);
    self.applyPlugins1("optimize-chunk-order", self.chunks);
    self.applyPlugins1("before-chunk-ids", self.chunks);
    self.applyChunkIds();
    self.applyPlugins1("optimize-chunk-ids", self.chunks);
    self.applyPlugins1("after-optimize-chunk-ids", self.chunks);
    self.sortItemsWithChunkIds();
    if(shouldRecord)
      self.applyPlugins2("record-modules", self.modules, self.records);
    if(shouldRecord)
      self.applyPlugins2("record-chunks", self.chunks, self.records);
    self.applyPlugins0("before-hash");
    self.createHash();
    self.applyPlugins0("after-hash");
    if(shouldRecord)
      self.applyPlugins1("record-hash", self.records);
    self.applyPlugins0("before-module-assets");
    self.createModuleAssets();
    if(self.applyPluginsBailResult("should-generate-chunk-assets") !== false) {
      self.applyPlugins0("before-chunk-assets");
      self.createChunkAssets();
    }
    self.applyPlugins1("additional-chunk-assets", self.chunks);
    self.summarizeDependencies();
    if(shouldRecord)
      self.applyPlugins2("record", self, self.records);
    self.applyPluginsAsync("additional-assets", err => {
      if(err) {
        return callback(err);
      }
      self.applyPluginsAsync("optimize-chunk-assets", self.chunks, err => {
        if(err) {
          return callback(err);
        }
        self.applyPlugins1("after-optimize-chunk-assets", self.chunks);
        self.applyPluginsAsync("optimize-assets", self.assets, err => {
          if(err) {
            return callback(err);
          }
          self.applyPlugins1("after-optimize-assets", self.assets);
          if(self.applyPluginsBailResult("need-additional-seal")) {
            self.unseal();
            return self.seal(callback);
          }
          return self.applyPluginsAsync("after-seal", callback);
        });
      });
    });
  });
}

而各個鉤子函數(shù)執(zhí)行的順序可以查看下面的內(nèi)容:

'before run'
  'run'
    compile:func//調(diào)用 compile() 函數(shù)
        'before compile'
           'compile'//(1)compiler 對象的第一階段
               newCompilation:object//創(chuàng)建 compilation 對象
               'make' //(2)compiler 對象的第二階段 
                    compilation.finish:func
                       "finish-modules"
                    compilation.seal
                         "seal"
                         "optimize"
                         "optimize-modules-basic"
                         "optimize-modules-advanced"
                         "optimize-modules"
                         "after-optimize-modules"http://首先是優(yōu)化模塊
                         "optimize-chunks-basic"
                         "optimize-chunks"http://然后是優(yōu)化 chunk
                         "optimize-chunks-advanced"
                         "after-optimize-chunks"
                         "optimize-tree"
                            "after-optimize-tree"
                            "should-record"
                            "revive-modules"
                            "optimize-module-order"
                            "advanced-optimize-module-order"
                            "before-module-ids"
                            "module-ids"http://首先優(yōu)化 module-order,然后優(yōu)化 module-id
                            "optimize-module-ids"
                            "after-optimize-module-ids"
                            "revive-chunks"
                            "optimize-chunk-order"
                            "before-chunk-ids"http://首先優(yōu)化 chunk-order长窄,然后 chunk-id
                            "optimize-chunk-ids"
                            "after-optimize-chunk-ids"
                            "record-modules"http://record module 然后 record chunk
                            "record-chunks"
                            "before-hash"
                               compilation.createHash//func
                                 "chunk-hash"http://webpack-md5-hash
                            "after-hash"
                            "record-hash"http://before-hash/after-hash/record-hash
                            "before-module-assets"
                            "should-generate-chunk-assets"
                            "before-chunk-assets"
                            "additional-chunk-assets"
                            "record"
                            "additional-assets"
                                "optimize-chunk-assets"
                                   "after-optimize-chunk-assets"
                                   "optimize-assets"
                                      "after-optimize-assets"
                                      "need-additional-seal"
                                         unseal:func
                                           "unseal"
                                      "after-seal"
                    "after-compile"http://(4)完成模塊構(gòu)建和編譯過程(seal 函數(shù)回調(diào))    
    "emit"http://(5)compile 函數(shù)的回調(diào)滔吠,compiler 開始輸出 assets,是改變 assets 最后機會
    "after-emit"http://(6)文件產(chǎn)生完成
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挠日,一起剝皮案震驚了整個濱河市疮绷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嚣潜,老刑警劉巖冬骚,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異懂算,居然都是意外死亡只冻,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門计技,熙熙樓的掌柜王于貴愁眉苦臉地迎上來喜德,“玉大人,你說我怎么就攤上這事垮媒∩崦酰” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵涣澡,是天一觀的道長贱呐。 經(jīng)常有香客問我丧诺,道長入桂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任驳阎,我火速辦了婚禮抗愁,結(jié)果婚禮上馁蒂,老公的妹妹穿的比我還像新娘。我一直安慰自己蜘腌,他們只是感情好沫屡,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著撮珠,像睡著了一般沮脖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上芯急,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天勺届,我揣著相機與錄音,去河邊找鬼娶耍。 笑死免姿,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的榕酒。 我是一名探鬼主播胚膊,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼想鹰!你這毒婦竟也來了紊婉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤辑舷,失蹤者是張志新(化名)和其女友劉穎肩榕,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惩妇,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡株汉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了歌殃。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乔妈。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖氓皱,靈堂內(nèi)的尸體忽然破棺而出路召,到底是詐尸還是另有隱情,我是刑警寧澤波材,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布股淡,位于F島的核電站,受9級特大地震影響廷区,放射性物質(zhì)發(fā)生泄漏唯灵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一隙轻、第九天 我趴在偏房一處隱蔽的房頂上張望埠帕。 院中可真熱鬧垢揩,春花似錦、人聲如沸敛瓷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽呐籽。三九已至锋勺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間狡蝶,已是汗流浹背宙刘。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留牢酵,地道東北人悬包。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像馍乙,于是被迫代替她去往敵國和親布近。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容