摘要:系列文章系列第一篇基础杂记系列第二篇插件机制杂记系列第三篇流程杂记前言本身并不难,他所完成的各种复杂炫酷的功能都依赖于他的插件机制。的插件机制依赖于一个核心的库,。是什么是一个类似于的的库主要是控制钩子函数的发布与订阅。
系列文章
Webpack系列-第一篇基础杂记
Webpack系列-第二篇插件机制杂记
Webpack系列-第三篇流程杂记
webpack本身并不难,他所完成的各种复杂炫酷的功能都依赖于他的插件机制。或许我们在日常的开发需求中并不需要自己动手写一个插件,然而,了解其中的机制也是一种学习的方向,当插件出现问题时,我们也能够自己来定位。
TapableWebpack的插件机制依赖于一个核心的库, Tapable。
在深入webpack的插件机制之前,需要对该核心库有一定的了解。
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 需要处理的条件) { // 处理数据 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 } })
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._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"; " + this.header() + this.content({ onError: err => `throw ${err}; `, onResult: result => `return ${result}; `, onDone: () => "", rethrowIfPossible: true }) ); break; case "async": fn = new Function( this.args({ after: "_callback" }), ""use strict"; " + this.header() + // 这个 content 调用的是子类类的 content 函数, // 参数由子类传,实际返回的是 this.callTapsSeries() 返回的类容 this.content({ onError: err => `_callback(${err}); `, onResult: result => `_callback(null, ${result}); `, onDone: () => "_callback(); " }) ); break; case "promise": let code = ""; code += ""use strict"; "; code += "return new Promise((_resolve, _reject) => { "; code += "var _sync = true; "; code += this.header(); code += this.content({ onError: err => { let code = ""; code += "if(_sync) "; code += `_resolve(Promise.resolve().then(() => { throw ${err}; })); `; code += "else "; code += `_reject(${err}); `; return code; }, onResult: result => `_resolve(${result}); `, onDone: () => "_resolve(); " }); code += "_sync = false; "; code += "}); "; 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} */ shouldEmit: new SyncBailHook(["compilation"]), /** @type {AsyncSeriesHook } */ done: new AsyncSeriesHook(["stats"]), /** @type {AsyncSeriesHook<>} */ additionalPass: new AsyncSeriesHook([]), /** @type {AsyncSeriesHook } */ ...... ...... 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} */ buildModule: new SyncHook(["module"]), /** @type {SyncHook } */ rebuildModule: new SyncHook(["module"]), /** @type {SyncHook } */ failedModule: new SyncHook(["module", "error"]), /** @type {SyncHook } */ succeedModule: new SyncHook(["module"]), /** @type {SyncHook } */ addEntry: new SyncHook(["entry", "name"]), /** @type {SyncHook } */ } } }
具体参考上面提到的compiler实现。
编写一个插件了解到tapablecompilercompilation之后, 再来看插件的实现就不再一头雾水了
以下代码源自官方文档
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 |
事件钩子 | 触发时机 | 参数 | 类型 |
---|---|---|---|
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 插件
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/102367.html
摘要:系列文章系列第一篇基础杂记系列第二篇插件机制杂记系列第三篇流程杂记前言公司的前端项目基本都是用来做工程化的,而虽然只是一个工具,但内部涉及到非常多的知识,之前一直靠来解决问题,之知其然不知其所以然,希望这次能整理一下相关的知识点。 系列文章 Webpack系列-第一篇基础杂记 Webpack系列-第二篇插件机制杂记 Webpack系列-第三篇流程杂记 前言 公司的前端项目基本都是用...
摘要:最后执行了的回调函数,触发了事件点,并回到函数的回调函数触发了事件点执行对于当前模块,或许存在着多个依赖模块。 系列文章 Webpack系列-第一篇基础杂记 Webpack系列-第二篇插件机制杂记 Webpack系列-第三篇流程杂记 前言 本文章个人理解, 只是为了理清webpack流程, 没有关注内部过多细节, 如有错误, 请轻喷~ 调试 1.使用以下命令运行项目,./scrip...
摘要:入口文件打包出口地址在中可以配置我们的地址这里你要有一个七牛云的账户。特别像是七牛云这样拥有图片处理引擎的服务商,我们还可以通过来处理上传至的图片。 本项目源码均可在 这里 找到。 之前公司的官网项目静态文件都是放在静态服务器中,这其中的弊端就不赘述了。简单说一下 CDN 的好处: CDN 可以解决因分布、带宽、服务器性能带来的访问延迟问题,适用于站点加速、点播、直播等场景。使用户可就...
摘要:前提最近通过阅读官方文档的事件模块,有了一些思考和收获,在这里记录一下调用方法时需要手动绑定先从一段官方代码看起代码中的注释提到了一句话的绑定是必须的,其实这一块是比较容易理解的,因为这并不是的一个特殊点,而是这门语言的特性。 前提 最近通过阅读React官方文档的事件模块,有了一些思考和收获,在这里记录一下~ 调用方法时需要手动绑定this 先从一段官方代码看起: showImg(...
阅读 3451·2021-10-13 09:39
阅读 1437·2021-10-08 10:05
阅读 2242·2021-09-26 09:56
阅读 2254·2021-09-03 10:28
阅读 2649·2019-08-29 18:37
阅读 2009·2019-08-29 17:07
阅读 575·2019-08-29 16:23
阅读 2129·2019-08-29 11:24