写一个 Webpack 插件

通过前面章节内容的讲解,对于 Webpack 的插件应该已经不陌生了,而且对于 Webpack 很多高级的知识点应该都有了一定的了解,包括 Webpack 中的 Compiler 和 Compilation 对象,以及 Webpack 的插件原理。

在本章节中,主要以官网提供的例子 FileListPlugin/HelloWorldPlugin 来说明如何写一个插件,而这部分内容在前面应该已经都有了深入的了解。同时,在本章节中也会给出 Webpack 中不同插件的类型与区别。但是,如果想要写一个自己的 Webpack 的复杂插件,那么除了前面的内容以外,也要注意日常的积累��。

如何写一个 Webpack 的插件

Webpack 的插件机制将 Webpack 引擎的能力暴露给了开发者,使用 Webpack 内置的各种打包阶段钩子函数使得开发者能够引入他们自己的打包流程。写一个 Webpack 插件往往比写一个 Loader 复杂,因为需要了解 Webpack 内部很多细节的部分。

如何创建一个 Webpack 的插件

通过前面的章节内容应该有所了解,一个 Webpack 的插件其实包含以下几个条件:

1、一个 js 命名函数。
2、在原型链上存在一个 apply 方法。
3、为该插件指定一个 Webpack 的事件钩子函数。
4、使用 Webpack 内部的实例对象(Compiler 或者 Compilation)具有的属性或者方法。
5、当功能完成以后,需要执行 Webpack 的回调函数。

比如下面的函数就具备了上面的条件,所以它是可以作为一个 Webpack 插件的:

function MyExampleWebpackPlugin() {
};
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
  //我们主要关注 compilation 阶段,即 webpack 打包阶段
  compiler.plugin('compilation', function(compilation , callback) {
    console.log("This is an example plugin!!!");
    //当该插件功能完成以后一定要注意回调 callback 函数
    callback();
  });
};
Compiler 和 Compilation 实例

在前面的章节中已经深入讲解了这部分的内容,我们下面总结性的给出两个对象的作用。

Compiler 对象: Compiler 对象代表了 Webpack 完整的可配置的环境。该对象在 Webpack 启动的时候会被创建,同时该对象也会被传入一些可控的配置,如 Options、Loaders、Plugins。当插件被实例化的时候,会收到一个 Compiler 对象,通过这个对象可以访问 Webpack 的内部环境。
Compilation 对象: Compilation 对象在每次文件变化的时候都会被创建,因此会重新产生新的打包资源。该对象表示本次打包的模块、编译的资源、文件改变和监听的依赖文件的状态。而且该对象也会提供很多的回调点,我们的插件可以使用它来完成特定的功能,而提供的钩子函数在前面的章节已经讲过了,此处不再赘述

Hello World 插件

比如下面是我们写的一个插件:

//插件内部可以接受到该插件的配置参数
function HelloWorldPlugin(options) {
}
HelloWorldPlugin.prototype.apply = function(compiler) {
  //此处利用了 Compiler 提供的 done 钩子函数,作用前面已经说过
  compiler.plugin('done', function() {
    console.log('Hello World!');
  });
};
module.exports = HelloWorldPlugin;

那么在 Webpack 配置文件中就可以通过下面的方式来进行配置:

var HelloWorldPlugin = require('hello-world');
//已经发布到 NPM
var webpackConfig = {
  plugins: [
    new HelloWorldPlugin({options: true})
  ]
};

前面已经说过,Webpack 插件最重要的就是 Compilation 和 Compiler 对象。先来看看在插件里面如何使用 Compilation 对象:

function HelloCompilationPlugin(options) {}

HelloCompilationPlugin.prototype.apply = function(compiler) {
  //使用 Compiler 对象的 compilation 钩子函数就可以获取 Compilation 对象
  compiler.plugin("compilation", function(compilation) {
   //使用 Compilation 注册回调
    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 参数,当插件完成相应的功能以后,必须回调 callback() 函数。
当访问到 Webpack 的 Compiler 和每次产生的 Compilation 对象的时候,可以使用 Webpack 的引擎来完成任何事情。可以重新处理已经存在的文件,创建自己的派生文件(想要多产生的文件),或者对将要产生的资源进行修改(HtmlWebpackPlugin)等等。例如,在前面章节就已经讲述的下面的实例,该实例就是有效的利用了 Compiler 的文件输出 emit 阶段产生我们自己需要的文件:

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 前面已经说过
    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 的插件类型

插件可以根据它注册的事件分成不同的类型。每一个特定的钩子函数决定了它会被如何执行,比如插件可以分为如下的类型。

同步插件

此时 Tapable 实例通过下面的方式来执行插件:

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

这意味着每一个插件的回调函数将会被按照顺序依次执行(观察者模式),并传入特定的参数 args,这是插件的最简单的格式。很多有用的钩子函数如"compile"、"this-compilation"都期望每一个插件同步执行。下面给出 Webpack 对于 compile 这个钩子函数的执行方式:

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

这种类型的插件通过下面的方法来执行:

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

此时,每一个插件都会将前一个插件的返回值作为参数输入,并传入自己的参数,这种插件必须考虑插件的执行顺序。第一个插件传入的第二个参数值为 init,而最后一个插件的返回值作为 applyPluginsWaterfall 的返回值。这种插件的模式常用于 Webpack 的模板,如 ModuleTemplate、ChunkTemplate。比如 ModuleTemplate 下就使用了如下的内容:

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.必须考虑插件的执行顺序
        return this.applyPluginsWaterfall("package", moduleSourcePostRender, module, chunk, dependencyTemplates);
    }
    updateHash(hash) {
        hash.update("1");
        this.applyPlugins("hash", hash);
    }
};
异步插件

如果插件会被异步执行,那么应该使用下面的方式来完成:

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

此时插件处理函数调用的时候会传入 args 和签名为 (err?: Error) -> void 的回调函数。我们的处理函数将会按照注册时候的顺序被执行。而回调函数 callback() 将会在所有的处理函数被调用以后调用。这种模式常常用于如 "emit"、"run"等钩子函数。比如下面的 Compiler 的 run 方法的具体逻辑。

self.applyPluginsAsync("run", self, function(err) {
      if(err) return callback(err);
      self.readRecords(function(err) {
        if(err) return callback(err);
        //2.调用compile的回调函数
        self.compile(function onCompiled(err, compilation) {
         //其他代码逻辑
        });
      });
    });
异步瀑布流插件

此时所有的插件将会被异步执行,同时遵循瀑布流的方式。此时以下面的方式来调用:

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

此时插件的回调函数在调用的时候传入当前的值,回调函数被调用的时候会有如下的签名 (err: Error, nextValue: any) -> void。如果回调函数被调用了,那么 nextValue 就会成为下一个处理函数的当前值。第一个处理函数的当前值为 init。当所有的处理函数都执行以后,回调函数会传入最后一个插件的返回值。如果任何一个处理函数传入了一个 err,那么回调函数将会传入错误参数 err,此时余下的所有的处理函数都不会被执行。这种模式常常用于如 "before-resolve" 或者 "after-resolve"。

Webpack 插件调用顺序

Webpack 的源码中经常会看到上面说的执行插件注册的方法,我们给出下面的 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);
        });
      });
    });
  });
}

而各个钩子函数执行的顺序可以查看下面的内容:

'before run'
  'run'
    compile:func//调用 compile() 函数
        'before compile'
           'compile'//(1)compiler 对象的第一阶段
               newCompilation:object//创建 compilation 对象
               'make' //(2)compiler 对象的第二阶段 
                    compilation.finish:func
                       "finish-modules"
                    compilation.seal
                         "seal"
                         "optimize"
                         "optimize-modules-basic"
                         "optimize-modules-advanced"
                         "optimize-modules"
                         "after-optimize-modules"//首先是优化模块
                         "optimize-chunks-basic"
                         "optimize-chunks"//然后是优化 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"//首先优化 module-order,然后优化 module-id
                            "optimize-module-ids"
                            "after-optimize-module-ids"
                            "revive-chunks"
                            "optimize-chunk-order"
                            "before-chunk-ids"//首先优化 chunk-order,然后 chunk-id
                            "optimize-chunk-ids"
                            "after-optimize-chunk-ids"
                            "record-modules"//record module 然后 record chunk
                            "record-chunks"
                            "before-hash"
                               compilation.createHash//func
                                 "chunk-hash"//webpack-md5-hash
                            "after-hash"
                            "record-hash"//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"//(4)完成模块构建和编译过程(seal 函数回调)    
    "emit"//(5)compile 函数的回调,compiler 开始输出 assets,是改变 assets 最后机会
    "after-emit"//(6)文件产生完成
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容