写一个简单webpack plugin所引发的思考

1,856 阅读5分钟

webpack中有很多钩子,每个钩子都订阅了注册的plugin,等到这个钩子被触发时执行当前钩子订阅的plugin方法,注意webpack钩子会按特定的顺序来执行每一个注册的插件,问题来了,这个顺序规则是怎么实现的呢,webpack又对我们注册的插件做了什么?下面来简单分析下

各位大佬,小生献丑了~~

一个简单地plugin

// /plugins/LogStartBuildtips.js
const pluginName = 'LogStartBuildtips';

class LogStartBuildtips{
  apply(compiler) {
    compiler.hooks.run.tap(pluginName, compiltion => {
      console.log('webpack build starting!!!');
    })
  }
}

module.exports = LogStartBuildtips;

在webpack配置文件注册

const LogStartBuildtips = require('./plugins/LogStartBuildtips')
module.exports = {
  // ......
  plugins:[
    new LogStartBuildtips()
  ]
}

logStartBuildtips这个插件就是在webpack打包中提示正在打包(咳咳,我不瞎)。执行打包,可看到打印的信息,此处略过,,

我们关心这个过程,webpack做了什么?

简单分析plugin机制

那先看看webpack.js

// ./node_modules/webpack/lib/webpack.js
const webpack = (options, callback) => {
	let compiler;
  compiler = new Compiler(options.context);
  compiler.options = options;
  if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === "function") {
        plugin.call(compiler, compiler);
      } else {  // new 一个构造函数 返回的是一个对象
        plugin.apply(compiler);  // 通过apply来注册插件,将插件添加到对应的钩子中
      }
    }
  }
  console.log(compiler.hooks.run); //    <== 在这打印
	return compiler;
};

删减了很多,主要看对插件的处理。插件有两种类型,对象或函数,对于我们常用的插件一般都是插件的实例,所以一般为对象,走else。

plugin.apply(compiler),what?看了插件源码的大佬,不难发现很多插件,都有个apply方法,要它干啥?

我觉的就是webpack给我们暴露的一个接口,apply的执行用来将插件的注册到对应的钩子,apply参数compiler就是webpack完整配置信息,里面也有我们手写的plugin-LogStartBuildtips,我们实现的那个plugin,就是注册到了run这个钩子。有吗?打印,眼见为实

tap-hook

run这个钩子里的taps数组可以看到,该钩子一共被注册了两个plugin,一个是webpack内部使用的插件CachePlugin,另一个就是我们写的插件。

webpack有很多钩子:

// ./node_modules/webpack/lib/Compiler.js
const {
	Tapable,
	SyncHook,
	SyncBailHook,
	AsyncParallelHook,
	AsyncSeriesHook
} = require("tapable");

class Compiler extends Tapable {
	constructor(context) {
		super();
		this.hooks = {
			shouldEmit: new SyncBailHook(["compilation"]),
			done: new AsyncSeriesHook(["stats"]),
			additionalPass: new AsyncSeriesHook([]),
			beforeRun: new AsyncSeriesHook(["compiler"]),
			run: new AsyncSeriesHook(["compiler"]),  // <== 我们的run钩子
			// .....
			afterPlugins: new SyncHook(["compiler"]),
			afterResolvers: new SyncHook(["compiler"]),
			entryOption: new SyncBailHook(["context", "entry"])
		};
    // .....
  }
  // ......
}

这些钩子在Compiler实例创建阶段,就都初始化好了。Compiler继承自Tapable,并且后面每个钩子都会new一个从tapable导出的构造函数,这些构造函数是干什么的?下面来聊聊tapable

tapable

tapable是什么?webpack就是一个事件流,这个事件流是一个个插件组成的,而保证这些插件有序的运行就是tapable的作用,它是采用发布订阅的架构模式。tapable中有很多钩子方法,用来搜集注册的plugin。简单来说Tapable就是webpack用来创建钩子的库。

webpack有很多钩子,在不同的生命周期被触发,通俗的比喻下,webpack整个执行过程就像一个工厂的流水线,Tapable的钩子方法就是这条流水线上的一个个工人,在工程被创建时,给每个工人分配到不同的加工地点(webpack钩子),工人到达地点开始准备(new Tapable钩子方法),接着工人拿到要对产品加工的工具(plugin),等到有产品过来就用这些工具按特定顺序(plugin按规则执行)对其加工,加工完后再放回流水线,给下一个工人(webpack钩子)继续加工,直到所有产品出了流水线(打包完成)。

那看看这些工人的加工规则,tapable提供了9个钩子方法

Ta

写在中间

  • Sync*
    • SyncHook --> 同步串行钩子,不关心返回值
    • SyncBailHook --> 同步串行钩子,如果返回值不为null 则跳过之后的函数
    • SyncLoopHook --> 同步循环,如果返回值为true 则继续执行,返回值为false则跳出循环
    • SyncWaterfallHook --> 同步串行,上一个函数返回值会传给下一个监听函数
  • Async*
    • AsyncParallel*:异步并发
      • AsyncParallelBailHook --> 异步并发,只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数
      • AsyncParallelHook --> 异步并发,不关心返回值
    • AsyncSeries*:异步串行
      • AsyncSeriesHook --> 异步串行,不关心callback()的参数
      • AsyncSeriesBailHook --> 异步串行,callback()的参数不为null,就会忽略后续的函数,直接执行callAsync函数绑定的回调函数
      • AsyncSeriesWaterfallHook --> 异步串行,上一个函数的callback(err, data)的第二个参数会传给下一个监听函数

www.programmersought.com/article/145…,这个网址对每个钩子方法介绍的更详细

webpack插件注册方式:同步插件一般用tap;异步插件有三种tap、tapAsync、tapSync

总结

那webpack对我们注册的插件做了什么?

webpack先创建compiler,初始化其内部的所有钩子,然后会遍历我们在webpack配置文件配置的插件,执行apply(compiler)将我们写的插件方法通过tapable的钩子方法注册到对应的webpack钩子上,等到该钩子在编译时被触发,在按tapable的钩子方法的规则来执行插件的方法。

webpack的插件机制⼤体可以总结为三个步骤:

  1. 创建:webpack在其内部对象上创建各种钩⼦;
  2. 注册:插件通过tapable的钩子方法将⾃⼰的⽅法注册到对应webpack钩⼦上,交给webpack;
  3. 调⽤:webpack编译过程中,会适时地触发相应钩⼦,因此也就触发了插件的⽅法;