引言

围绕 Webpack 打包流程中最外围的机制就是所谓的 Plugin 机制。

所谓插件即是 webpack 生态中最要害的局部, 它为社区用户提供了一种强有力的形式来间接涉及 webpack 的编译过程(compilation process)。

明天,咱们来聊聊 Webpack 中必不可少的外围 Plugin 机制 ~

Plugin

实质上在 Webpack 编译阶段会为各个编译对象初始化不同的 Hook ,开发者能够在本人编写的 Plugin 中监听到这些 Hook ,在打包的某个特定时间段触发对应 Hook 注入特定的逻辑从而实现本人的行为。

对于 Plugin 中的 Hook 外部齐全是基于 tapable 来实现

Plugin 中的罕用对象

首先让咱们先来看看 Webpack 中哪些对象能够注册 Hook :

  • compiler Hook
  • compilation Hook
  • ContextModuleFactory Hook
  • JavascriptParser Hooks
  • NormalModuleFactory Hooks
别放心,兴许对于这 5 个对象当初你会感觉到十分生疏,之后我会逐渐带你攻克它们。

插件的根本形成

咱们先来看这样一个最简略的插件,它会在 compilation(编译)实现时执行输入 done :

class DonePlugin {  apply(compiler) {    // 调用 Compiler Hook 注册额定逻辑    compiler.hooks.done.tap('Plugin Done', () => {      console.log('compilation done');    });  }}module.exports = DonePlugin;

此时,在 compilation 实现时打包终端会打印进去一行 compilation done

咱们能够看到一个 Webpack Plugin 次要由以下几个方面组成:

  • 首先一个 Plugin 应该是一个 class,当然也能够是一个函数。
  • 其次 Plugin 的原型对象上应该存在一个 apply 办法,当 webpack 创立 compiler 对象时会调用各个插件实例上的 apply 办法并且传入 compiler 对象作为参数。
  • 同时须要指定一个绑定在 compiler 对象上的 Hook , 比方 compiler.hooks.done.tap 在传入的 compiler 对象上监听 done 事件。
  • 在 Hook 的回调中解决插件本身的逻辑,这里咱们简略的做了 console.log。
  • 依据 Hook 的品种,在实现逻辑后告诉 webpack 持续进行。

插件的构建对象

上边咱们有提到过 Webpack Plugin 中哪些对应能够进行 Hook 注册,接下来我会带你深刻这 5 个对象。

了解它们是了解并利用 Webpack Plugin 的重中之重。

compiler 对象

class DonePlugin {  apply(compiler) {    // 调用 Compiler Hook 注册额定逻辑    compiler.hooks.done.tapAsync('Plugin Done', (stats, callback) => {      console.log(compiler, 'compiler 对象');    });  }}module.exports = DonePlugin;

在 compiler 对象中保留着残缺的 Webpack 环境配置,它通过 CLI 或 者 Node API传递的所有选项创立出一个 compilation 实例。

这个对象会在首次启动 Webpack 时创立,咱们能够通过 compiler 对象上拜访到 Webapck 的主环境配置,比方 loader 、 plugin 等等配置信息。

compiler 你能够认为它是一个单例,每次启动 webpack 构建时它都是一个举世无双,仅仅会创立一次的对象。

对于 compiler 对象存在以下几个次要属性:

  • 通过 compiler.options , 咱们能够拜访编译过程中 webpack 的残缺配置信息。

在 compiler.options 对象中存储着本次启动 webpack 时候所有的配置文件,包含但不限于 loaders 、 entry 、 output 、 plugin 等等残缺配置信息。

  • 通过 compiler.inputFileSystem(获取文件相干 API 对象)、outputFileSystem(输入文件相干 API 对象) 能够帮忙咱们实现文件操作,你能够将它简略的了解为 Node Api 中的 fs 模块的拓展。

如果咱们心愿自定义插件的一些输入输出行为可能跟 webpack 尽量同步,那么最好应用 compiler 提供的这两个变量。

须要额定留神的是当 compiler 对象运行在 watch 模式通常是 devServer 下,outputFileSystem 会被重写成内存输入对象,换句话来说也就是在 watch 模式下 webpack 构建并非生成真正的文件而是保留在了内存中。

如果你的插件对于文件操作存在对应的逻辑,那么接下里请应用 compiler.inputFileSystem/outputFileSystem 更换掉代码中的 fs 吧。
  • 同时 compiler.hooks 中也保留了扩大了来自 tapable 的不同品种 Hook ,监听这些 Hook 从而能够在 compiler 生命周期中植入不同的逻辑。

对于 compiler 对象的属性你能够在 webpack/lib/Compiler.js中进行查看所有属性。

compilation 对象

class DonePlugin {  apply(compiler) {    compiler.hooks.afterEmit.tapAsync(      'Plugin Done',      (compilation, callback) => {        console.log(compilation, 'compilation 对象');      }    );  }}module.exports = DonePlugin;

所谓 compilation 对象代表一次资源的构建,compilation 实例可能拜访所有的模块和它们的依赖。

一个 compilation 对象会对构建依赖图中所有模块,进行编译。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和从新创立(restore)。

在 compilation 对象中咱们能够获取/操作本次编译以后模块资源、编译生成资源、变动的文件以及被跟踪的状态信息,同样 compilation 也基于 tapable 拓展了不同机会的 Hook 回调。

简略来说比方在 devServer 下每次批改代码都会进行从新编译,此时你能够了解为每次构建都会创立一个新的 compilation 对象。

对于 compilation 对象存在以下几个次要属性:

  • modules

它的值是一个 Set 类型,对于 modules 。简略来说你能够认为一个文件就是一个模块,无论你应用 ESM 还是 Commonjs 编写你的文件。每一个文件都能够被了解成为一个独立的 module。

  • chunks

所谓 chunk 即是多个 modules 组成而来的一个代码块,当 Webapck 进行打包时会首先依据我的项目入口文件剖析对应的依赖关系,将入口依赖的多个 modules 组合成为一个大的对象,这个对象即可被称为 chunk 。

所谓 chunks 当然是多个 chunk 组成的一个 Set 对象。

  • assets

assets 对象上记录了本次打包生成所有文件的后果。

  • hooks

同样在 compilation 对象中基于 tapable 提供给一系列的 Hook ,用于在 compilation 编译模块阶段进行逻辑增加以及批改。

在 Webpack 5 之后提供了一系列 compilation API 代替间接操作 moduels/chunks/assets 等属性,从而提供给开发者来操作对应 API 影响打包后果。

比方一些常见的输入文件工作,当初应用 compilation.emitAsset API 来代替间接操作 compilation.assets 对象。

ContextModuleFactory Hook

class DonePlugin {  apply(compiler) {    compiler.hooks.contextModuleFactory.tap(      'Plugin',      (contextModuleFactory) => {        // 在 require.context 解析申请的目录之前调用该 Hook        // 参数为须要解析的 Context 目录对象        contextModuleFactory.hooks.beforeResolve.tapAsync(          'Plugin',          (data, callback) => {            console.log(data, 'data');            callback();          }        );      }    );  }}module.exports = DonePlugin;

compiler.hooks 对象上同样存在一个 contextModuleFactory ,它同样是基于 tapable 进行衍生了一些列的 hook 。

contextModuleFactory 提供了一些列的 hook ,正如其名称那样它次要用来应用 Webpack 独有 API require.context 解析文件目录时候进行解决。

对于 ContextModuleFactory 系列的 Hook 不是特地罕用

参考webpack视频解说:进入学习

NormalModuleFactory Hook

class DonePlugin {  apply(compiler) {    compiler.hooks.normalModuleFactory.tap(      'MyPlugin',      (NormalModuleFactory) => {        NormalModuleFactory.hooks.beforeResolve.tap(          'MyPlugin',          (resolveData) => {            console.log(resolveData, 'resolveData');            // 仅仅解析目录为./src/index.js 疏忽其余引入的模块            return resolveData.request === './src/index.js';          }        );      }    );  }}module.exports = DonePlugin;

Webpack compiler 对象中通过 NormalModuleFactory 模块生成各类模块。

换句话来说,从入口文件开始,NormalModuleFactory 会合成每个模块申请,解析文件内容以查找进一步的申请,而后通过合成所有申请以及解析新的文件来爬取全副文件。在最初阶段,每个依赖项都会成为一个模块实例。

咱们能够通过 NormalModuleFactory Hook 来注入 Plugin 逻辑从而管制 Webpack 中对于默认模块援用时的解决,比方 ESM、CJS 等模块引入前后时注入对应逻辑。

对于 NormalModuleFactory Hook 能够用于在 Plugin 中解决 Webpack 解析模块时注入特定的逻辑从而影影响打包时的模块引入内容

JavascriptParser Hook

const t = require('@babel/types');const g = require('@babel/generator').default;const ConstDependency = require('webpack/lib/dependencies/ConstDependency');class DonePlugin {  apply(compiler) {    // 解析模块时进入    compiler.hooks.normalModuleFactory.tap('pluginA', (factory) => {      // 当应用javascript/auto解决模块时会调用该hook      const hook = factory.hooks.parser.for('javascript/auto');      // 注册      hook.tap('pluginA', (parser) => {        parser.hooks.statementIf.tap('pluginA', (statementNode) => {          const { code } = g(t.booleanLiteral(false));          const dep = new ConstDependency(code, statementNode.test.range);          dep.loc = statementNode.loc;          parser.state.current.addDependency(dep);          return statementNode;        });      });    });  }}module.exports = DonePlugin;

上边咱们提到了 compiler.normalModuleFactory 钩子用于 Webpack 对于解析模块时候触发,而 JavascriptParser Hook 正是基于模块解析生成 AST 节点时注入的 Hook 。

webpack应用 Parser 对每个模块进行解析,咱们能够在 Plugin 中注册 JavascriptParser Hook 在 Webpack 对于模块解析生成 AST 节点时增加额定的逻辑。

上述的 DonePlugin 会将模块中所有的 statementIf 节点的判断表达式批改称为 false 。

结尾

Webpack Plugin 的外围机制就是基于 tapable 产生的公布订阅者模式,在不同的周期触发不同的 Hook 从而影响最终的打包后果。

其实乍一看很多文章中很多概念,而且对于 Webpack 文档确实很多中央也没有进行欠缺的补充,然而回过头来认真梳理一下。

你感觉到生疏的仅仅是文章中列举进去的 API 而已,文章的目标并不是心愿通过短短几千字你能够具体把握 Webpack Plugin 的各种开发方式,而是在于让你对于 Plugin 机制和开发用法有一个简短的理解和概念。

之后我会在专栏中补充一些 Plugin 的实战开发,真正带大家领略开源插件我的项目中是如何在这些看似系统的常识中化零为整,成为真正投身于业务之中的企业应用。