【webpack进阶】可视化展示webpack内部插件与钩子关系 - 掘金
【钩子的类函数】
通过Tapable,可以快速创建各类钩子。以下是各种钩子的类函数:
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
然后,webpack中的插件会将所需执行的函数通过 .tap()
/ .tapAsync()
/ .tapPromise()
等方法注册到对应钩子上。这样,webpack调用相应钩子时,插件中的函数就会自动执行。
那么,还有一个问题:webpack是如何调用插件,将插件中的方法在编译阶段注册到钩子上的呢?
对于这个问题,webpack规定每个插件的实例,必须有一个.apply()
方法,webpack打包前会调用所有插件的.apply()
方法,插件可以在该方法中进行钩子的注册。
在webpack的lib/webpack.js
中,有如下代码:
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler);
}
}
复制代码
上面这段代码会从webpack配置的plugins
字段中取出所有插件的实例,然后调用其.apply()
方法,并将Compiler
的实例作为参数传入。这就是为什么webpack要求我们所有插件都需要提供.apply()
方法,并在其中进行钩子的注册。
注意,和
.call()
一样,这里的.apply()
也不是js的原生方法。你会在源码中看到许多.call()
与.apply()
,但它们基本都不是你认识的那个方法。
【钩子分类】
钩子数量众多。webpack内部的钩子非常多,数量达到了180+,类型也五花八门。除了官网列出的compiler
与compilation
中那些常用的钩子,还存在着众多其他可以使用的钩子。有些有用的钩子你可能无从知晓,例如我最近用到的localVars
、requireExtensions
等钩子。
模块/插件与钩子的关系主要分为三类:
- 模块/插件「创建」钩子,如
this.hooks.say = new SyncHook()
; - 模块/插件将方法「注册」到钩子上,如
obj.hooks.say.tap('one', () => {...})
; - 模块/插件通过「调用」来触发钩子事件,如
obj.hooks.say.call()
。
【webpack进阶】前端运行时的模块化设计与实现
【模块包装】
浏览器原生实际是不支持所谓的CommonJS或ESM模块化规范的。那么webpack是如何在打包出的代码中实现模块化的呢?
当我们写一个Node(JavaScript)模块时,模块里的module
、require
、__filename
等这些变量是哪来的?如果你看过Node loader.js 部分源码,应该就大致能理解:
Module.wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
Module.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'
});'
];
复制代码
Node会自动将每个模块进行包装(wrap),将其变为一个function。
module
、require
、__filename
这些变量都是哪来的? —— 它们会被作为function的参数在模块编译执行时注入进来。
【模块暂存】
仔细对比webpack与Node,你会发现在__webpack_require__
中有一个重要的区别:
在webpack中不存在像Node一样调用._compile()
这种方法的过程。即不会像Node那样,对一个未载入缓存的模块,通过「读取模块路径 -> 编译模块代码 -> 执行模块」来载入模块。为什么呢?
这是因为,Node作为服务端语言,模块都是本地文件,加载时延低,可同步阻塞进行模块文件寻址、读取、编译和执行,这些过程在模块require的时候再“按需”执行即可;而webpack运行在客户端(浏览器),显然不能在需要时(即执行__webpack_require__
时)再通过网络加载js文件,并同步地等待加载完成后再返回__webpack_require__
。这种网络时延,显然不能满足“同步依赖”的要求。
把同步依赖的模块先“注册”到内存中(模块暂存),等到require时,再执行该模块、缓存模块对象、返回对应的exports
。而webpack中,这个所谓的内存就是modules
对象。
【异步依赖】
webpack支持使用动态模块引入的语法(代码拆分),例如:dynamic import
和早期的require.ensure
,这种方式与使用CommonJS的require
和ESM的import
最重要的区别在于,该类方法会异步(或者说按需)加载依赖。
异步依赖的核心方法就是__webpack_require__.e
。
该方法首先会根据chunkId在installChunks中判断该chunk是否正在加载或已经被加载;如果没有则会创建一个promise,将其保存在installChunks中,并通过jsonpScriptSrc()
方法获取文件路径,通过sciript标签加载,最后返回该promise。
【Resolve】
在webpackJsonpCallback()
方法中,有一段代码就是根据chunkIds的数组,检查并更新chunk的加载状态:
上面的代码先根据模块注册时的chunkId,取出installedChunks对应的所有loading中的chunk,最后将这些chunk的promise进行resolve操作。