前言
在了解原理之前,我们需要手动配置并运行过简单的webpack,如:

重要的几个点:
- entry: 入口文件
- module: 模块,一个文件是一个模块
- loader:文件转换器,能让你在js中引入各种其他文件如 .txt .css
- plugin:插件,最重要,实现各种功能
- chunk:代码块,按需加载中的分块文件
基于node环境,js代码有了操作计算机文件的权限,因此,webpack就是一堆js代码,然后去折腾一堆文件。
工厂流水线
webpck可以看成是一个工厂的流水线。
一个饮料流水线的例子

从图中可以看到,整个过程是从左至右进行的,紫色的字所代表的,就是一个大的操作集, 操作集中可以包含多个子操作,如 发酵 操作集中,有3个子操作,也就是说只有完成这3个子操作,才能结束发酵这个操作集,才能进入到下一个操作集:调配。
webpack 流程
现在,我们把webpack带入到流程图中,蓝色大字。

解析1: hook(钩子)
图中的每一个紫色操作集,在webpack中,都是一个hook的实例,hook是一个类,定义了一些方法如,添加plugin,运行所有plugin等等。hook有多种,同步or异步执行plugin,是否带返回值等....
例如

const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
var hook = new SyncHook() // 实例一个同步的hook
hook.addPlugin(plugin1)
hook.addPlugin(plugin2)
...
Complier.emit = function() {
hook.runAllPlugins()
}
Complier.emit() // 运行
解析2:Complier,Compliation
图中可以发现,加粗的蓝色为Compliation。webpack中Complier负责整个的构建流程(准备、编译、输出等),而Compliation只负责其中的 编译 过程。还有,Compliation只代表一次编译,也就是说,每当文件有变动,就重新生成一个Compliation实例,即一个文件结构,对应一个Compliation。
一个Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。
解析3:运行流程
对于紫色操作集,webpack中是定义 死 的,所以按顺序执行代码就行
Complier.beforeRun() // 执行对应的hook中的一堆plugin
Complier.run() // 执行对应的hook中的一堆plugin
Complier.make() // 执行对应的hook中的一堆plugin执行一堆plugin
Compliation.buildModule() // 执行对应的hook中的一堆plugin执行一堆plugin
...
而每个里面的plugin是活的,且允许用户自定义添加plugin,之前说了是hook类提供的plugin相关的方法,接下来讲一下hook是怎么玩的 ==> Tabable
Tabable

tapable库暴露了很多Hook(钩子)类,为插件提供挂载的钩子。
我们主要看这hook类,其他的Hook可以后自行研究
exports.SyncHook = require("./SyncHook");
SyncHook.js
继承自Hook.js,这里我就都写一起了,挑干的
class SyncHook {
constructor() {
// 存放plugins
this.taps = []
}
// 添加plugin
tap(name, fn) {
item = Object.assign({}, {type: 'sync', name, fn})
// 添加plugin
this.taps.push(item)
// 实际上,在push之前,还有会注册拦截器、对taps中的plugins进行排序等,这里只简单模拟下
}
// 执行plugins
// 这个call,走的是SyncHook.js中的compile()
call() {
// factory.setup(this, options);
// return factory.create(options);
// 实际上后面走的callTapsSeries、callTap
// 模拟
this.taps.forEach(tap => { // 逐个plugin执行
tap.fn()
})
}
}
所以,hook就是订阅发布设计模式,将plugin的回调函数存到数组中,再集中执行。
Compiler.js
class Compiler {
constructor() {
super();
this.hooks = { // 一大堆hooks
shouldEmit: new SyncBailHook(["compilation"]),
done: new AsyncSeriesHook(["stats"]),
additionalPass: new AsyncSeriesHook([]),
beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(["compiler"]),
emit: new AsyncSeriesHook(["compilation"]),
afterEmit: new AsyncSeriesHook(["compilation"]),
compilation: new SyncHook(["compilation", "params"]),
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
make: new AsyncParallelHook(["compilation"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
watchRun: new AsyncSeriesHook(["compiler"]),
environment: new SyncHook([]),
afterEnvironment: new SyncHook([]),
afterPlugins: new SyncHook(["compiler"]),
entryOption: new SyncBailHook(["context", "entry"])
}
}
run() {
// 这个callAsync就是上面hook类中的call,只不过是异步的
this.hooks.beforeRun.callAsync(this, err => {
this.hooks.run.callAsync(this, err => {
this.readRecords(err => {
this.compile(); //
});
});
});
}
compile() {
this.hooks.beforeCompile.callAsync(err => {
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync()
...
})
}
}
从上面我们可以看到了,this.hooks里定义了全部的 操作子集 , 而run、compile、等都是操作集,每个操作集里面会运行多个子集,这就和之前的流程图对应上了。
不过,上面都是执行hook的call方法,那么,hook里面的plugins是什么时候tap加进去的呢?接着看


所以
plugin
class MyPlugin {
apply(compiler) { // 接收传过来的compiler实例
compiler.hooks.beforeRun.tap('MyPlugin', function() {
// 回调函数
console.log(3333)
file等操作...
})
// 可以注册多个不同的hook
compiler.hooks.afterCompile.tap('MyPlugin', function() {
// 回调函数
console.log(444)
file等操作...
})
}
}
class YouPlugin {
apply(compiler) { // 接收传过来的compiler实例
compiler.hooks.compile.tap('YouPlugin', function() {
// 回调函数
console.log(444)
file等操作...
})
}
}
webpack.js
const webpack = (options) => {
let compiler = new Compiler()
// 循环plugins,把每个子plugin添加到对应的compiler的操作集上
for (const plugin of options.plugins) {
plugin.apply(compiler); // 将plugin注册添加到compiler对应中
}
// 注册plugin结束
// 开始运行。。。
compiler.run()
}
执行!
const options = {
plugins: [
new MyPlugin(),
new YouPlugin()
]
}
webpack(options)
!!!想查看一些插件是什么时候加到hook中的,打开WebpackOptionsApply.js文件
到这里,webpack的整体运行流程就差不多了,接下来,我们逐个研究流程中的各个操作集细节~
源码部分
注意:按文件搜索目标文件,比如想查看hooks.beforeRun的插件是什么时候注册的,搜索lib文件夹hooks.beforeRun.tap,若按照代码去找,需要花很长时间。。。

主要研究以下这些
- Compiler.run()
- Compiler.compile() 开始编译
- Compiler.make() 从入口分析依赖以及间接依赖模块,创建模块对象
- Compilation.buildModule() 模块构建
- Compiler.normalModuleFactory() 构建
- Compilation.seal() 构建结果封装, 不可再更改
- Compiler.afterCompile() 完成构建,缓存数据
- Compiler.emit() 输出到dist目录
Compiler.run()
// Compiler.js
run() {
// NodeEnvironmentPlugin.js 中 beforeRun添加plugins
this.hooks.beforeRun.callAsync(this, err => {
// CachePlugin.js 中 beforeRun添加plugins
this.hooks.run.callAsync(this, err => {
this.readRecords(err => {
// 开始编译,重要
this.compile(onCompiled);
});
});
});
// 按文件搜索this.hooks.beforeRun.tap 就能找到在哪个js中添加的,其他插件同样这样搜
}
Compiler.compile() 重要
// Compiler.js
compile() {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
this.hooks.compile.call(params);
// 新建一个 compilation
// compilation 里面也定义了 this.hooks , 原理和 Compiler 一样
const compilation = this.newCompilation(params);
// 执行make函数,重要的
this.hooks.make.callAsync(compilation, err => {
compilation.finish(err => {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {
return callback(null, compilation);
});
});
});
});
}
this.hooks.make 方法
一个新的compilation对象创建完毕, 即将从entry开始读取文件,根据文件类型和编译的loader对文件进行==编译==,编译完后再找出该文件依赖的文件,递归地编译和解析
// 搜索lib目录,compiler.hooks.make.tap
// 主要定位到文件SingleEntryPlugin.js,DllEntryPlugin.js,
// SingleEntryPlugin.js
apply(compiler) {
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
// 进入compilation.addEntry方法
compilation.addEntry(context, dep, name, callback);
}
);
}
Compilation.addEntry
// 主要执行的_addModuleChain方法
_addModuleChain方法
// 主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。
通过 *ModuleFactory.create方法创建模块,(有NormalModule , MultiModule , ContextModule , DelegatedModule 等)
对模块使用的loader进行加载。
调用 acorn 解析经 loader 处理后的源文件生成抽象语法树 AST。遍历 AST,构建该模块所依赖的模块
_addModuleChain() {
moduleFactory.create(rsu => { // 打开 NormalModuleFactory.js 查看 create 方法
// 收集一系列信息然后创建一个module传入回调
// 执行this.buildModule方法方法,重要
this.buildModule() // 见下方
})
}
buildModule
buildModule(module, optional, origin, dependencies, thisCallback) {
// 触发buildModule事件点
this.hooks.buildModule.call(module);
//!!! 开始build,主要执行的是NormalModuleFactory生成的NormalModule中的build方法,中的build方法,打开NormalModule
module.build( // doBuild
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,
error => {
......
}
);
NormalModule.js
build() {
// 先看执行的doBuild
return this.doBuild(options, compilation, resolver, fs, err => {
// 调用parse方法,创建依赖Dependency并放入依赖数组
try {
// 调用parser.parse
const result = this.parser.parse(
this._ast || this._source.source(),
{
current: this,
module: this,
compilation: compilation,
options: options
},
(err, result) => {
if (err) {
handleParseError(err);
} else {
handleParseResult(result);
}
}
);
if (result !== undefined) {
// parse is sync
handleParseResult(result);
}
} catch (e) {
handleParseError(e);
}
})
}
doBuild(options, compilation, resolver, fs, callback) {
runLoaders(rsu => { // 获取loader相关的信息并转换成webpack需要的js文件,原理见下方链接
callback()// 回build中执行ast(抽象语法树,编译过程中常见结构,vue、babel原理都有)
})
}
runLoaders:segmentfault.com/a/119000001…
对于当前模块,或许存在着多个依赖模块。当前模块会开辟一个依赖模块的数组,在遍历 AST 时,将 require() 中的模块通过 addDependency() 添加到数组中。当前模块构建完成后,webpack 调用 processModuleDependencies 开始递归处理依赖的 module,接着就会重复之前的构建步骤。
Compilation.prototype.addModuleDependencies = function(module, dependencies, bail, cacheGroup, recursive, callback) {
// 根据依赖数组(dependencies)创建依赖模块对象
var factories = [];
for (var i = 0; i < dependencies.length; i++) {
var factory = _this.dependencyFactories.get(dependencies[i][0].constructor);
factories[i] = [factory, dependencies[i]];
}
...
// 与当前模块构建步骤相同
}
最后, 所有的模块都会被放入到Compilation的modules里面, 如下:


总结一下:
module 是 webpack 构建的核心实体,也是所有 module 的 父类,它有几种不同子类:NormalModule , MultiModule ,
ContextModule , DelegatedModule 等,一个依赖对象(Dependency,还未被解析成模块实例的依赖对象。
比如我们运行 webpack 时传入的入口模块,或者一个模块依赖的其他模块,都会先生成一个 Dependency 对象。)
经过对应的工厂对象(Factory)创建之后,就能够生成对应的模块实例(Module)。
Compilation.seal
buildModule后,就到了Seal封装构建结果这一步骤
seal(callback) {
// 触发事件点seal
this.hooks.seal.call();
// 生成chunk
for (const preparedEntrypoint of this._preparedEntrypoints) {
const module = preparedEntrypoint.module;
const name = preparedEntrypoint.name;
// 整理每个Module和chunk,每个chunk对应一个输出文件。
const chunk = this.addChunk(name);
const entrypoint = new Entrypoint(name);
entrypoint.setRuntimeChunk(chunk);
entrypoint.addOrigin(null, name, preparedEntrypoint.request);
this.namedChunkGroups.set(name, entrypoint);
this.entrypoints.set(name, entrypoint);
this.chunkGroups.push(entrypoint);
GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
GraphHelpers.connectChunkAndModule(chunk, module);
chunk.entryModule = module;
chunk.name = name;
this.assignDepth(module);
}
this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice());
this.sortModules(this.modules);
this.hooks.afterChunks.call(this.chunks);
this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
......
this.hooks.beforeChunkAssets.call();
this.createChunkAssets(); // 生成对应的Assets
this.hooks.additionalAssets.callAsync(...)
});
}
每个 chunk 的生成就是找到需要包含的 modules。这里大致描述一下 chunk 的生成算法:
1.webpack 先将 entry 中对应的 module 都生成一个新的 chunk
2.遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中
3.如果一个依赖 module 是动态引入的模块,那么就会根据这个 module 创建一个新的 chunk,继续遍历依赖
4.重复上面的过程,直至得到所有的 chunks
然后,对生成编译后的源码,合并,拆分,生成 hash 。 同时这是我们在开发时进行代码优化和功能添加的关键环节。
template.getRenderMainfest.render()
通过模板(MainTemplate、ChunkTemplate)把chunk生产 _webpack_requie() 的格式。
Compiler.done
最后一步,webpack 调用 Compiler 中的 emitAssets() ,按照 output 中的配置项将文件输出到了对应的 path 中,从而 webpack 整个打包过程结束。
webpack很复杂,其中好多个点都需要深入剖析,我们需要定期的回看源码,每一次都会有更深的理解。我借鉴了很多大佬的文章思路,理解尚浅,在通往大佬的过程中,我会经常回来完善~~~