webpack核心原理-2

简介: webpack核心原理-2

When: 什么时候会触发钩子


了解 webpack 插件的基本形态之后,接下来需要弄清楚一个问题:webpack 会在什么时间节点触发什么钩子?这一块我认为是知识量最大的一部分,毕竟源码里面有237个钩子,但官网只介绍了不到100个,且官网对每个钩子的说明都太简短,就我个人而言看完并没有太大收获,所以有必要展开聊一下这个话题。先看先看几个例子:

compiler.hooks.compilation


  • 时机:启动编译创建出 compilation 对象后触发
  • 参数:当前编译的 compilation 对象
  • 示例:很多插件基于此事件获取 compilation 实例
compiler.hooks.make



  • 时机:正式开始编译时触发
  • 参数:同样是当前编译的 compilation 对象
  • 示例:webpack 内置的 EntryPlugin 基于此钩子实现 entry 模块的初始化
compilation.hooks.optimizeChunks



  • 时机: seal 函数中,chunk 集合构建完毕后触发
  • 参数:chunks 集合与 chunkGroups 集合
  • 示例: SplitChunksPlugin 插件基于此钩子实现 chunk 拆分优化
compiler.hooks.done


  • 时机:编译完成后触发
  • 参数: stats 对象,包含编译过程中的各类统计信息
  • 示例: webpack-bundle-analyzer 插件基于此钩子实现打包分析

这是我总结的钩子的三个学习要素:触发时机、传递参数、示例代码。


触发时机

触发时机与 webpack 工作过程紧密相关,大体上从启动到结束,compiler 对象逐次触发如下钩子:

compilation 对象逐次触发:

所以

理解清楚前面说的 webpack 工作的主流程,基本上就可以捋清楚“什么时候会触发什么钩子”。


参数

传递参数与具体的钩子强相关,官网对这方面没有做出进一步解释,我的做法是直接在源码里面搜索调用语句,例如对于 compilation.hooks.optimizeTree ,可以在 webpack 源码中搜索 hooks.optimizeTree.call 关键字,就可以找到调用代码:

// lib/compilation.js#2297
this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
});
复制代码


结合代码所在的上下文,可以判断出此时传递的是经过优化的 chunks 及 modules 集合。


找到示例

Webpack 的钩子复杂程度不一,我认为最好的学习方法还是带着目的去查询其他插件中如何使用这些钩子。例如,在 compilation.seal 函数内部有 optimizeModules 和 afterOptimizeModules 这一对看起来很对偶的钩子,optimizeModules 从字面上可以理解为用于优化已经编译出的 modules ,那 afterOptimizeModules 呢?从 webpack 源码中唯一搜索到的用途是 ProgressPlugin ,大体上逻辑如下:

compilation.hooks.afterOptimizeModules.intercept({
  name: "ProgressPlugin",
  call() {
    handler(percentage, "sealing", title);
  },
  done() {
    progressReporters.set(compiler, undefined);
    handler(percentage, "sealing", title);
  },
  result() {
    handler(percentage, "sealing", title);
  },
  error() {
    handler(percentage, "sealing", title);
  },
  tap(tap) {
    // p is percentage from 0 to 1
    // args is any number of messages in a hierarchical matter
    progressReporters.set(compilation.compiler, (p, ...args) => {
      handler(percentage, "sealing", title, tap.name, ...args);
    });
    handler(percentage, "sealing", title, tap.name);
  }
});
复制代码



基本上可以猜测出,afterOptimizeModules 的设计初衷就是用于通知优化行为的结束。


apply 虽然是一个函数,但是从设计上就只有输入,webpack 不 care 输出,所以在插件中只能通过调用类型实体的各种方法来或者更改实体的配置信息,变更编译行为。例如:


compilation.addModule :添加模块,可以在原有的 module 构建规则之外,添加自定义模块

compilation.emitAsset:直译是“提交资产”,功能可以理解将内容写入到特定路径

到这里,插件的工作机理和写法已经有一个很粗浅的介绍了,回头单拎出来细讲吧。

How: 如何影响编译状态


解决上述两个问题之后,我们就能理解“如何将特定逻辑插入 webpack 编译过程”,接下来才是重点 —— 如何影响编译状态?强调一下,webpack 的插件体系与平常所见的 订阅/发布 模式差别很大,是一种非常强耦合的设计,hooks 回调由 webpack 决定何时,以何种方式执行;而在 hooks 回调内部可以通过修改状态、调用上下文 api 等方式对 webpack 产生 side effect。比如,EntryPlugin 插件:

class EntryPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap(
      "EntryPlugin",
      (compilation, { normalModuleFactory }) => {
        compilation.dependencyFactories.set(
          EntryDependency,
          normalModuleFactory
        );
      }
    );
    compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
      const { entry, options, context } = this;
      const dep = EntryPlugin.createDependency(entry, options);
      compilation.addEntry(context, dep, options, (err) => {
        callback(err);
      });
    });
  }
}
复制代码

上述代码片段调用了两个影响 compilation 对象状态的接口:


compilation.dependencyFactories.set

compilation.addEntry

操作的具体含义可以先忽略,这里要理解的重点是,webpack 会将上下文信息以参数或 this (compiler 对象) 形式传递给钩子回调,在回调中可以调用上下文对象的方法或者直接修改上下文对象属性的方式,对原定的流程产生 side effect。所以想纯熟地编写插件,除了要理解调用时机,还需要了解我们可以用哪一些api,例如:


compilation.addModule:添加模块,可以在原有的 module 构建规则之外,添加自定义模块

compilation.emitAsset:直译是“提交资产”,功能可以理解将内容写入到特定路径

compilation.addEntry:添加入口,功能上与直接定义 entry 配置相同

module.addError:添加编译错误信息

Loader 介绍

Loader 的作用和实现比较简单,容易理解,所以简单介绍一下就行了。回顾 loader 在编译流程中的生效的位置:


98d9cc395a881dc4d66b73282fb2f5c7.png


流程图中, runLoaders 会调用用户所配置的 loader 集合读取、转译资源,此前的内容可以千奇百怪,但转译之后理论上应该输出标准 JavaScript 文本或者 AST 对象,webpack 才能继续处理模块依赖。


理解了这个基本逻辑之后,loader 的职责就比较清晰了,不外乎是将内容 A 转化为内容 B,但是在具体用法层面还挺多讲究的,有 pitch、pre、post、inline 等概念用于应对各种场景。


为了帮助理解,这里补充一个示例: Webpack 案例 – vue-loader 原理分析。


附录

源码阅读技巧


避重就轻: 挑软柿子捏,比如初始化过程虽然绕,但是相对来说是概念最少、逻辑最清晰的,那从这里入手摸清整个工作过程,可以习得 webpack 的一些通用套路,例如钩子的设计与作用、编码规则、命名习惯、内置插件的加载逻辑等,相当于先入了个门


学会调试: 多用 ndb 单点调试功能追踪程序的运行,虽然 node 的调试有很多种方法,但是我个人更推荐 ndb ,灵活、简单,配合 debugger 语句是大杀器


理解架构: 某种程度上可以将 webpack 架构简化为 compiler + compilation + plugins ,webpack 运行过程中只会有一个 compiler ;而每次编译 —— 包括调用 compiler.run 函数或者 watch = true 时文件发生变更,都会创建一个 compilation 对象。理解这三个核心对象的设计、职责、协作,差不多就能理解 webpack 的核心逻辑了


抓大放小:


plugin 的关键是“钩子”,我建议战略上重视,战术上忽视!钩子毕竟是 webpack 的关键概念,是整个插件机制的根基,学习 webpack 根本不可能绕过钩子,但是相应的逻辑跳转实在太绕太不直观了,看代码的时候一直揪着这个点的话,复杂性会剧增,我的经验是:


认真看一下 tapable 仓库的文档,或者粗略看一下 tapable 的源码,理解同步钩子、异步钩子、promise 钩子、串行钩子、并行钩子等概念,对 tapable 提供的事件模型有一个较为精细的认知,这叫战略上重视

遇到不懂的钩子别慌,我的经验我连这个类都不清楚干啥的,要去理解这些钩子实在太难了,不如先略过钩子本身的含义,去看那些插件用到了它,然后到插件哪里去加 debugger 语句单点调试,等你缕清后续逻辑的时候,大概率你也知道钩子的含义了,这叫战术上忽视

保持好奇心:


学习过程保持旺盛的好奇心和韧性,善于 & 敢于提出问题,然后基于源码和社区资料去总结出自己的答案,问题可能会很多,比如:


loader 为什么要设计 pre、pitch、post、inline?

compilation.seal 函数内部设计了很多优化型的钩子,为什么需要区分的这么细?webpack 设计者对不同钩子有什么预期?

为什么需要那么多 module 子类?这些子类分别在什么时候被使用?

ModuleModule 子类


从上文可以看出,webpack 构建阶段的核心流程基本上都围绕着 module 展开,相信接触过、用过 Webpack 的读者对 module 应该已经有一个感性认知,但是实现上 module 的逻辑是非常复杂繁重的。


以 webpack@5.26.3 为例,直接或间接继承自 Module (webpack/lib/Module.js 文件) 的子类有54个:


网络异常,图片无法展示
|


要一个一个捋清楚这些类的作用实在太累了,我们需要抓住本质:module 的作用是什么?

————————————————module 是 webpack 资源处理的基本单位,可以认为 webpack 对资源的路径解析、读入、转译、分析、打包输出,所有操作都是围绕着 module 展开的。有很多文章会说 module = 文件, 其实这种说法并不准确,比如子类 AsyncModuleRuntimeModule 就只是一段内置的代码,是一种资源而不能简单等价于实际文件。


Webpack 扩展性很强,包括模块的处理逻辑上,比如说入口文件是一个普通的 js,此时首先创建 NormalModule 对象,在解析 AST 时发现这个文件里还包含了异步加载语句,例如 requere.ensure ,那么相应地会创建 AsyncModuleRuntimeModule 模块,注入异步加载的模板代码。上面类图的 54 个 module 子类都是为适配各种场景设计的。


目录
相关文章
|
1月前
|
监控 前端开发 JavaScript
Webpack 中 HMR 插件的工作原理
【10月更文挑战第23天】可以进一步深入探讨 HMR 工作原理的具体细节、不同场景下的应用案例,以及与其他相关技术的结合应用等方面的内容。通过全面、系统地了解 HMR 插件的工作原理,能够更好地利用这一功能,为项目的成功开发提供有力保障。同时,要不断关注技术的发展动态,以便及时掌握最新的 HMR 技术和最佳实践。
|
1月前
|
缓存 前端开发 JavaScript
Webpack 动态加载的原理
【10月更文挑战第23天】Webpack 动态加载通过巧妙的机制和策略,实现了模块的按需加载和高效运行,提升了应用程序的性能和用户体验。同时,它也为前端开发提供了更大的灵活性和可扩展性,适应了不断变化的业务需求和技术发展。
|
1月前
|
缓存 前端开发 JavaScript
webpack 原理
【10月更文挑战第23天】Webpack 原理是一个复杂但又非常重要的体系。它通过模块解析、依赖管理、加载器和插件的协作,实现了对各种模块的高效打包和处理,为现代前端项目的开发和部署提供了强大的支持。同时,通过代码分割、按需加载、热模块替换等功能,提升了应用程序的性能和用户体验。随着前端技术的不断发展,Webpack 也在不断演进和完善,以适应不断变化的需求和挑战。
|
2月前
|
缓存 前端开发 JavaScript
Webpack 打包的基本原理
【10月更文挑战第5天】
|
3月前
|
JavaScript 前端开发
手写一个简易bundler打包工具带你了解Webpack原理
该文章通过手写一个简易的打包工具bundler,帮助读者理解Webpack的工作原理,包括模块解析、依赖关系构建、转换源代码以及生成最终输出文件的整个流程。
|
4月前
|
缓存 前端开发 JavaScript
Webpack 模块解析:打包原理、构造形式、扣代码补参数和全局导出
Webpack 模块解析:打包原理、构造形式、扣代码补参数和全局导出
180 1
|
7月前
|
API 开发工具 开发者
webpack热更新原理
Webpack的Hot Module Replacement(HMR)提升开发效率,无需刷新页面即可更新模块。开启HMR需在配置中设`devServer.hot: true`。Webpack构建时插入HMR Runtime,通过WebSocket监听并处理文件变化。当模块改变,Webpack发送更新到浏览器,HMR Runtime找到对应模块进行热替换,保持应用状态。开发者可利用`module.hot` API处理热替换逻辑。
|
7月前
|
前端开发 测试技术 开发者
深入理解 Webpack 热更新原理:提升开发效率的关键
深入理解 Webpack 热更新原理:提升开发效率的关键
|
7月前
|
缓存 前端开发 算法
Webpack 进阶:深入理解其工作原理与优化策略
Webpack 进阶:深入理解其工作原理与优化策略
177 2
|
7月前
|
存储 缓存 JavaScript
《Webpack5 核心原理与应用实践》学习笔记-> webpack的loader开发技巧
《Webpack5 核心原理与应用实践》学习笔记-> webpack的loader开发技巧
90 1