书读百遍,其义自见。
- 为了研读源码首先clone一份webpack的repo到本地。刚开始研读的时候一定是一头雾水,如果实在是难以下咽可以先尝试了解一下
tapable
、compiler
、compilation
这些核心定义, 在进行阅读。
git clone https://github.com/webpack/webpack.git
从入口看起 - webpack.js
/* path - ./lib/webpack.js */
const Compiler = require("./Compiler");
const webpack = (options, callback) => {
let compiler
// 非必要的watch 参数就暂时忽略
// 根据options 来判断使用 createCompiler / createMultiCompiler 来实例化
compiler = createCompiler(options)
// 如果传入callback函数,则自启动
if(callback){
compiler.run((err, states) => {
compiler.close((err2)=>{
callbacl(err || err2, states)
})
})
}
return compiler
}
webpack函数执行后返回compiler对象,在webpack中存在两个非常重要的核心对象,分别为compiler和compilation,它们在整个编译过程中被广泛使用。
- Compiler类(./lib/Compiler.js):webpack的主要引擎,在compiler对象记录了完整的webpack环境信息,在webpack从启动到结束,compiler只会生成一次。你可以在compiler对象上读取到webpack config信息,outputPath等;
- Compilation类(./lib/Compilation.js):代表了一次单一的版本构建和生成资源。compilation编译作业可以多次执行,比如webpack工作在watch模式下,每次监测到源文件发生变化时,都会重新实例化一个compilation对象。一个compilation对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。
Compiler 在上面的 createCompiler 中被实例化,其中对于plugins的操作需要特别注意一下;这也是为什么plugins配置时要保持为函数,或者一个有apply字段的对象且apply是函数。
const createCompiler = rawOptions => {
// 对于一些options 的操作直接过滤
const compiler = new Compiler(options.context);
compiler.options = options;
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
// 调用相关hook
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 对webpack options的初始化
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
// 最终返回compiler
return compiler;
};
在 WebpackOptionsApply
主要完成了对options的初始化;在这个类中主要做了两件事;
- new很多的Plugin,并且apply它们。
- 根据options.xxx的配置项,做初始化工作。
提纲挈领 - tapable
Tapable 是一个小型的库,允许你对一个 javascript 模块添加和应用插件。看到很多文章把它形容为webpack的管家或者骨架。把它放在第一个来了解主要是为了防止后面的阅读过程中由于这几个API带来的一头雾水。
Tapable 通过工厂类 HookCodeFactory
,释放出以下几个API:
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
-
Hook 类型
每个hook都可以触发一个或多个功能。 它们如何执行取决于hook类型:- Basic hook.: 按照事件注册顺序,依次执行handler,handler之间互不干扰;
- Waterfall:按照事件注册顺序,依次执行handler,前一个handler的返回值将作为下一个handler的入参;
- Bail: 按照事件注册顺序,依次执行handler,若其中任一handler返回值不为undefined,则剩余的handler均不会执行;
- Loop:按照事件注册顺序,依次执行handler,若任一handler的返回值不为undefined,则该事件链再次从头开始执行,直到所有handler均返回undefined
hook可以是同步的或异步的。 为了反映这一点,提供了“ Sync”,“ AsyncSeries”和“ AsyncParallel” hook类:
- Sync.
- AsyncSeries.
- AsyncParallel.
hook类型反映在其类名称中。例如,AsyncSeriesWaterfallHook允许异步函数并依次运行它们,将每个函数的返回值传递给下一个函数。
-
Interception
- call: (...args) => void 当hook被触发时,向拦截器添加呼叫将被触发。 可以访问hooks参数。
- tap: (tap: Tap) => void 将插件添加到拦截器中时,将在插件点击钩子时触发。 提供的是Tap对象。 点击对象无法更改。
- loop: (...args) => void 在拦截器中添加循环将为循环钩子的每个循环触发。
- register: (tap: Tap) => Tap | undefined 将寄存器添加到拦截器将为每个添加的Tap触发并允许对其进行修改。
其他
还有一些其他的具体参数,详细可以看 https://github.com/webpack/tapable
Compiler
参考了下面文章中的伪代码部分;
class Compiler {
constructor(context){
// 所有钩子都是由`Tapable`提供的,不同钩子类型在触发时,调用时序也不同
this.hooks = {
beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(["compiler"]),
done: new AsyncSeriesHook(["stats"]),
// ...
}
}
// ...
run(callback){
const onCompiled = (err, compilation) => {
if(err) return
const stats = new Stats(compilation);
this.hooks.done.callAsync(stats, err => {
if(err) return
callback(err, stats)
this.hooks.afterDone.call(stats)
})
}
this.hooks.beforeRun.callAsync(this, err => {
if(err) return
this.hooks.run.callAsync(this, err => {
if(err) return
this.compile(onCompiled)
})
})
}
}
run 这个阶段hook住编译的一些阶段并在不同阶段执行一些准备好的hook;在run函数中出现的钩子有:beforeRun --> run --> done --> afterDone
。第三方插件可以钩住不同的生命周期,接收compiler对象,处理不同逻辑。
在this.compile中引出了另外一个重要的阶段 compilation;
Compilation
compile(callback){
const params = this.newCompilationParams() // 初始化模块工厂对象
this.hooks.beforeCompile.callAsync(params, err => {
this.hooks.compile.call(params)
// compilation记录本次编译作业的环境信息
const compilation = new Compilation(this)
this.hooks.make.callAsync(compilation, err => {
compilation.finish(err => {
compilation.seal(err=>{
this.hooks.afterCompile.callAsync(compilation, err => {
return callback(null, compilation)
})
})
})
})
})
}
compile函数和run一样,触发了一系列的钩子函数,在compile函数中出现的钩子有:beforeCompile --> compile --> make --> afterCompile
。
我们关心的make过程,在compile过程中暴露出来的仅仅是一个hook; 探究具体: this.addEntry --> this.addModuleChain --> this.handleModuleCreation --> this.addModule --> this.buildModule --> this._buildModule --> module.build(this指代compiliation)
在build时 会优先执行doBuild
,选用合适的 loader 去加载对应的资源;webpack对处理标准的JS模块很在行,但处理其他类型文件(css, scss, json, jpg)等就无能为力了,此时它就需要loader的帮助。loader的作用就是转换源代码为JS模块,这样webpack就可以正确识别了。
TODO: parse、 seal
资源参考:
- https://github.com/webpack/webpack
- https://github.com/webpack/tapable
- https://juejin.im/post/5dcba29f6fb9a04abb01fd77#heading-6