1、前文回顾
上文主要讨论了 webpack/lib/webpack.js 导出的方法 webpack 及其创建 Compiler 实例(编译器)对象的主要过程,主要集中在 createCompiler 方法中,这篇你需要了解的点有以下这些:
- 创建编译器会根据 options 是否为数组决定创建多编译器或者是单独编译器:if(Array.isArray(options))
- applyWebpackOptionsBaseDefaults 方法进行基础配置如 context/target/mode 等选项的初始化;
- 通过 Compiler 这个类型实例化一个 compiler 实例对象;
- 用户定义的插件在 Compiler 实例化之后被应用;
- 调用 applyWebpackOptionsDefaults 方法对 webpack 设置默认选项;
- 利用 WebpackOptionsApply 类型依据前面得出的 options 进行默认插件引用以及一些内建特性的应用;
- compiler.resolverFactory.hooks.resolveOptions.for() 声明创建 Resolver 路径解析器所需的配置项;
本文讨论上文中的一个重要的分支流程 new WebpackOptionsApply().process
,本文算是上文的一篇补充,之所以将他拿出来除了篇幅外更重要的是它的重大意义。
很多人看不懂 webpack 源码就是从这里开始的,这里不展开具体的逻辑实现,但是大家需要对本文中的提到的内容有个大概印象。
2、WebpackOptionsApply
前文提到过在得到 webpack 的基础配置后(applyWebpackOptionsBaseDefaults 方法执行后)调用 new WebpackOptionsApply().process(options, compiler) 方法应用一些基础特性。
其核心是利用 WebpackOptionsApply 类型依据前面得出的 options 进行默认插件引用以及一些内建特性的应用。
- 类型位置:webpack/lib/WebpackOptionsApply.js
- 参数:无
- 构造函数:忽略
- 重要方法:WebpackOptionsApply.prototype.process
2.1 整体结构
js
class WebpackOptionsApply extends OptionsApply {
constructor() {
super();
}
process(options, compiler) {
compiler.outputPath = options.output.path;
compiler.recordsInputPath = options.recordsInputPath || null;
compiler.recordsOutputPath = options.recordsOutputPath || null;
compiler.name = options.name;
if (options.externals) {
const ExternalsPlugin = require("./ExternalsPlugin");
new ExternalsPlugin(options.externalsType, options.externals).apply(
compiler
);
}
// 以下都是类似语法:
// if( 某个配置项 ) new XxxPLugin().apply(compiler) 根据某个不同选项初始化不同插件
// new XxxPlugin().apply(compiler) 所有编译选项都需要初始化的插件
// 下面选一些重要的能力初始化介绍给大家认识一下:
// 1.
new JavascriptModulesPlugin().apply(compiler);
// 2.
new JsonModulesPlugin().apply(compiler);
// 3.
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
// 4.
if (options.experiments.syncWebAssembly) {
const WebAssemblyModulesPlugin = require("./wasm-sync/WebAssemblyModulesPlugin");
new WebAssemblyModulesPlugin({
mangleImports: options.optimization.mangleWasmImports
}).apply(compiler);
}
// 5.
if (options.optimization.concatenateModules) {
const ModuleConcatenationPlugin = require("./optimize/ModuleConcatenationPlugin");
new ModuleConcatenationPlugin().apply(compiler);
}
// 6.
if (options.optimization.splitChunks) {
const SplitChunksPlugin = require("./optimize/SplitChunksPlugin");
new SplitChunksPlugin(options.optimization.splitChunks).apply(compiler);
}
// 7.
if (options.cache && typeof options.cache === "object") {
const cacheOptions = options.cache;
switch (cacheOptions.type) {
case "memory": {
// ....
break;
}
case "filesystem": {
// 8.
const AddBuildDependenciesPlugin = require("./cache/AddBuildDependenciesPlugin");
for (const key in cacheOptions.buildDependencies) {
new AddBuildDependenciesPlugin(list).apply(compiler);
}
if (!isFinite(cacheOptions.maxMemoryGenerations)) {
const MemoryCachePlugin = require("./cache/MemoryCachePlugin");
new MemoryCachePlugin().apply(compiler);
} else if (cacheOptions.maxMemoryGenerations !== 0) {
const MemoryWithGcCachePlugin = require("./cache/MemoryWithGcCachePlugin");
new MemoryWithGcCachePlugin({
maxGenerations: cacheOptions.maxMemoryGenerations
}).apply(compiler);
}
if (cacheOptions.memoryCacheUnaffected) {
compiler.moduleMemCaches = new Map();
}
switch (cacheOptions.store) {
case "pack": {
// 9.
const IdleFileCachePlugin = require("./cache/IdleFileCachePlugin");
// 10.
const PackFileCacheStrategy = require("./cache/PackFileCacheStrategy");
new IdleFileCachePlugin(
new PackFileCacheStrategy({/* .... */}).apply(compiler);
break;
}
}
break;
}
}
}
// 11.
new ResolverCachePlugin().apply(compiler);
// 12.
compiler.resolverFactory.hooks.resolveOptions
.for("normal")
.tap("WebpackOptionsApply", resolveOptions => {
resolveOptions = cleverMerge(options.resolve, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
return resolveOptions;
});
compiler.resolverFactory.hooks.resolveOptions
.for("context")
.tap("WebpackOptionsApply", resolveOptions => {
resolveOptions = cleverMerge(options.resolve, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
resolveOptions.resolveToContext = true;
return resolveOptions;
});
compiler.resolverFactory.hooks.resolveOptions
.for("loader")
.tap("WebpackOptionsApply", resolveOptions => {
resolveOptions = cleverMerge(options.resolveLoader, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
return resolveOptions;
});
compiler.hooks.afterResolvers.call(compiler);
}
}
2.2 JavascriptModulesPlugin
JavaScriptModulesPlugin 的作用是注册各种类型模块的 JavaScript 解析器 (Parser) 及 JavaScript 产物生成器 (JavaScriptGenerator) 。
分别注册在 normalModuleFactory.hooks.createParser 和 normalModuleFactory.hooks.createGenerator 钩子上。
2.3 JsonModulesPlugin
JsonModulesPlugin 工作和前面 JavaScriptModulesPlugin 类似,只不过 JsonModulesPlugin 注册的是 JSON 模块的解析器和内容生成器。
2.4 EntryOptionPlugin
EntryOptionPlugin 是处理 entry 注册的插件,我们在 webpack.config.js(或其他方式注册的 entry) 中注册 entry:
js
module.exports = {
entry: './src/index.js'
}
或者是动态形式的入口,都会在这个插件中得到处理,根据不同类型的入口声明选择 DynamicEntryPlugin 或者 Entryplugin 进行入口的注册,所谓入口也是一个编译的起始模块,webpack 将会从指定的入口开始构建模块并产出依赖图谱,最终生成 bundle;
2.5 WebAssemblyModulesPlugin
WebAssemblyModulesPlugin 工作和前面 JsonModulesPlugin、JavaScriptModulesPlugin 类似,只不过 WebAssemblyModulesPlugin 注册的是 注册 WebAssembly 模块的解析器和内容生成器。
2.6 ModuleConcatenationPlugin
这个插件是 webpack 性能优化的重要一环,通过 ModuleConcatenationPlugin 插件,在打包时将所有模块预编译到一个闭包中,以提升代码在浏览器中的执行速度,这个也就是大家说的 "作用域提升(scope hoisting)"特性实现的核心插件。
2.7 SplitChunksPlugin
这个插件大家不陌生,就是拆分 chunk 用的,按照一定的规则将一个较大的 chunk 拆分成多个较小的 chunk。这是体积与 request 请求次数的权衡产物。当然,这个是 webpack 开箱即用的性能优化抓手之一。
2.8 if (options.cache && typeof options.cache === "object")
这里需要单独提一下这个条件,这个条件是和下面的 2.4.8/2.4.9/2.4.10 连用的,options.cache 是 webpack5 当中的重要特性持久化缓存(Persistant Cache)启用配置,设置该选项,webpack 构建将会在初次构建时写入缓存并在之后的构建中优先使用缓存中的内容,此举目的是 webpack 大幅提升构建速度。缓存中包括模块、request 解析结果、Loader等构建依赖缓存等多项中间产物的缓存。
这里不展开,前面的系列专注于如何从源代码到 bundle 的过程,后面我们会写专门的篇幅讨论 webpack 的缓存系统实现以及设计。
我老板曾经高度赞扬 webpack 的缓存架构设计,也确实是个让人眼前一亮的设计
2.9 AddBuildDependenciesPlugin
AddBuildDependenciesPlugin 插件是将 options.cache.buildDependencies 主动声明的构建依赖项目加入到 webpack 收集的构建依赖范围中,其默认收集 loader 等 ndoe_modules 下的内容作为构建依赖。
此举的意义在于让开发者可以选择性的将其项目代码中的目录、模块声明成构建依赖。
2.10 IdleFileCachePlugin
这个插件是处理 webpack 缓存的写入和恢复的插件,爱摸鱼!IdleFileCachePlugin,自然这个也是摸鱼缓存插件了。
不过人家是闲时处理缓存,所谓闲时就是 webpack 没有处理编译任务的时候。之所以闲时,肯定是为 webpack 编译时让路,避免挤占构建资源拖慢构建速度!
注意,这个插件不负责缓存具体的读写,它只负责调度。这也是 webpack 缓存系统设计架构的一部分,具体的读写交给各个缓存策略去实现。
2.11 PackFileCacheStrategy
上面 2.4.9 说 IdleFileCachePlugin 个插件不负责缓存具体的读写,它只负责调度但不负责读写,具体的读写交给各个缓存策略去实现。
PackFileCacheStrategy 是配合上面的 IdleFileCachePlugin 插件使用的,这里面定义的缓存的具体的读、写、校验逻辑。
2.12 ResolverCachePlugin
ResolverCachePlugin 也是 webpack 缓存的重要组成部分,webpack 构建的耗时有相当一部分都花在了 resolve 的过程中,因此这也是 webpack 优化编译时的手段之一,下次面试不要问到 webpack 如何提升构建速度 只会 external 和多进程啦!
ResolverCachePlugin 做了两项工作:
- 统计缓存利用率;
- 拦截 resolve 过程,优先使用缓存,若缓存有效则直接返回缓存结果,否则进行常规 resolve 过程;
2.13 compiler.resolverFactory.hooks.resolveOptions.for()
这里也不是插件,这是为 webpack 内建的三种模块注册路径解析器(Resolver)的配置项:
- normal: 即 NormalModule 的 resolver 创建的是所需的配置项;
- loader: 即 loader 模块 resolver 创建时所需的配置项;
- context: ContextModule 模块的 resolver 创建时所需的配置项;
这个东西会在后面构建 module 的过程中再做讲解;
3、总结
本文讨论 new WebpackOptionsApply().process
,本文算是上文的一篇补充,之所以将他拿出来除了篇幅外更重要的是它的重大意义。
- 为不同类型的模块注册 Parser 和 Generator:
- 1.1 注册 JavascriptModulesPlugin:普通 JS 模块的 Parser 和 Generator
- 1.2 JsonModulesPlugin:JSON 模块的 Parser 和 Generator
- 1.3 WebAssemblyModulesPlugin:WASM 的 Parser 和 Generator
- 为 entry 声明 EntryPlugin;
- 注册性能优化相关插件,当然包括 treeshaking 相关的:
- 4.1 ModuleConcatenationPlugin:作用域提升的实现
- 4.2 SplitChunkPlugin:拆分 chunk 的实现
- 处理 if (options.cache && ... 开启持久化缓存,这其中重点介绍了四个模块(或插件):
- 6.1 AddBuildDependenciesPlugin:处理构建编译
- 6.2 IdleFileCachePlugin:缓存读写调度器
- 6.3 PackFileCacheStrategy:缓存读/写实现
- 6.4 ResolverCachePlugin:路径解析相关缓存重要组成插件