webpack5 打包流程源码剖析(1)
开篇
webpack 相信大家都不陌生,近些年来被广泛应用于打包资源前端项目工程之中。
通常,我们只要掌握了 webpack 一些常用配置,足以满足项目构建的大多数应用场景。
然,当你想去站在一个更高的角度去看待和使用 webpack 时,如做优化、自定义 plugin 和 loader,理解 webpack 的打包编译流程尤为重要。掌握打包流程线上的各个阶段所做的工作,能够更准确的帮助我们实现高标准的定制化功能。
本篇,我们将以 Webpack 5 作为材料,调试源码来熟悉 webpack 打包流程线,有了源码流程上的熟悉,今后想深入那部分配置的原理将变得容易。
一、前置知识
「1. Webpack」
webpack 是 JavaScript 应用程序静态模块打包器。它的源码设计采用了插件架构,由 Tapable
提供注册和调用插件的能力。因此,webpack 中应用了很多内置插件,来完成对每一个 config 配置项功能的实现。
「2. Compiler」 compiler 理解为 编译器,仅在 webpack 初始化时创建一次实例,是 webpack 打包流程上的支柱引擎。
在 compiler 实例上提供了大量 hooks
来面向用户实现自定义插件,在 run
启动打包之后会创建 compilation
实例处理模块的编译工作。
「3. Compilation」
compilation
理解为 编译,是由 compiler
编译器创建而成,一个编译器可能会创建一次或多次编译,则 compilation
可能会被创建多次。(watch
)
compilation 会从入口模块开始,编译模块以及它的依赖子模块。所有的模块的编译都会经过:加载(loaded)、封存(sealed)、优化(optimized)、分块(chunked)、哈希(hashed) 和 重新创建(restored)。
「4. Dependence」 webpack 用于它来记录模块间依赖关系。在模块中引用其它模块,会将引用关系表述为 Dependency 子类并关联 module 对象,等到当前 module 内容都解析完毕之后,启动下次循环开始将 Dependency 对象转换为新的 Module 子类。
「5. Module」 webpack 处理每一个资源文件时,都会以 module 对象形式存在,包含了资源的路径、上下文、依赖、内容等信息。所有关于资源的构建、转译、合并也都是以 module 为基本单位进行。
「6. Chunk」 编译完成准备输出时,webpack 会将 module 按特定的规则组织成一个一个的 chunk,这些 chunk 某种程度上跟最终输出的文件一一对应。
「7. Assets」 asset 代表了最终要输出写入磁盘的文件内容,它与 chunk 一一对应。
「8. Tapable」 Tapable 提供注册和调用插件的能力,来串联 webapck 整个打包流程的插件工作。
它能够在特定时机触发钩子,并附带上足够的上下文信息,去通知插件注册的钩子回调,去产生 side effect
影响编译状态和后续流程。
Tapable 的具体介绍和使用可以查阅这篇文章 Webpack 插件架构 - Tapable
流程概览
webpack 打包流程整体可划分为四大块,后续的源码调试也将按照以下划分进行分析。
- 初始化阶段
- 构建阶段(make)
- 生成阶段(seal)
- 写入阶段(emit)
由于内容和篇幅过长,将分成两篇文章来介绍流程,本篇主要介绍「初始化阶段」和 「构建阶段」;「生成阶段」和「写入阶段」请移步到 webpack5 打包流程源码剖析(2)。
二、调试环境
我们先来初始化一个调试环境:
mkdir webpack-debugger && cd webpack-debugger && npm init -y && npm install webpack webpack-cli -D
在 webpack-debugger
目录下创建 src/index.js
作为入口模块,文件中添加一行打印代码:
// src/index.js
conspole.log('webpack-debugger');
新建 webpack.config.js
,我们使用最简单的打包配置:
// webpack.config.js
const path = require('path');
module.exports = () => ({
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'build'),
filename: 'static/js/[name].[contenthash:8].js',
}
})
最后,创建 build.js
作为调用 webpack 打包的执行文件:
// build.js
const path = require('path');
const webpack = require('webpack');
const configFactory = require('./webpack.config.js');
const config = configFactory();
debugger;
const compiler = webpack(config);
debugger;
compiler.run();
通常我们都会在 package.json scripts 字段中使用 webpack bin 命令执行去 webpack-cli
进行打包,而进入 webpack-cli 内部调用打包与上面大同小异。
上述代码可知:webpack 默认导出一个函数方法,接收 config 配置对象作为参数,返回 compiler
对象;通过 compiler.run()
开启打包流程。
如果你使用的 VSCode,新建一个 JavaScript Debug Terminal
并执行 node build.js
开启调试。
接下来我们分析初始化阶段 webpack 做了哪些事情。
三、初始化阶段
我们把在真正构建入口模块之前的这一阶段划分为初始化阶段,主要步骤如下:
- 初始化参数:将用户传入配置与默认配置结合得到最终配置参数;
- 创建编译器对象:根据配置参数创建
Compiler
实例对象; - 初始化编译环境:注册用户配置插件及内置插件;
- 运行编译:执行
compiler.run
方法; - 确定入口:根据配置
entry
找寻所有入口文件,并转换为dependence
对象,等待执行compilition.addEntry
编译工作。
3.1、webpack()
webpack 依赖包默认导出一个方法,这个方法的定义在 webpack/lib/webpack.js
之中:
// webpack/lib/webpack.js
const webpack = (options, callback) => {
const create = () => {
const webpackOptions = options;
const compiler = createCompiler(webpackOptions);
}
if (callback) {
const { compiler } = create();
compiler.run((err, stats) => {
compiler.close(err2 => {
callback(err || err2, stats);
});
});
return compiler;
} else {
const { compiler } = create();
return compiler;
}
}
这个方法允许传递两个参数,若传递了 callback
,创建 compiler 实例后会自动调用 run 方法启动打包,否则交由外部手动调用来启动打包。
3.2、createCompiler
参数合并、compiler
编译器实例创建、注册外部和内部插件都在这里来完成:
// webpack/lib/webpack.js
const createCompiler = rawOptions => {
// 规范 webpack config 配置项(创建 config 配置项)
const options = getNormalizedWebpackOptions(rawOptions);
// 设置默认的 config.context
applyWebpackOptionsBaseDefaults(options);
// 创建 compiler 编译器实例
const compiler = new Compiler(options.context, options);
// 应用 Node 环境插件,如为 compiler 提供 fs 文件操作 API(fs 模块二次封装)。
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
// 注册用户传入的插件配置
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
// 应用配置项默认值
applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 关键,注册 Webpack 打包流程的内置插件
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};
这里我们重点提及两处:
- new Compiler(options.context, options) Compiler 是一个 ES6 class 构造函数,到这里我们只认识到了一个 run 方法,它的基础结构如下:
// webpack/lib/Compiler.js
class Compiler {
constructor(context, options = {}) {
this.hooks = Object.freeze({
initialize: new SyncHook([]),
run: new AsyncSeriesHook(["compiler"]),
done: new AsyncSeriesHook(["stats"]),
emit: new AsyncSeriesHook(["compilation"]),
make: new AsyncParallelHook(["compilation"]),
... 很多很多
});
this.options = options;
this.context = context;
}
run(callback) {}
}
- new WebpackOptionsApply().process(options, compiler)
上面「前置知识」中我们了解到:webpack 是一个插件结构化的设计架构,即每一个功能的实现都是由一个插件来完成的,比如模块的编译入口
entry
是由EntryPlugin
来管理和执行。
WebpackOptionsApply().process
中注册的内置插件很多,这里我们只关心本篇会涉及到的插件配置。
// webpack/lib/WebpackOptionsApply.js
class WebpackOptionsApply extends OptionsApply {
constructor() {
super();
}
process(options, compiler) {
new JavascriptModulesPlugin().apply(compiler);
// entry 插件
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
...
}
}
EntryOptionPlugin
会为config.entry
中的每个配置应用EntryPlugin
来注册编译入口,等后续hooks.make
时机开始入口模块的编译。JavascriptModulesPlugin
提供了parse
AST 的核心实现,后续收集模块内的所引入的deps
依赖模块时会用到。
到这里,compiler 实例创建完成,并且将相关插件注册成功。
接下来会执行 compiler.run()
开启打包。由于还没有走到真正的编译,将这部分内容放在「初始化阶段」一并介绍。
3.3、compiler.run
// webpack/lib/Compiler.js
class Compiler {
...
run(callback) {
const finalCallback = (err, stats) => {} // 所有工作都完成后的最终执行函数
const onCompiled = (err, compilation) => {} // 编译完成后的执行函数
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
}
}
首先执行了 beforeRun
和 run
两个 hook 钩子,如果有插件中注册了这两类钩子,注册的回调函数就会立刻执行。下面我们移步到 this.compile
之中。
// webpack/lib/Compiler.js
class Compiler {
...
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
compilation.finish(err => {
compilation.seal(err => {
return callback(null, compilation);
});
});
});
});
}
}
compile()
是启动编译的关键,编译实例 compilation
的参数定义、实例创建以及编译完成后的收尾工作都在这里实现。
- 首先是
this.newCompilationParams
创建 compilation 编译模块所需的参数:
// webpack/lib/Compiler.js
newCompilationParams() {
const params = {
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory()
};
return params;
}
这里,我们需要留意一下 createNormalModuleFactory
,在 webpack 中,每一个依赖模块都可以看作是一个 Module
对象,通常会有很多模块要处理,这里创建一个模块工厂 Factory。
// webpack/lib/Compiler.js
createNormalModuleFactory() {
const normalModuleFactory = new NormalModuleFactory({
context: this.options.context,
fs: this.inputFileSystem,
resolverFactory: this.resolverFactory, // resolve 模块时使用
options: this.options.module,
associatedObjectForCache: this.root,
layers: this.options.experiments.layers
});
this._lastNormalModuleFactory = normalModuleFactory;
this.hooks.normalModuleFactory.call(normalModuleFactory);
return normalModuleFactory;
}
- 创建
compilation
实例对象:
// webpack/lib/Compiler.js
newCompilation(params) {
this._cleanupLastCompilation(); // 清除上次 compilation
const compilation = this._lastCompilation = new Compilation(this, params);
this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);
return compilation;
}
Compilation
和 Compiler
都是一个 class 构造函数,实例上也包含了非常多的属性和方法。
// webpack/lib/Compilation.js
class Compilation {
constructor(compiler, params) {
this.hooks = Object.freeze({ ... });
this.compiler = compiler;
this.params = params;
this.options = compiler.options;
this.entries = new Map(); // 存储 entry module
this.modules = new Set();
this._modules = new Map(); // 存储所有 module
...
}
}
- 调用
hooks.make
这一步很关键,在上面创建得到compilation
之后,就可以进入编译阶段,编译会从入口模块开始进行。
而入口模块的准备工作是在注册的 EntryOptionPlugin
之中:
// webpack/lib/EntryOptionsPlugin.js
class EntryOptionPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
EntryOptionPlugin.applyEntryOption(compiler, context, entry);
return true;
});
}
static applyEntryOption(compiler, context, entry) {
const EntryPlugin = require("./EntryPlugin");
for (const name of Object.keys(entry)) {
const desc = entry[name];
const options = EntryOptionPlugin.entryDescriptionToOptions(compiler, name, desc);
for (const entry of desc.import) {
new EntryPlugin(context, entry, options).apply(compiler); // 注册 entry 插件
}
}
}
}
其中关键部分是为每个 entry 注册 EntryPlugin
,在 EntryPlugin 中就会看到与 hook.make
相关的逻辑:
// webpack/lib/EntryPlugin.js
class EntryPlugin {
apply(compiler) {
// 1、记录 entry 模块解析时使用 normalModuleFactory
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
EntryDependency, // key
normalModuleFactory // value
);
}
);
const { entry, options, context } = this;
// 2、为 entry 创建 Dependency 对象
const dep = EntryPlugin.createDependency(entry, options);
// 3、监听 hook.make,执行 compilation.addEntry
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {
callback(err);
});
});
}
}
现在我们清楚了:当执行 hooks.make.callAsync
时,其实就是执行 compilation.addEntry
开始入口模块的编译构建阶段。
这也是很关键的一步:找到入口进行构建。
四、构建阶段
hooks.make
是触发入口模块编译的开始,在 webpack 的构建阶段,流程如下:
- 为
entry
入口模块创建 entryData 存储在compilation.entries
,用于后续为每个入口输出chunk
; - 拿到处理
entry
模块的工厂方法moduleFactory
,开始ModuleTree
的创建,后面每个文件模块都会先生成一个Module
; - 执行
handleModuleCreation
开始处理入口模块的构建,当然,入口模块中所引入的依赖模块,构建也都是从这里开始; - 构建过程会经历
feactorize(创建 module)
、addModule
、buildModule
三个阶段,build 阶段涉及到 loader 代码转换和依赖收集; - 模块构建完成后,若存在子依赖(
module.dependencies
),回到第三步开始子依赖的构建。
下面,我们看看代码上的流程线。
4.1、构建 EntryModuleTree
从 compilation.addEntry
开始进入 entry 模块的编译,调用 _addEntryItem
创建 entryData
加入到 this.entries
集合中。
// webpack/lib/Compilation.js
class Compilation {
constructor(compiler, params) {
this.entries = new Map();
...
}
addEntry(context, entry, options, callback) {
this._addEntryItem(context, entry, "dependencies", options, callback);
}
_addEntryItem(context, entry, target, options, callback) {
const { name } = options;
let entryData = name !== undefined ? this.entries.get(name) : this.globalEntry;
if (entryData === undefined) {
entryData = {
dependencies: [],
includeDependencies: [],
options: {
name: undefined,
...options
}
};
entryData[target].push(entry);
this.entries.set(name, entryData);
}
this.hooks.addEntry.call(entry, options);
this.addModuleTree({ context, dependency: entry, contextInfo: undefined }, (err, module) => {
this.hooks.succeedEntry.call(entry, options, module);
return callback(null, module);
});
}
}
接着,执行 addModuleTree
获取 moduleFactory
即上文存储的 normalModuleFactory
:
// webpack/lib/Compilation.js
addModuleTree({ context, dependency, contextInfo }, callback) {
const Dep = dependency.constructor; // EntryDependency
// dependencyFactories.get(EntryDependency) = normalModuleFactory
const moduleFactory = this.dependencyFactories.get(Dep); // 用于后续执行 moduleFactory.create()
this.handleModuleCreation({
factory: moduleFactory,
dependencies: [dependency],
originModule: null, contextInfo, context
}, (err, result) => {
callback(null, result);
});
}
然后,执行 handleModuleCreation
:
// webpack/lib/Compilation.js
handleModuleCreation(
{
factory, // moduleFactory
dependencies, // [dep]
...
},
callback
) {
const moduleGraph = this.moduleGraph;
this.factorizeModule(
{
currentProfile: false,
factory,
dependencies,
factoryResult: true,
originModule,
contextInfo,
context
},
(err, factoryResult) => {
const newModule = factoryResult.module;
this.addModule(newModule, (err, module) => {
...
});
}
);
}
factorizeModule
有分解模块的意思,可以理解为:为 entry 创建一个 Module
。它的函数体逻辑十分简单:
// webpack/lib/Compilation.js
Compilation.prototype.factorizeModule = function (options, callback) {
this.factorizeQueue.add(options, callback);
}
4.2、模块编译所经历的阶段
看到这里会不会感到困惑。从代码来看,将 options 加入到 factorizeQueue
中流程就结束了。
其实不然,这是一个 AsyncQueue
异步队列,你可以理解为每个模块的 factorize 分解都是一个任务加入在队列中,在排到它时便会执行。
与 factorizeQueue
相似的功能队列还有 addModuleQueue
和 buildQueue
,它们在初始化 compilation
实例时的定义如下:
// webpack/lib/Compilation.js
class Compilation {
constructor(compiler, params) {
this.processDependenciesQueue = new AsyncQueue({
name: "processDependencies",
parallelism: options.parallelism || 100,
processor: this._processModuleDependencies.bind(this)
});
this.addModuleQueue = new AsyncQueue({
name: "addModule",
parent: this.processDependenciesQueue,
getKey: module => module.identifier(),
processor: this._addModule.bind(this)
});
this.factorizeQueue = new AsyncQueue({
name: "factorize",
parent: this.addModuleQueue,
processor: this._factorizeModule.bind(this)
});
this.buildQueue = new AsyncQueue({
name: "build",
parent: this.factorizeQueue,
processor: this._buildModule.bind(this)
});
}
_processModuleDependencies(module, callback) { }
_addModule(module, callback) { }
_factorizeModule(params, callback) { }
_buildModule(module, callback) { }
}
一个模块的编译会经过 factorize 创建模块
、addModule 添加模块
、buildQueue 构建模块
和 processDependencies 递归处理子依赖模块(如果有)
几个阶段。
而每个阶段的真正执行函数绑定在 Queue.processor
处理器上。
4.3、factorize 创建模块
_factorizeModule
下的链路比较长,先后经过:factory.create
--> hooks.factorize
--> hooks.resolve
--> new NormalModule
得到 module 对象。
// webpack/lib/Compilation.js
_factorizeModule(
{
currentProfile,
factory,
dependencies,
originModule,
factoryResult,
contextInfo,
context
},
callback
) {
factory.create({ context, dependencies, ... }, (err, result) => {
callback(null, factoryResult ? result : result.module);
});
}
factory.create
是创建模块 module
的开始,这里的 factory 就是创建 compilation.params 时传入的 normalModuleFactory
。
在 create()
中执行 hooks.factorize.callSync
,而注册 hooks.factorize.tapAsync
发生在初始化 NormalModuleFactory
实例时。
此外,在初始化时还注册了 hooks.resolve.tapAsync
,它的执行时机更好在 hooks.factorize.tapAsync
之中。代码如下:
// webpack/lib/NormalModuleFactory.js
class NormalModuleFactory extends ModuleFactory {
constructor() {
this.hooks.factorize.tapAsync({}, (resolveData, callback) => {
this.hooks.resolve.callAsync(resolveData, (err, result) => {
...
}
})
this.hooks.resolve.tapAsync({}, (data, callback) => {
...
})
}
create(data, callback) {
const resolveData = {
contextInfo,
resolveOptions,
context,
request,
dependencies,
dependencyType,
createData: {},
cacheable: true
...
};
this.hooks.factorize.callAsync(resolveData, (err, module) => {
const factoryResult = {
module,
fileDependencies,
missingDependencies,
contextDependencies,
cacheable: resolveData.cacheable
};
callback(null, factoryResult);
}
}
}
首先第一步是在 hooks.resolve.tapAsync
中:
- 调用
enhanced-resolve
第三方库得到资源基于 context 的绝对路径; - 根据资源后缀(文件类型)收集
webpack.config.js
中配置的 loader,得到最终的loaders
集合,这里涉及到 loader 先后顺序以及内联方式和配置方式的处理; - 根据上述信息得到一个创建 module 时所需的数据 -->
createData
。
// webpack/lib/NormalModuleFactory.js
this.hooks.resolve.tapAsync({}, (data, callback) => {
// 创建一个 normal Resolve 实例
const normalResolver = this.getResolver("normal", resolveOptions);
let resourceData, loaders;
const continueCallback = () => {
... 一系列 loader 规则处理
// 生成 create module 相关数据集合
Object.assign(data.createData, {
layer:
layer === undefined ? contextInfo.issuerLayer || null : layer,
request: stringifyLoadersAndResource(
allLoaders,
resourceData.resource
),
userRequest,
rawRequest: request,
loaders: allLoaders,
resource: resourceData.resource, // 资源的完整绝对路径
context:
resourceData.context || getContext(resourceData.resource),
matchResource: matchResourceData
? matchResourceData.resource
: undefined,
resourceResolveData: resourceData.data,
settings,
type,
parser: this.getParser(type, settings.parser), // module 的 parse 依赖收集解析器
parserOptions: settings.parser,
generator: this.getGenerator(type, settings.generator), // module 的代码生成器
generatorOptions: settings.generator,
resolveOptions
});
callback();
}
// 执行 enhanced-resolve 第三方库解析模块路径,得到 resolvedResource
this.resolveResource(
contextInfo,
context,
unresolvedResource,
normalResolver,
resolveContext,
(err, resolvedResource, resolvedResourceResolveData) => {
if (resolvedResource !== false) {
resourceData = {
resource: resolvedResource,
data: resolvedResourceResolveData,
...cacheParseResource(resolvedResource)
};
}
continueCallback();
}
);
})
有了 module createData
,接下来就是创建 NormalModule
实例得到 module
:
this.hooks.factorize.tapAsync({}, (resolveData, callback) => {
this.hooks.resolve.callAsync(resolveData, (err, result) => {
const createData = resolveData.createData;
this.hooks.createModule.callAsync(createData, resolveData, (err, createdModule) => {
// 创建 module 实例
if (!createdModule) createdModule = new NormalModule(createData);
createdModule = this.hooks.module.call(createdModule, createData, resolveData);
return callback(null, createdModule);
})
}
})
一个 module
实例上,记录了 parse 以及 loader 相关信息,包含常用的属性和方法:
// webpack/lib/NormalModule.js
class NormalModule extends Module {
constructor({ ...createData }) {
this.request = request;
this.parser = parser;
this.generator = generator;
this.resource = resource;
this.loaders = loaders;
this._source = null; // module 文件内容
}
createSource() {},
_doBuild(options, compilation, resolver, fs, hooks, callback) {}
build(options, compilation, resolver, fs, callback) {}
codeGeneration() {}
...
}
create module 完成后,将 result 回传给 callback 即回到了 this.factorizeModule
的回调中执行 addModule
:
// webpack/lib/Compilation.js
handleModuleCreation({ ... }, callback) {
this.factorizeModule({ ... }, (err, factoryResult) => {
const newModule = factoryResult.module;
this.addModule(newModule, (err, module) => {
...
});
});
}
4.4、addModule 存储模块
addModule AsyncQueue 的处理器是 _addModule
,将 module 添加到 modules
集合中,后续在 seal
「生成阶段」会遍历 modules 读取模块代码。
// webpack/lib/Compilation.js
_addModule(module, callback) {
const identifier = module.identifier();
const alreadyAddedModule = this._modules.get(identifier);
if (alreadyAddedModule) {
return callback(null, alreadyAddedModule);
}
this._modulesCache.get(identifier, null, (err, cacheModule) => {
if (cacheModule) {
cacheModule.updateCacheModule(module);
module = cacheModule;
}
this._modules.set(identifier, module);
this.modules.add(module);
callback(null, module);
});
}
module 添加完成后,执行 callback 回到 addModule
的回调中:
// webpack/lib/Compilation.js
handleModuleCreation({ ... }, callback) {
this.factorizeModule({ ... }, (err, factoryResult) => {
const newModule = factoryResult.module;
this.addModule(newModule, (err, module) => {
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i]; // entry dep
moduleGraph.setResolvedModule(
connectOrigin ? originModule : null,
dependency,
module
);
}
this._handleModuleBuildAndDependencies(
originModule,
module,
recursive,
callback
);
});
});
}
4.5、buildModule 构建模块
接下来执行 _handleModuleBuildAndDependencies
进入 build 阶段。
build 阶段做了以下几件事情:
- 创建 loader context 上下文;
- 调用
loader-runner
第三方库提供的runLoaders()
执行loader
进行代码转换,这里会传入resource
、loaders
、loaderContext
,得到一个转换后的源代码 result; 3、执行createSource
创建RawSource
实例到module._source
上,通过_source.source()
拿到文件转换后的源代码; - 执行
parse
(JavascriptParser) 对source
进行 ast 解析,收集模块的依赖集合到module.dependencies
中。
下面我们看看代码上的实现。
// webpack/lib/Compilation.js
_handleModuleBuildAndDependencies(originModule, module, recursive, callback) {
this.buildModule(module, err => {
...
})
}
首先判断是否需要 build,初次打包都会需要,接着执行 module.build
即 NormalModule.build
,并执行 _doBuild
进行打包。
// webpack/lib/Compilation.js
_buildModule(module, callback) {
module.needBuild({ ... }, (err, needBuild) => {
this.hooks.buildModule.call(module);
this.builtModules.add(module);
module.build(
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,
err => {
...
}
)
})
}
// webpack/lib/NormalModule.js
build(options, compilation, resolver, fs, callback) {
return this._doBuild(options, compilation, resolver, fs, hooks, err => {
...
})
}
在 _doBuild
中创建 loader 执行上下文,通过 runLoaders
执行 loader 转换代码,最后得到 this._source
对象。
// webpack/lib/NormalModule.js
_doBuild(options, compilation, resolver, fs, hooks, callback) {
// 创建 loader 上下文
const loaderContext = this._createLoaderContext(resolver, options, compilation, fs, hooks);
// 生成 _source 对象
const processResult = (err, result) => {
const source = result[0];
const sourceMap = result.length >= 1 ? result[1] : null;
this._source = this.createSource(
options.context,
this.binary ? asBuffer(source) : asString(source),
sourceMap,
compilation.compiler.root
);
return callback();
}
// 调用 loader 进行代码转换
runLoaders({
resource: this.resource,
loaders: this.loaders, // 配置的 loader
context: loaderContext,
}, (err, result) => {
processResult(err, result.result);
})
}
_doBuild 执行完毕后回到 build 作用域下,对经过 loader 转换后的源代码进行 parse ast
解析,收集依赖模块,并生成 build module hash。
// webpack/lib/NormalModule.js
build(options, compilation, resolver, fs, callback) {
return this._doBuild(options, compilation, resolver, fs, hooks, err => {
const handleParseResult = result => {
this._initBuildHash(compilation);
return handleBuildDone();
}
const handleBuildDone = () => {
// 创建快照
compilation.fileSystemInfo.createSnapshot(..., (err, snapshot) => {
return callback();
})
}
const source = this._source.source();
// 这里的 parse 是由 JavascriptParser.js 提供
result = this.parser.parse(this._ast || source, {
source,
current: this,
module: this,
compilation: compilation,
options: options
});
handleParseResult(result);
})
}
最后回到 build 时传递的 callback,将 module 存储在 _modulesCache
中:
// webpack/lib/Compilation.js
module.build(
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,
err => {
this._modulesCache.store(module.identifier(), null, module, err => {
this.hooks.succeedModule.call(module);
return callback();
});
}
)
至此,module 阶段就已完成,回到 this.buildModule
中执行 processModuleDependencies
处理依赖模块。
// webpack/lib/Compilation.js
this.buildModule(module, err => {
this.processModuleDependencies(module, err => {
callback(null, module);
});
})
4.6、processModuleDependencies
如果模块存在 dependencies
依赖,则会对子模块调用 handleModuleCreation()
进行上述构建步骤,否则执行 callback 模块编译结束。
// webpack/lib/Compilation.js
_processModuleDependencies(module, callback) {
// 没有要处理的依赖
if (sortedDependencies.length === 0 && inProgressTransitive === 1) {
return callback();
}
// 处理子依赖
for (const item of sortedDependencies) {
this.handleModuleCreation(item, err => {
...
});
}
}
现在,所有的依赖处理完成,依次完成 this.handleModuleCreation
---> this.addModuleTree
---> compilation.addEntry
---> compiler.hooks.make
。
4.7、compilation.finish
进入了 compilation.finish
意味着模块的 make
打包制作阶段完成。在这里,调用 hooks.finishModules
并收集模块构建过程中产生的 errors
和 warnings
。
// webpack/lib/Compilation.js
class Compilation {
constructor(compiler, params) {
this.errors = [];
this.warnings = [];
}
finish(callback) {
this.factorizeQueue.clear();
const { modules } = this;
this.hooks.finishModules.callAsync(modules, err => {
for (const module of modules) {
// 收集 error
const errors = module.getErrors();
if (errors !== undefined) {
for (const error of errors) {
this.errors.push(error);
}
}
// 收集 warning
const warnings = module.getWarnings();
if (warnings !== undefined) {
for (const warning of warnings) {
this.warnings.push(warning);
}
}
}
this.moduleGraph.unfreeze();
callback();
});
}
}
最后
感谢阅读。
第二篇介绍了「生成阶段」和「写入阶段」,可以移步到这里查看 webpack5 打包流程源码剖析(2)。
转载自:https://juejin.cn/post/7128705370335215630