系列文章

Webpack系列-第一篇基础杂记
webpack系列-插件机制杂记

前言

webpack本身并不难,他所完成的各种复杂炫酷的功能都依赖于他的插件机制。或许我们在日常的开发需求中并不需要自己动手写一个插件,然而,了解其中的机制也是一种学习的方向,当插件出现问题时,我们也能够自己来定位。

Tapable

Webpack的插件机制依赖于一个核心的库, Tapable
在深入webpack的插件机制之前,需要对该核心库有一定的了解。

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)
复制代码

钩子类型

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}})复制代码

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

WaterfallHook:类似于 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 需要处理的条件) {// 处理数据 datareturn data} else {return}})
x.hooks.tap('B', (data) => {if (满足B需要处理的条件) {// 处理数据 datareturn data} else {return}})x.hooks.tap('C', (data) => {if (满足 C 需要处理的条件) {// 处理数据 datareturn data} else {return}})
复制代码

LoopHook:不停的循环执行 Hook,直到所有函数结果 result === undefined。同样的,由于对串行性有依赖,所以只有 SyncLoopHook 和 AsyncSeriseLoopHook (PS:暂时没看到具体使用 Case)

Tapable的源码分析

Tapable 基本逻辑是,先通过类实例的 tap 方法注册对应 Hook 的处理函数, 这里直接分析sync同步钩子的主要流程,其他的异步钩子和拦截器等就不赘述了。

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);}
}
复制代码

从类SyncHook看到, 他是继承于一个基类Hook, 他的核心实现compile等会再讲, 我们先看看基类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;
}
复制代码

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

// 注册
hook.tap('a', function (arg1, arg2) {console.log('a')
})hook.tap('b', function (arg1, arg2) {console.log('b')
})
复制代码

很明显, 这两个语句都会调用基类中的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);
}
复制代码

从上面的源码分析, 可以看到_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;
}
}
复制代码

_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}
});
复制代码

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

function createCompileDelegate(name, type) {return function lazyCompileHook(...args) {this[name] = this._createCall(type);return this[name](...args);};
}
复制代码

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");
}
复制代码

可以看到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);}
}
复制代码

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

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

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

create(options) {// 初始化参数,保存options到本对象this.options,保存new Hook(["options"]) 传入的参数到 this._argsthis.init(options);let fn;// 动态构建钩子,这里是抽象层,分同步, 异步, promiseswitch (this.options.type) {// 先看同步case "sync":// 动态返回一个钩子函数fn = new Function(// 生成函数的参数,no before no after 返回参数字符串 xxx,xxx 在// 注意这里this.args返回的是一个字符串,// 在这个例子中是optionsthis.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;
}
复制代码

最后生成的代码大致如下, 参考文章

"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);
}
复制代码

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
}
复制代码

可以看到, 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();}
);
复制代码

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>} */}}
}
复制代码

具体参考上面提到的compiler实现。

编写一个插件

了解到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();});}
}
复制代码

可以看到其实就是在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也不复杂,复杂的是插件本身..
另外, 本应该先写流程的, 流程只能后面补上了。

引用

不满足于只会使用系列: tapable
webpack系列之二Tapable
编写一个插件
Compiler
Compilation
compiler和comnpilation钩子
看清楚真正的 Webpack 插件

webpack-插件机制杂记相关推荐

  1. webpack插件机制

    webpack插件机制是整个webpack工具的核心,那么webpack插件有什么特点呢? 1.独立的JS模块,暴露相应的函数 2.函数原型上的apply方法会注入compiler对象(之所以要定义a ...

  2. 探寻 webpack 插件机制

    webpack 可谓是让人欣喜又让人忧,功能强大但需要一定的学习成本.在探寻 webpack 插件机制前,首先需要了解一件有意思的事情,webpack 插件机制是整个 webpack 工具的骨架,而 ...

  3. mixin机制 vue_读?VuePress(四)插件机制

    前言 从 9 月份开始,vuepress 源码进行了重新设计和拆分.先是开了个 next 分支,后来又合并到 master 分支,为即将发布的 1.x 版本做准备. 最主要的变化是:大部分的全局功能都 ...

  4. delphi 的插件机制与自动更新

    delphi 的插件机制与自动更新 : 1.https://download.csdn.net/download/cxp_2008/2226978   参考 2.https://download.cs ...

  5. 【面试】815- 面试官常问的 webpack 插件

    Plugin ❝ 何为插件(Plugin)?专注处理 webpack 在编译过程中的某个特定的任务的功能模块,可以称为插件.plugin 是一个扩展器,它丰富了 webpack 本身,针对是 load ...

  6. 霖呆呆的六个自定义Webpack插件详解-自定义plugin篇(3)

    霖呆呆的webpack之路-自定义plugin篇 你盼世界,我盼望你无bug.Hello 大家好!我是霖呆呆! 有很多小伙伴在打算学写一个webpack插件的时候,就被官网上那一长条一长条的API给吓 ...

  7. Webpack插件是如何编写的——prerender-spa-plugin源码解析

    概述 本文主要的内容是通过之前使用的prerender-spa-plugin插件的源码阅读,来看下我们应该如何编写一个webpack的插件,同时了解下预渲染插件到底是如何实现的. 这个内容其实已经在使 ...

  8. 小程序工程化实践(上篇)-- 手把手教你撸一个小程序 webpack 插件,一个例子带你熟悉 webpack 工作流程...

    本文基于 webpack 4 和 babel 7,Mac OS,VS Code 小程序开发现状: 小程序开发者工具不好用,官方对 npm 的支持有限,缺少对 webpack, babel 等前端常用工 ...

  9. 初探maven插件机制

    初探maven插件机制 第一部分 Plexus 本质上说,Maven是一个plugin的框架,所以需要有一个管理者来管理这些plugin.Maven选择了Plexus作为plugin的管理者.作为初探 ...

最新文章

  1. [JS]请填充代码,使mySort()能使传入的参数按照从小到大的顺序显示出来。
  2. 遇见BUG(5)如何找到VHDL的包呢?
  3. Bluetooth GAP介绍
  4. mybatis方法传入多参数
  5. MySQL- 用Navicat通过隧道连接到远程数据库
  6. Customer Material Info in CRM and C4C
  7. TokenInsight:反映区块链行业整体表现的TI指数较昨日同期上涨6.21%
  8. Cobbler详解(五)——cobbler常用命令
  9. Android开发的第一天
  10. 怎样看pytorch源码最有效?
  11. 专升本高数——常用公式总结大全【补充扩展】
  12. mysql 同义词_在数据库mysql中存储和检索同义词的最佳方法
  13. UML建模--用例图
  14. Introduction to ML
  15. java毕业设计学生考勤系统Mybatis+系统+数据库+调试部署
  16. 软件工程课程第二次任务——需求分析与原型设计
  17. 一起学时序分析之延迟与时钟偏斜和抖动
  18. 职场的1000+篇文章总结
  19. FreeRTOS嵌入式实时操系统查看指定任务剩余堆栈大小方法
  20. 胖哥食品网络诊断分析

热门文章

  1. Shell练习-统计出每个IP的访问量有多少?
  2. 中国联通:联通集团正研究混改 具体实施方案在讨论中
  3. AbstractBeanDefinition:lenientConstructorResolution属性源码分析
  4. cocos2dx luajavaBridge 学习笔记
  5. (NO.00001)iOS游戏SpeedBoy Lite成形记(九)
  6. mysql-主从服务器同步搭建
  7. 周立功-成功心法(2):通过讲故事营销自己
  8. Linux 修改SSH 默认端口 22,防止被破解密码
  9. [20180408]那些函数索引适合字段的查询.txt
  10. Kali Linux重设root密码