寫一個 Webpack 插件

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

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

如何寫一個 Webpack 的插件

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

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

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

1、一個 js 命名函數(shù)。
2、在原型鏈上存在一個 apply 方法。
3、為該插件指定一個 Webpack 的事件鉤子函數(shù)。
4、使用 Webpack 內(nèi)部的實例對象(Compiler 或者 Compilation)具有的屬性或者方法。
5、當功能完成以后,需要執(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!!!");
    //當該插件功能完成以后一定要注意回調(diào) callback 函數(shù)
    callback();
  });
};
Compiler 和 Compilation 實例

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

Compiler 對象: Compiler 對象代表了 Webpack 完整的可配置的環(huán)境。該對象在 Webpack 啟動的時候會被創(chuàng)建,同時該對象也會被傳入一些可控的配置,如 Options、Loaders、Plugins。當插件被實例化的時候,會收到一個 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ù),當插件完成相應的功能以后,必須回調(diào) callback() 函數(shù)。
當訪問到 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í)行,那么應該使用下面的方式來完成:

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)用的時候傳入當前的值,回調(diào)函數(shù)被調(diào)用的時候會有如下的簽名 (err: Error, nextValue: any) -> void。如果回調(diào)函數(shù)被調(diào)用了,那么 nextValue 就會成為下一個處理函數(shù)的當前值。第一個處理函數(shù)的當前值為 init。當所有的處理函數(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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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

  • 你真的來了。 兩年零兩個月了,我記得我們相識的點點滴滴,如今你是真的來了。 我有些忐忑,網(wǎng)絡的友誼會不會跟現(xiàn)實一樣...
    湍河故事閱讀 296評論 2 2
  • 我們生活在一個數(shù)字化的時代,電腦、智能手機、平板電腦等都已經(jīng)成為我們生活中不可或缺的工具,這些電子產(chǎn)品已經(jīng)和我們的...
    設計獅媽咪閱讀 721評論 9 2
  • 上周開始,牙齦又開始隱隱作痛。沒想太多,以為是最近吃的不太營養(yǎng)長了蛀牙。直到這周開始,牙齦的痛感又開始襲來,我才后...
    中二文藝女青年龍琪琪閱讀 1,811評論 1 1
  • 感賞昨天我的兒子和女兒陪著我去運動、購物和看電影。左手挽著兒子,右手牽著女兒,走在路上的感覺特好。在電影院放映前,...
    旦子閱讀 184評論 0 0

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