Before
上文webpack5源码之旅 - webpack-cli讲到通过webpack-cli或者其它任何方式处理完参数后,
都会执行webpack(config)。
接下来我们就进入webpack一探究竟。
require('webpack')
webpack的入口文件是lib/index.js(package.json中配置的main值),也就是require('webpack')时执行的文件。
const mergeExports = (obj, exports) => {
const descriptors = Object.getOwnPropertyDescriptors(exports);
for (const name of Object.keys(descriptors)) {
const descriptor = descriptors[name];
if (descriptor.get) {
const fn = descriptor.get;
Object.defineProperty(obj, name, {
configurable: false,
enumerable: true,
get: memoize(fn)
});
} else if (typeof descriptor.value === "object") {
Object.defineProperty(obj, name, {
configurable: false,
enumerable: true,
writable: false,
value: mergeExports({}, descriptor.value)
});
} else {
throw new Error(
"Exposed values must be either a getter or an nested object"
);
}
}
return /** @type {A & B} */ (Object.freeze(obj));
};
const fn = lazyFunction(() => require("./webpack"));
module.exports = mergeExports(fn, {...})
执行mergeExports方法,传入fn和一个对象exports。返回fn和exports对象merge后的对象(属性只读,不能删,不可修改)。(getOwnPropertyDescriptors,返回属性的描述对象。)
所以module.exports出去的其实就是fn,只不过增加了一堆属性。
再看fn,它执行了lazyFunction:
const memoize = fn => {
let cache = false;
/** @type {T} */
let result = undefined;
return () => {
if (cache) {
return result;
} else {
result = fn();
cache = true;
// Allow to clean up memory for fn
// and all dependent resources
fn = undefined;
return result;
}
};
};
const lazyFunction = factory => {
const fac = memoize(factory);
const f = /** @type {any} */ (
(...args) => {
return fac()(...args);
}
);
return /** @type {T} */ (f);
};
lazyFunction把你传入的方法用memoize包了一下,返回一个新的方法。
而memoize做的事情就是把你传入的方法(() => require("./webpack"))执行一次,然后把结果缓存起来,下次再调用的时候直接返回缓存的结果。
上面的mergeExports方法,处理get的时候也用了memoize包一下,所以webpack中的所有方法都会在第一次调用后被缓存起来。
接着进入到lib/webpack.js文件。
webpack(config)
const webpack = /** @type {WebpackFunctionSingle & WebpackFunctionMulti} */ (
(options, callback) => {...}
);
module.exports = webpack;
这个文件导出一个函数webpack,这个函数接收options和callback作为参数,到这里就真正进入到打包环节。
我们通常将整个编译过程分为三个阶段:
- 初始化阶段
- 构建阶段
- 生成阶段
本文先介绍一下整个阶段会常遇到的一些模块、概念以及用到的独立的库,留一个印象,便于后续的阅读。
Compiler
编译管理器,传入options生成Compiler对象,这个对象是唯一的,会一直存活至全流程结束。
它控制着主流程,具体的事情会交给具体的模块处理。
const compiler = new Compiler(options.context, options); // 创建Compiler对象
Compilation
单次编辑过程的管理器,在构建阶段处理模块,保存构建信息。 每次文件变更触发重新编译时,都会创建一个新的 compilation 对象。
const params = {
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory()
}; // params为创建Compilation对象时需要的参数
const compilation = this.newCompilation(params); // 创建Compilation对象
Entry
入口,编译的起点,通常情况下webpack会从从入口开始寻找到所有的依赖,打成一个chunk。
...
const { entry, options, context } = this;
const dep = EntryPlugin.createDependency(entry, options); // 创建Dependency对象
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {
callback(err);
});
}); // 注册make钩子,执行compilation.addEntry方法
...
static createDependency(entry, options) {
const dep = new EntryDependency(entry); // 入口文件会被实例化为EntryDependency
// TODO webpack 6 remove string option
dep.loc = { name: typeof options === "object" ? options.name : options };
return dep;
}
Dependency
依赖对象,webpack 基于该类型记录模块间依赖关系。 上面Entry的代码中可以看到createDependency里,实例化了EntryDependency, 这个类继承自Dependency,还有很多很多不同种类的**Dependency,都继承自Dependency。
Module
在webpack的世界中,万事万物都是模块,js、css、图片或者视频,都会变成**Module。
普通的模块会生成NormalModule,创建Compilation对象的时候传入的params中有一个normalModuleFactory,
这个factory就是创建NormalModule的地方。
NormalModule中会调用对应的parse方法将源码解析为AST(抽象语法树),从而解析出所有依赖。
构建模块时,还会生成moduleGraph来记录模块之间引用信息。
Chunk
输出时,webpack会将之前生成的所有module,按照一定的规则打成一个或者多个chunk。webpack内置的chunk规则有:
- 从entry开始,找到所有依赖的module,打包成一个chunk
- 使用动态引入语句引入的模块,打包成一个chunk
Loader
webpack原生只支持js的处理,像是css、图片等就都需要通过loader转为js,才能被webpack处理。
所以通常我们会在配置文件中配置各种rules,这些rules会在实例化normalModuleFactory时进行处理。
module.exports = {
...,
module: {
rules: [
{test: /\.ts$/, loader: 'ts-loader'},
]
}
}
Plugin
相信用过webpack的小伙伴都不对Plugin感到陌生,通常都会写几个Plugin到配置文件中。 其实webpack的各种能力基本都是通过Plugin实现的,在初始化Compiler后,会挂载你配置的所有plugin, 然后在中WebpackOptionsApply,根据配置再去挂载各种内置Plugin(即使没有配置文件,它也会默认给你配置一些功能)。
const createCompiler = rawOptions => {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
const compiler = new Compiler(options.context, options);
...
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
} // 挂载配置的plugin
new WebpackOptionsApply().process(options, compiler); // 根据配置挂载内置plugin
compiler.hooks.initialize.call();
return compiler;
};
使用Plugin实现各种功能的好处就是这些Plugin都是与webpack解耦的,有很强的可扩展性。
class EntryOptionPlugin {
/**
* @param {Compiler} compiler the compiler instance one is tapping into
* @returns {void}
*/
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
EntryOptionPlugin.applyEntryOption(compiler, context, entry);
return true;
});
}
...
}
每个Plugin都会有一个apply方法,它接收compiler作为参数,然后往上挂载钩子(hooks),然后就可以在特定的节点介入编译过程。
这个过程是基于Tapable实现的,下面也会简单介绍Tapable。
Parser
上面Module部分提到了Parser,webpack中最常用到的Parser就是JavascriptParser,它使用acorn将js解析为AST, 然后就可以分析中其中的所有依赖,还可以解析注释等等。
Tapable
这是webpack中最重要的一个库,基本上每个功能模块都可以看到它,Plugin的实现就是依赖于Tapable。它是一种发布订阅模式。
entryOption: new SyncBailHook(["context", "entry"]) // 定义钩子
实例化一个钩子,并定义回调函数的行参。
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
EntryOptionPlugin.applyEntryOption(compiler, context, entry);
return true;
});
}
使用tap方法注册事件,并传入回调函数。
compiler.hooks.entryOption.call(options.context, options.entry);
使用call方法触发事件,这时候就会执行注册时传入的回调函数,并传入参数。
理解Tapable是读webpack源码的基础,参考的部分有相关文章,可以进行阅读学习,这边总结一下Tapable的几种钩子以及它们对应的注册和调用方法。
hook | 描述 | 注册方法 | 调用方法 |
---|---|---|---|
SyncHook | 同步 | tap | call |
SyncBailHook | 同步保险 | tap | call |
SyncWaterfallHook | 同步瀑布 | tap | call |
SyncLoopHook | 同步循环 | tap | call |
AsyncParallelHook | 异步并行 | tap、tapAsync、tapPromise | call、callAsync、promise |
AsyncParallelBailHook | 异步并行保险 | tap、tapAsync、tapPromise | call、callAsync、promise |
AsyncSeriesHook | 异步串行 | tap、tapAsync、tapPromise | call、callAsync、promise |
AsyncSeriesBailHook | 异步串行保险 | tap、tapAsync、tapPromise | call、callAsync、promise |
AsyncSeriesWaterfallHook | 异步串行瀑布 | tap、tapAsync、tapPromise | call、callAsync、promise |
Summary
介绍了webpack的入口文件,以及后续编译过程会遇到的一些概念,下一节我们就从初始化开始了解webpack到底是如何工作的。
Reference
1. webpack5 源码详解 - 先导
2. [万字总结] 一文吃透 Webpack 核心原理
3. 【中级/高级前端】为什么我建议你一定要读一读 Tapable 源码?