chst365's blog chst365's blog
首页
  • Git
  • 网络
  • 操作系统
  • 浏览器
  • webpack
  • JavaScript
  • TypeScript
  • 性能
  • 工程化
  • React
  • 编程题
  • React技术揭秘
  • 算法
  • Node
  • 编码解码
  • NodeJS系列
  • Linux系列
  • JavaScript系列
  • HTTP系列
  • GIT系列
  • ES6系列
  • 设计模式系列
  • CSS系列
  • 小程序系列
  • 数据结构与算法系列
  • React系列
  • Vue3系列
  • Vue系列
  • TypeScript系列
  • Webpack系列
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

chst365

DIV工程师
首页
  • Git
  • 网络
  • 操作系统
  • 浏览器
  • webpack
  • JavaScript
  • TypeScript
  • 性能
  • 工程化
  • React
  • 编程题
  • React技术揭秘
  • 算法
  • Node
  • 编码解码
  • NodeJS系列
  • Linux系列
  • JavaScript系列
  • HTTP系列
  • GIT系列
  • ES6系列
  • 设计模式系列
  • CSS系列
  • 小程序系列
  • 数据结构与算法系列
  • React系列
  • Vue3系列
  • Vue系列
  • TypeScript系列
  • Webpack系列
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 浏览器

  • webpack

  • TypeScript

  • 性能

  • 工程化

    • webpack-loader机制
    • webpack-插件机制
      • Tapable
        • Tapable 是什么
        • Tapable 的使用
        • Tapable 的源码分析
      • compile
        • compile 是什么
        • compile 的内部实现
        • compilation
        • 编写一个插件
        • compiler 和 compilation 一些比较重要的事件钩子
      • 总结
    • webpack工作流程
    • Babel原理
    • uglify-js代码是如何被压缩的
    • Tree-Shaking性能优化
    • 前端模块化机制
    • 前端微服务
    • Vite为什么快?快在哪?
    • npm script了解多少?
  • React

  • JavaScript

  • 编程题

  • React技术揭秘

  • 算法

  • 前端
  • 工程化
chst365
2021-04-25
目录

webpack-插件机制

# Tapable

webpack 的插件机制依赖于一个核心的库,Tapable。

# Tapable 是什么

tapable 是一个类似于 nodejs 的 EventEmitter 的库,主要控制钩子函数的发布与订阅。tapable 提供的 hook 机制比较全面,分为同步和异步两大类(异步中又区分异步并行和异步串行),而根据事件执行的终止条件的不同,由衍生出 Bail/Waterfall/Loop 类型。

# Tapable 的使用

# 基本使用

const { SyncHook } = require("tapable");

// 创建一个同步 Hook,指定参数
const hook = new SyncHook(["arg1", "arg2"]);

// 注册
hook.tap("a", function (arg1, arg2) {
  console.log("a");
});

hook.tap("b", function (arg1, arg2) {
  console.log("b");
});

hook.call(1, 2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 钩子类型

BasicHook: 执行每一个,不关心函数的返回值,有SyncHook、AsyncParallelHook、AsyncSeriesHook。 BailHook:顺序执行 Hook,遇到第一个结果result!==undefined则返回,不再继续执行。有SyncBailHook、AsyncSeriseBailHook,AsyncParallelBailHook。

什么样的场景下会使用 BailHook 呢?设想如下一个例子:假设我们有个模块 M,如果它满足 A 或 B 或 C 三者任何一个条件,就将其打包为一个单独的。这里的 A/B/C 不存在先后顺序,那么就可以使用 AsyncParallelBailHook 来解决

x.hooks.拆分模块的Hook.tap('A', () => {
   if (A 判断条件满足) {
     return true
   }
 })
 x.hooks.拆分模块的Hook.tap('B', () => {
   if (B 判断条件满足) {
     return true
   }
 })
 x.hooks.拆分模块的Hook.tap('C', () => {
   if (C 判断条件满足) {
     return true
   }
 })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如果 A 中返回为 true,那么就无须再去判断 B 和 C。 但是当 A、B、C 的校验,需要严格遵循先后顺序时,就需要使用有顺序的 SyncBailHook(A、B、C 是同步函数时使用) 或者 AsyncSeriseBailHook(A、B、C 是异步函数时使用)。

WaterfillHook:类似于 reduce,如果一个 Hook 函数的结果 result !== undefined,则 result 会作为后一个 Hook 函数的第一个参数。既然是顺序执行,那么就只有 Sync 和 AsyncSeries 类中提供这个 Hook:SyncWaterfallHook,AsyncSeriesWaterfallHook 当一个数据,需要经过 A,B,C 三个阶段的处理得到最终结果,并且 A 中如果满足条件 a 就处理,否则不处理,B 和 C 同样,那么可以使用如下

x.hooks.tap('A', (data) => {
   if (满足 A 需要处理的条件) {
     // 处理数据 data
     return data
   } else {
     return
   }
 })
x.hooks.tap('B', (data) => {
   if (满足B需要处理的条件) {
     // 处理数据 data
     return data
   } else {
     return
   }
 })
 x.hooks.tap('C', (data) => {
   if (满足 C 需要处理的条件) {
     // 处理数据 data
     return data
   } else {
     return
   }
 })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

LoopHook:不停的循环执行 Hook,直到所有函数结果 result === undefined。同样的,由于对串行性有依赖,所以只有 SyncLoopHook和 AsyncSeriseLoopHook

# Tapable 的源码分析

Tapable 基本逻辑是,先通过类实例的 tap 方法注册对应 Hook 的处理函数。 const hook = new SyncHook(['arg1', 'arg2']) 从该句代码,作为源码分析的入口

class SyncHook extends Hook {
  // 错误处理,防止调用者调用异步钩子
  tapAsync() {
    throw new Error("tapAsync is not supported on a SyncHook");
  }
  // 错误处理,防止调用者调用promise钩子
  tapPromise() {
    throw new Error("tapPromise is not supported on a SyncHook");
  }
  // 核心实现
  compile(options) {
    factory.setup(this, options);
    return factory.create(options);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

从 SyncHook 看到,他是继承于一个基类 Hook,他的核心实现 complie ,我们先看看基类 Hook

// 变量的初始化
constructor(args) {
	if (!Array.isArray(args)) args = [];
	this._args = args;
	this.taps = [];
	this.interceptors = [];
	this.call = this._call;
	this.promise = this._promise;
	this.callAsync = this._callAsync;
	this._x = undefined;
}
1
2
3
4
5
6
7
8
9
10
11

初始化完成后,通常会注册一个事件,如

// 注册
hook.tap("a", function (arg1, arg2) {
  console.log("a");
});

hook.tap("b", function (arg1, arg2) {
  console.log("b");
});
1
2
3
4
5
6
7
8

这两个语句都会调用基类中的 tap 方法

tap(options, fn) {
    // 参数处理
	if (typeof options === "string") options = { name: options };
	if (typeof options !== "object" || options === null)
		throw new Error(
			"Invalid arguments to tap(options: Object, fn: function)"
		);
	options = Object.assign({ type: "sync", fn: fn }, options);
	if (typeof options.name !== "string" || options.name === "")
		throw new Error("Missing name for tap");
	// 执行拦截器的register函数, 比较简单不分析
	options = this._runRegisterInterceptors(options);
	// 处理注册事件
	this._insert(options);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

从上面的源码分析,可看到_insert 方法是注册阶段的关键函数,直接进入该方法内部

_insert(item) {
    // 重置所有的 调用 方法
	this._resetCompilation();
	// 将注册事件排序后放进taps数组
	let before;
	if (typeof item.before === "string") before = new Set([item.before]);
	else if (Array.isArray(item.before)) {
		before = new Set(item.before);
	}
	let stage = 0;
	if (typeof item.stage === "number") stage = item.stage;
	let i = this.taps.length;
	while (i > 0) {
		i--;
		const x = this.taps[i];
		this.taps[i + 1] = x;
		const xStage = x.stage || 0;
		if (before) {
			if (before.has(x.name)) {
				before.delete(x.name);
				continue;
			}
			if (before.size > 0) {
				continue;
			}
		}
		if (xStage > stage) {
			continue;
		}
		i++;
		break;
	}
	this.taps[i] = item;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

_insert 主要是排序 tap 并放入到 taps 数组里,排序的算法并不是特别复杂,到这里,注册阶段就已结束,继续看触发阶段。

hook.call(1, 2) // 触发函数

在基类 hook 中,有一个初始化过程

this.call = this._call;

Object.defineProperties(Hook.prototype, {
  _call: {
    value: createCompileDelegate("call", "sync"),
    configurable: true,
    writable: true,
  },
  _promise: {
    value: createCompileDelegate("promise", "promise"),
    configurable: true,
    writable: true,
  },
  _callAsync: {
    value: createCompileDelegate("callAsync", "async"),
    configurable: true,
    writable: true,
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

我们可以看出_call 是由createCompileDelegate生成的,往下看

function createCompileDelegate(name, type) {
  return function lazyCompileHook(...args) {
    this[name] = this._createCall(type);
    return this[name](...args);
  };
}
1
2
3
4
5
6

createCompileDelegate 返回一个名为 lazyCompileHook 的函数,顾名思义,即懒编译, 直到调用 call 的时候, 才会编译出正在的 call 函数。 createCompileDelegate 也是调用的_createCall, 而_createCall 调用了 Compier 函数

_createCall(type) {
	return this.compile({
		taps: this.taps,
		interceptors: this.interceptors,
		args: this._args,
		type: type
	});
}
compile(options) {
	throw new Error("Abstract: should be overriden");
}
1
2
3
4
5
6
7
8
9
10
11

可以看到 compiler 必须由子类重写, 返回到 syncHook 的 compile 函数, 即我们一开始说的核心方法

class SyncHookCodeFactory extends HookCodeFactory {
	content({ onError, onResult, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) => onError(err),
			onDone,
			rethrowIfPossible
		});
	}
}

const factory = new SyncHookCodeFactory();

class SyncHook extends Hook {
    ...
	compile(options) {
		factory.setup(this, options);
		return factory.create(options);
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

关键就在于 SyncHookCodeFactory 和工厂类 HookCodeFactory, 先看 setup 函数,

setup(instance, options) {
  // 这里的instance 是syncHook 实例, 其实就是把tap进来的钩子数组给到钩子的_x属性里.
  instance._x = options.taps.map(t => t.fn);
}
1
2
3
4

然后是最关键的 create 函数, 可以看到最后返回的 fn,其实是一个 new Function 动态生成的函数

create(options) {
  // 初始化参数,保存options到本对象this.options,保存new Hook(["options"]) 传入的参数到 this._args
  this.init(options);
  let fn;
  // 动态构建钩子,这里是抽象层,分同步, 异步, promise
  switch (this.options.type) {
    // 先看同步
    case "sync":
      // 动态返回一个钩子函数
      fn = new Function(
        // 生成函数的参数,no before no after 返回参数字符串 xxx,xxx 在
        // 注意这里this.args返回的是一个字符串,
        // 在这个例子中是options
        this.args(),
        '"use strict";\n' +
          this.header() +
          this.content({
            onError: err => `throw ${err};\n`,
            onResult: result => `return ${result};\n`,
            onDone: () => "",
            rethrowIfPossible: true
          })
      );
      break;
    case "async":
      fn = new Function(
        this.args({
          after: "_callback"
        }),
        '"use strict";\n' +
          this.header() +
          // 这个 content 调用的是子类类的 content 函数,
          // 参数由子类传,实际返回的是 this.callTapsSeries() 返回的类容
          this.content({
            onError: err => `_callback(${err});\n`,
            onResult: result => `_callback(null, ${result});\n`,
            onDone: () => "_callback();\n"
          })
      );
      break;
    case "promise":
      let code = "";
      code += '"use strict";\n';
      code += "return new Promise((_resolve, _reject) => {\n";
      code += "var _sync = true;\n";
      code += this.header();
      code += this.content({
        onError: err => {
          let code = "";
          code += "if(_sync)\n";
          code += `_resolve(Promise.resolve().then(() => { throw ${err}; }));\n`;
          code += "else\n";
          code += `_reject(${err});\n`;
          return code;
        },
        onResult: result => `_resolve(${result});\n`,
        onDone: () => "_resolve();\n"
      });
      code += "_sync = false;\n";
      code += "});\n";
      fn = new Function(this.args(), code);
      break;
  }
  // 把刚才init赋的值初始化为undefined
  // this.options = undefined;
  // this._args = undefined;
  this.deinit();

  return fn;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

最后生成的代码大致如下

"use strict";
function (options) {
  var _context;
  var _x = this._x;
  var _taps = this.taps;
  var _interterceptors = this.interceptors;
// 我们只有一个拦截器所以下面的只会生成一个
  _interceptors[0].call(options);

  var _tap0 = _taps[0];
  _interceptors[0].tap(_tap0);
  var _fn0 = _x[0];
  _fn0(options);
  var _tap1 = _taps[1];
  _interceptors[1].tap(_tap1);
  var _fn1 = _x[1];
  _fn1(options);
  var _tap2 = _taps[2];
  _interceptors[2].tap(_tap2);
  var _fn2 = _x[2];
  _fn2(options);
  var _tap3 = _taps[3];
  _interceptors[3].tap(_tap3);
  var _fn3 = _x[3];
  _fn3(options);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

ok, 以上就是 Tapabled 的机制, 然而本篇的主要对象其实是基于 tapable 实现的 compile 和 compilation 对象。不过由于他们都是基于 tapable,所以介绍的篇幅相对短一点。

# compile

# compile 是什么

compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用 compiler 来访问 webpack 的主环境。 也就是说, compile 是 webpack 的整体环境。

# compile 的内部实现

class Compiler extends Tapable {
  constructor(context) {
    super();
    this.hooks = {
      /** @type {SyncBailHook<Compilation>} */
      shouldEmit: new SyncBailHook(["compilation"]),
      /** @type {AsyncSeriesHook<Stats>} */
      done: new AsyncSeriesHook(["stats"]),
      /** @type {AsyncSeriesHook<>} */
      additionalPass: new AsyncSeriesHook([]),
      /** @type {AsyncSeriesHook<Compiler>} */
      ......
      ......
      some code
    };
    ......
    ......
    some code
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

可以看到, Compier 继承了 Tapable, 并且在实例上绑定了一个 hook 对象, 使得 Compier 的实例 compier 可以像这样使用

compiler.hooks.compile.tapAsync("afterCompile", (compilation, callback) => {
  console.log("This is an example plugin!");
  console.log(
    "Here’s the `compilation` object which represents a single build of assets:",
    compilation
  );

  // 使用 webpack 提供的 plugin API 操作构建结果
  compilation.addModule(/* ... */);

  callback();
});
1
2
3
4
5
6
7
8
9
10
11
12

# compilation

# 什么是 compilation

compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。

# compilation 的实现

class Compilation extends Tapable {
  /**
   * Creates an instance of Compilation.
   * @param {Compiler} compiler the compiler which created the compilation
   */
  constructor(compiler) {
    super();
    this.hooks = {
      /** @type {SyncHook<Module>} */
      buildModule: new SyncHook(["module"]),
      /** @type {SyncHook<Module>} */
      rebuildModule: new SyncHook(["module"]),
      /** @type {SyncHook<Module, Error>} */
      failedModule: new SyncHook(["module", "error"]),
      /** @type {SyncHook<Module>} */
      succeedModule: new SyncHook(["module"]),

      /** @type {SyncHook<Dependency, string>} */
      addEntry: new SyncHook(["entry", "name"]),
      /** @type {SyncHook<Dependency, string, Error>} */
    };
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 编写一个插件

了解了 tapable\compiler\compilation 之后, 再来看插件的实现就不再一头雾水了

class MyExampleWebpackPlugin {
  // 定义 `apply` 方法
  apply(compiler) {
    // 指定要追加的事件钩子函数
    compiler.hooks.compile.tapAsync("afterCompile", (compilation, callback) => {
      console.log("This is an example plugin!");
      console.log(
        "Here’s the `compilation` object which represents a single build of assets:",
        compilation
      );

      // 使用 webpack 提供的 plugin API 操作构建结果
      compilation.addModule(/* ... */);

      callback();
    });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

可以看到其实就是在 apply 中传入一个 Compiler 实例, 然后基于该实例注册事件, compilation 同理, 最后 webpack 会在各流程执行 call 方法。

# compiler 和 compilation 一些比较重要的事件钩子

# compier

事件钩子 触发时机 参数 类型
entry-option 初始化 option - SyncBailHook
run 开始编译 compiler AsyncSeriesHook
compile 真正开始的编译,在创建 compilation 对象之前 compilation SyncHook
compilation 生成好了 compilation 对象,可以操作这个对象啦 compilation SyncHook
make 从 entry 开始递归分析依赖,准备对每个模块进行 build compilation AsyncParallelHook
after-compile 编译 build 过程结束 compilation AsyncSeriesHook
emit 在将内存中 assets 内容写到磁盘文件夹之前 compilation AsyncSeriesHook
after-emit 在将内存中 assets 内容写到磁盘文件夹之后 compilation AsyncSeriesHook
done 完成所有的编译过程 stats AsyncSeriesHook
failed 编译失败的时候 error SyncHook

# compilation

事件钩子 触发时机 参数 类型
normal-module-loader 普通模块 loader,真正(一个接一个地)加载模块图(graph)中所有模块的函数。 loaderContext module SyncHook
seal 编译(compilation)停止接收新模块时触发。 - SyncHook
optimize 优化阶段开始时触发。 - SyncHook
optimize-modules 模块的优化 modules SyncBailHook
optimize-chunks 优化 chunk chunks SyncBailHook
additional-assets 为编译(compilation)创建附加资源(asset)。 - AsyncSeriesHook
optimize-chunk-assets 优化所有 chunk 资源(asset)。 chunks AsyncSeriesHook
optimize-assets 优化存储在 compilation.assets 中的所有资源(asset) assets AsyncSeriesHook

# 总结

插件机制并不复杂,webpack 也不复杂,复杂的是插件本身。

#前端#工程化
上次更新: 2021/12/14, 23:27:23
webpack-loader机制
webpack工作流程

← webpack-loader机制 webpack工作流程→

最近更新
01
面试官
03-27
02
this&指针&作用域&闭包
03-27
03
前端
03-27
更多文章>
Theme by Vdoing | Copyright © 2019-2025 chst365 | 豫ICP备17031889号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式