by:沐沐
其实阅读 webpack 源码也是一个非常大的挑战,想说明清楚也是非常难,在我们团队做分享时候被 leader 点醒,画图下面一个图来形象的说明 webpack 核心部分内容,就先上一个图。
注:图中的*,对于第一个图是接口,能够挂各种各样的设备来对水进行处理;第二个图是hooks,同步、异步、阻塞等各种类型hooks来对文件流进行处理
webpack本质:可以将其理解是一种基于事件流的编程范例,一系列的插件运行。
插件
这里先做一些先行步骤,先了解一些 webpack 用到的一些核心插件。
args
yargs主要用来处理参数系统插件(webpack-cli引入):提供命令和分组参数,分析命令行参数,对各个参数进行转换,组成编译配置项。
cli/command
对命令定制,解析参数,引用webpack,根据配置项进行编译和构建。
Tapable
主要是控制钩子函数的发布与订阅,控制着 webpack 的插件系统。Tapable 库暴露了很多 Hook(钩子)类,为插件提供挂载的钩子
类似于 Node.js 的 EventEmitter 的库,主要是控制钩子函数的发布与订阅,控制着 webpack 的插件系统。暴露了很多 Hook(钩子)类,为插件提供挂载的钩子。
注:这里可以想想本篇第一张对比图,是不是各种类型的钩子基本都可以在图中找到痕迹?
const {
SyncHook, //同步钩子
SyncBailHook, //同步熔断钩子
SyncWaterfallHook, //同步流水钩子
SyncLoopHook, //同步循环钩子
AsyncParallelHook, //异步并发钩子
AsyncParallelBailHook, //异步并发熔断钩子
AsyncSeriesHook, //异步串行钩子
AsyncSeriesBailHook, //异步串行熔断钩子
AsyncSeriesWaterfallHook //异步串行流水钩子
} = require("tapable");
Compiler.js:
class Compiler extends Tapable {
/* code */
}
Compilation.js:
class Compilation extends Tapable {
/* code */
}
从执行命令开始
npm run start 后发生了什么?
开始执行
进入node_modules\.bin 查找 webpack.sh 或者 webpack.cmd 或者 webpack.ps1 文件(对于 windows 的 powershell,可以执行的是 ps1,这里应该是针对不同终端做的,比如 git 终端可以执行的是 shell)
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
& "$basedir/node$exe" "$basedir/../webpack/bin/webpack.js" $args
$ret=$LASTEXITCODE
} else {
& "node$exe" "$basedir/../webpack/bin/webpack.js" $args
$ret=$LASTEXITCODE
}
exit $ret
最终执行文件是webpack.js(webpack/bin/webpack.js),然后可以查看源码:
process.exitCode = 0; // 1. 正常执行返回 const runCommand = (command, args) =>{...}; // 2. 运行某个命令 const isInstalled = packageName =>{...}; // 3. 判断某个包是否安装 const CLIs =[...]; // 4. webpack 可用的 cli: webpack-cli 和 webpack-command const installedClis = CLIs.filter(cli => cli.installed); // 5. 判断是否两个 cli 是否安装了 if (installedClis.length === 0){...}else if // 6. 根据安装数量进行处理 (installedClis.length)
发现这里会查找并进入 webpack-cli
webpack-cli
后面可以具体查看 webpack-cli/bin/config/config-yargs.js 等了解更多命令,这里主要使用了yargs进行参数处理
Config options: 配置相关参数(文件名称、运行环境等)Basic options: 基础参数(entry设置、debug模式设置、watch监听设置、devtool设置)Module options: 模块参数,给loader设置扩展Output options: 输出参数(输出路径、输出文件名称)Advanced options: 高级用法(记录设置、缓存设置、监听频率、bail等)Resolving options: 解析参数(alias和 解析的文件后缀设置)Optimizing options: 优化参数Stats options: 统计参数options: 通用参数(帮助命令、版本信息等)
webpack-cli 对配置文件和命令行参数进行转换最终生成配置选项参数 options,最终会根据配置参数实例化 webpack 对象,然后执行构建流程。
let compiler;
try {
compiler = webpack(options);
} catch (err) {
if (err.name === "WebpackOptionsValidationError") {
if (argv.color) console.error(`\u001b[1m\u001b[31m${err.message}\u001b[39m\u001b[22m`);
else console.error(err.message);
// eslint-disable-next-line no-process-exit
process.exit(1);
}
throw err;
}
webpack
可以将其理解是一种基于事件流的编程范例,一系列的插件运行。
进入webpack主要处理流程
webpack的编译都按照下面的钩子调用顺序执行
开始
查看 webpack.js 可以发现,如果 options 是数组则调用 new MultiCompiler(),然后递归 webpack 方法;否则调用 new Compiler();然后compiler.options = new WebpackOptionsApply().process(options, compiler) 绑定 options。这里可以看到几个核心部分的关系。
let compiler;
if (Array.isArray(options)) {
compiler = new MultiCompiler(
Array.from(options).map(options => webpack(options))
);
} else if (typeof options === "object") {
options = new WebpackOptionsDefaulter().process(options); // 参数处理
compiler = new Compiler(options.context); // Compiler
compiler.options = options;
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
compiler.hooks.environment.call(); // 环境
compiler.hooks.afterEnvironment.call();
compiler.options = new WebpackOptionsApply().process(options, compiler); // 参数
} else {
throw new Error("Invalid argument: options");
}
处理参数:
这里可以查看 webpack 源码中的 WebpackOptionsApply.js 文件,发现里边是各种的配置参数到 plugin 的转换。
// ...
const JavascriptModulesPlugin = require("./JavascriptModulesPlugin");
const JsonModulesPlugin = require("./JsonModulesPlugin");
// 各种plugin引入
class WebpackOptionsApply extends OptionsApply {
process(options, compiler) {
if (typeof options.target === "string") {
let JsonpTemplatePlugin;
let FetchCompileWasmTemplatePlugin;
let ReadFileCompileWasmTemplatePlugin;
// plugin 变量声明
switch (options.target) {
case "web":
JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin");
FetchCompileWasmTemplatePlugin = require("./web/FetchCompileWasmTemplatePlugin");
NodeSourcePlugin = require("./node/NodeSourcePlugin");
// ...
}
// ...
}
// ...
}
// ...
}
开始编译:
compiler.run((err, stats) => {
if (compiler.close) {
compiler.close(err2 => {
compilerCallback(err || err2, stats);
});
} else {
compilerCallback(err, stats);
}
});
编译
compiler.run:
可以具体查看 run 方法:
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => { // run
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
onCompiled:
const onCompiled = (err, compilation) => { }
this.compile:
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => { // make
if (err) return callback(err);
compilation.finish(err => {
if (err) return callback(err);
compilation.seal(err => { // seal
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
}
Compiler
相关 hooks 有,流程相关:
- (before-)run
- (before-/after-)compile
- make
- (after-)emit
- done
监听相关:
- watch-run
- watch-close
也可以查看Compiler.js的constructor注意:
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
/** @type {SyncBailHook<Compilation>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<Stats>} */
/* 一堆的 hooks 定义 */
/** @type {SyncBailHook<string, Entry>} */
entryOption: new SyncBailHook(["context", "entry"])
};
// TODO webpack 5 remove this
this.hooks.infrastructurelog = this.hooks.infrastructureLog;
// ...
}
}
Compilation
这也是 webpack 比较核心的一个功能,Compiler 调用 Compilation 生命周期方法,这里可以通过查看源码。
addEntry→addModuleChain(添加入口)finish(上报模块错误)sea(构建完成,内容输出、生成和优化这一块内容)
class Compiler extends Tapable {
run(callback) {
// ...
const onCompiled = (err, compilation) => {
// ...
this.emitAssets(compilation, err => {
// ...
if (compilation.hooks.needAdditionalPass.call()) {
// ...
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
this.hooks.additionalPass.callAsync(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
return;
}
// ...
});
};
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
}
// ...
createCompilation() {
return new Compilation(this);
}
newCompilation(params) {
const compilation = this.createCompilation();
// ...
return compilation;
}
compile(callback) {
// ...
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish(err => {
if (err) return callback(err);
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
}
}
详细钩子可以查看 Compilation.js 源码中的 constructor:
class Compilation extends Tapable {
/**
* Creates an instance of Compilation.
* @param {Compiler} compiler the compiler which created the compilation
*/
constructor(compiler) {
super();
this.hooks = {
/** @type {SyncHook<Module>} */
buildModule: new SyncHook(["module"]),
/** @type {SyncHook<Module>} */
rebuildModule: new SyncHook(["module"]),
/* 同样一堆 hooks 定义 */
/** @type {SyncBailHook<Chunk[]>} */
optimizeExtractedChunksAdvanced: new SyncBailHook(["chunks"]),
/** @type {SyncHook<Chunk[]>} */
afterOptimizeExtractedChunks: new SyncHook(["chunks"])
};
// ...
}
// ...
}
ModuleFactory
开始进入模块构建阶段,Compiler 中会创建两个 ModuleFactory(模块工厂对象),分别是 NormalModuleFactory 和 ContextModuleFactory(其实他们两个也是继承自 Tapable)。
createNormalModuleFactory() {
const normalModuleFactory = new NormalModuleFactory(
this.options.context,
this.resolverFactory,
this.options.module || {}
);
this.hooks.normalModuleFactory.call(normalModuleFactory);
return normalModuleFactory;
}
createContextModuleFactory() {
const contextModuleFactory = new ContextModuleFactory(this.resolverFactory);
this.hooks.contextModuleFactory.call(contextModuleFactory);
return contextModuleFactory;
}
newCompilationParams() {
const params = {
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory(),
compilationDependencies: new Set()
};
return params;
}
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
});
}
NormalModuleFactory 是指比如代码中经常写 module.exports,直接导出,然后可以通过 require 和 import 来引入;
ContextModuleFactory 是前面的一些路径之类的。
Module
包含比较多的模块,但是都是继承自 Module class,常见的有:
NormalModule(普通模块)ContextModule(./src/a)ExternalModule(module.exports = jQuery)DelegatedModule(manifest)MultiModule(entry: ['a', 'b'])
这里就是 Compiler 调用 Compilation 的各种方法,处理 js/ts 的模块(也就是 import、request等),进行打包。
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => { // make
if (err) return callback(err);
compilation.finish(err => {
if (err) return callback(err);
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
}
总结
回归头部第一张图片,其实 webpack 主流程就是类似于水流,不同地方挂载不同的 hooks,来对文件流进行处理,也就是 webpack 就是对 hooks 调用过程。
plugin:
写过 plugin 会发现,写插件主要核心就是绑定一些 hooks,然后 webpack 主流程会在合适的时机来触发,达到对文件流的处理。
module:
这一块其实就是对文件模块的解析、处理。
loader:
这个是处理不同文件的加载方式,用来解析文件的,只有能正确解析的进来的文件才能当做文件流处理。
招贤纳士
青藤前端团队是一个年轻多元化的团队,坐落在有九省通衢之称的武汉。我们团队现在由 20+ 名前端小伙伴构成,平均年龄26岁,日常的核心业务是网络安全产品,此外还在基础架构、效率工程、可视化、体验创新等多个方面开展了许多技术探索与建设。在这里你有机会挑战类阿里云的管理后台、多产品矩阵的大型前端应用、安全场景下的可视化(网络、溯源、大屏)、基于Node.js的全栈开发等等。
如果你追求更好的用户体验,渴望在业务/技术上折腾出点的成果,欢迎来撩~ yan.zheng@qingteng.cn