多种维度解析webpack的plugin实现

1,022 阅读3分钟

序言

  • 今天多种维度来大家解析plugin实现原理。这样可以让大家知道plugin是干什么的,在什么时期来执行plugin是最好的时机
  1. 从源码的角度来分析。2. 从插件html-webpack-plugin的实现原理看分析。3. 从自我实现来分析

多维度分析

① webpack中plugin怎么使用???

图片.png

  • 以上的代码是:用vue-cli来创建vue2的项目,在文件webpack.prod.config.js中书写的
  • 通过上述的实例其实我们知道plugin插件在webpack中的哪个位置进行工作,使用的方式是什么,接下来我们来说下,他是如何在webpack中加载的。

② webpack中plugin如何实现???

compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin({
 infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (options.plugins && Array.isArray(options.plugins)) {
 for (const plugin of options.plugins) {
  if (typeof plugin === "function") {
   // 如果是方法直接调用
   plugin.call(compiler, compiler);
  } else {
   // 一般都是类 类实例一定是对象 直接调用方法apply
   plugin.apply(compiler);
  }
 }
}

以上代码来自webpack4源码,地址是resolve-code, 46 ~60行

  • 通过上述的判断可以得到,如果在options中存在plugins的话,直接进行遍历
  • plugin有两种实现方案,如果是function的话,直接调用。反之如果不是的话调用apply方法。
  • 这里肯定会有很多人疑问???为什么是apply方法呢??首先大家要记住这个apply跟Function.prototype.apply完全是两码事,至于为什么叫apply的话,只能去问webpack作者他命名这个的意义了
  • 至于插件怎么写,我们会在下面进行讲述。

③ html-webpack-plugin是如何实现的呢???

// 插件HtmlWebpackPlugin
class HtmlWebpackPlugin {
  /**
   * @param {HtmlWebpackOptions} [options]
   */
  constructor (options) {
    /** @type {HtmlWebpackOptions} */
    this.userOptions = options || {};
    this.version = HtmlWebpackPlugin.version;
  }

  // 表示调用入口 compiler -- 表示webpack compiler实例
  apply (compiler) {
    // Wait for configuration preset plugions to apply all configure webpack defaults
    // 当编译器对象被初始化时调用
    compiler.hooks.initialize.tap('HtmlWebpackPlugin', () => {
      const userOptions = this.userOptions;

以上的代码来自html-webpack-plugin源码,地址是code-resolve 28 ~ 44行

  • 通过上述源码,我们就可以看到其实这个插件本身是一个类,在类中实现了apply方法
  • 所以在webpack在调用插件的时候,其实调用了实例的apply方法,将自身的编译器实例compiler当参数进行传递

④ 那插件的调用时机是是什么???

// webpack的 编译器
class Compiler extends Tapable {
 constructor(context) {
  super();

  // 通过Tapable 实现各种hook钩子
  // 每个钩子 都有不同的调用时机,可以参照<https://webpack.docschina.org/api/compiler-hooks/#hooks>
  this.hooks = {
   /** @type {SyncBailHook<Compilation>} */
   shouldEmit: new SyncBailHook(["compilation"]),
   /** @type {AsyncSeriesHook<Stats>} */
   done: new AsyncSeriesHook(["stats"]),
   /** @type {AsyncSeriesHook<>} */
   additionalPass: new AsyncSeriesHook([]),
   /** @type {AsyncSeriesHook<Compiler>} */
   beforeRun: new AsyncSeriesHook(["compiler"]),
   /** @type {AsyncSeriesHook<Compiler>} */
   run: new AsyncSeriesHook(["compiler"]),
   /** @type {AsyncSeriesHook<Compilation>} */
   emit: new AsyncSeriesHook(["compilation"]),
   /** @type {AsyncSeriesHook<string, Buffer>} */
   assetEmitted: new AsyncSeriesHook(["file", "content"]),
   /** @type {AsyncSeriesHook<Compilation>} */
   afterEmit: new AsyncSeriesHook(["compilation"]),
...

以上的代码来自webpack4源码,地址是 code-resolve 42~65行

  • 其实想了解触发时机,必须先知道发布订阅模式以及Tapable插件
  • 上述代码的this.hooks 就是发布订阅的实例,每种订阅的类型都不同
  • 那插件是如何订阅的呢,请看第二幅源码(html-webpack-plugin源码)compiler.hooks.initialize.tap('HtmlWebpackPlugin', () => {
  • 这里做下解释,compiler就是this,可以通过initialize.tap进行订阅。
  • 那到底订阅的时机是什么呢,可以参照下官网Compiler
订阅方法描述
environment在编译器准备环境时调用,时机就在配置文件中初始化插件之后
afterEnvironment当编译器环境设置完成后,在 environment hook 后直接调用
initialize当编译器对象被初始化时调用
......
  • 这里大致的意思就是不同时期订阅不同的钩子,代码执行过程中会在合适的时机进行发布

⑤ 如何实现自己的插件呢???

class RunPlugin {
  apply(compiler) {
    compiler.hooks.run.tap('run', () => {
      console.log('已经开始执行了.............')
    })
  }
}

module.exports = RunPlugin
class Compiler {
  constructor(options) {
    this.hooks = {
      run: new SyncHook(),
      done: new SyncHook()
    }
    this.options = options
  }

以上代码的地址是webpack-flow, webpack-flow-Comilper

  • 其实说到这里,我想大家都应该明白webpack-plugin实现原理了吧。但是具体要用plugin做什么事情,需要大家慢慢探索啊

结语

  • 如果有不对的地方希望大家多多指针,大家一起学习一起“卷”
  • 其实小编还写了其他的文章,可以参照个人博客