前言
上一篇文章中我们实现了一个简易打包器,简单了解了 webpack 是的工作原理,为了验证我们的猜想对不对,那我们就去源码找找看吧
How 怎么读源码
读源码之前,首页要准备好源码(废话)
有两种获得源码的方式
第一种是 git clone
, 这种方法可以得到各个版本的 webpack
代码,当 webpack
更新的时候也可以通过 git pull
拉取最新代码,不过可能是网络原因,我是使用 git clone
一直无法拉取下来,退而求其次我选择了第二种方法
第二种是通过 releases 下载,那就需要选择对应的版本下载,这里我选择的是 5.10.0 版本,下载对应的压缩文件即可
带着问题读源码
webpack 代码量极大,里面分支众多,错综复杂,如果想一行一行的把代码都读一遍,显然是不可行的,也是低效的,那么我们的目标应该是只看核心代码,了解 webpack
打包的整体流程
为了了解 webpack
的整体运行流程这一个大目标,我们需要把它分解成一个个小目标或问题,通过不断地拆解问题,找到答案的过程中,了解整个脉络
那么第一问题是: 编译的起点?
源码解析
如何运行 webpack
在问题开始之前,我们先了解一个基本知识,启动 webpack 有两种方法
第一种方法 使用 webpack-cli 从命令行启动
npx webpack-cli
这是最快最便捷的方法,默认的打包入口是 src/index.js
第二种方法 通过 require('webpack')
引入包的方式执行
两种方式都会调用 webpack
来打包代码,只是操作方式不同
问题一:编译的起点
一切从 compiler = webpack(options, callback);
开始
webpack
函数源码(lib/webpack.js)
const webpack = ((options,callback) => {
const create = () => {
...
let compiler;
if (Array.isArray(options)) {
...
} else {
/** @type {Compiler} */
compiler = createCompiler(options);
...
}
return { compiler, watch, watchOptions }
};
if (callback) {
try {
const { compiler, watch, watchOptions } = create();
if (watch) {
compiler.watch(watchOptions, callback);
} else {
compiler.run((err, stats) => {
compiler.close(err2 => {
callback(err || err2, stats);
});
});
}
return compiler;
} catch (err) {...}
} else {...}
});
这段代码中可以看到 compiler.run
是核心代码,代表者编译的开始,而 createCompiler
里面主要是实例化 compiler
和做一些初始化,比如读取配置,激活 webpack
的内置插件...
在 webpack
中 compiler
是核心对象之一,通过 new Compiler
实例化,compiler 记录了完整的 webpack
环境信息,webpack
从启动到结束,compiler
只会被生成一次,你可以在 compiler
对象上读取到 webpack
config 信息,outputPath
等
从 comilper.run()
编译真正开始了
Tapable
在阅读 webpack
源码的时候,经常会看到 xxx.hooks.xxx
,这实际上是一个事件管理库 Tapable
, 想要继续阅读源码就必须快速了解一下 Tapable
的基本用法
// 定义一个事件/钩子
this.hooks.eventname = new SyncHook(['arg1', 'arg2'])
// 监听一个事件/钩子
this.hooks.eventName.tab('监听理由',fn)
// 触发一个事件/钩子
this.hooks.eventName.call('arg1', arg2)
问题二:webpack 的流程是怎样的
上文说到 compiler.run()
是编译的开始,那我们跟着这个方法,进去看吧
找到 Compiler
类的定义文件 lib/Compiler.js
(源码)
run(callback) {
const onCompiled = (err, compilation) => {...};
const run = () => {
this.hooks.beforeRun.callAsync(this, err => {
...
this.hooks.run.callAsync(this, err => {
...
this.readRecords(err => {
...
this.compile(onCompiled);
});
});
});
};
if (this.idle) {
...
run();
});
} else {
run();
}
}
在 compiler.run
的代码中,我们可以看到在编译的过程中,会在相应的阶段触发钩子,如 hooks.beforeRun
hooks.run
,这种方式就很像我们在用 vue
或者 react
中的生命周期钩子,第三方插件可以定义对于不同的事件的处理函数,在相应的编译时间段触发,来实现插件的效果
在阅读的过程中,我们应该把重要的钩子和方法记录下来,方便阅读
比如上面的代码就可以记成
new Compiler // 重要的方法
-env // -hooks 代表钩子
-init
compiler.run
-beforeRun
-run
compiler.readRecords
compiler.compiler
...
通过这种方法,我们可以快速了解 webpack
的流程
接着上面的代码,我们要开始看 compiler.compiler
webpack 源码 lib/Compiler.js
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 => {
this.hooks.finishMake.callAsync(compilation, err => {
process.nextTick(() => {
compilation.finish(err => {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {
return callback(null, compilation);
});
});
});
});
});
});
});
}
因为源码细节比较多,所以上面简略了一些错误处理和log信息
继续记录编译过程
new Compiler // 重要的方法
-env // -hooks 代表钩子
-init
compiler.run
-beforeRun
-run
compiler.readRecords
compiler.compiler
-beforeCompile
-compile
newCompilation
-make
-finishMake
nextTick
compilation.finish
compilation.seal
-afterCompile
最终我们基本可以知道 webpack
的编译可以分为 env > init > run > compile > compilation > make > finishMake > afterCompile > seal > emit 这几个阶段
另外从代码中我们看到了一个重量级的对象 compilation
, 看名字就可以知道,这个对象在编译过程中一定是起着重要作用的。compilation
代表一次单一的编译作业,compilation
记录了当前编译作业的模块资源和编译生成的文件、以及依赖信息等。
问题三:读取 index.js 并分析依赖是在哪个阶段
按照我们刚刚分析的几个阶段,我们首先可以排除 env init(猜的),肯定是在 compiler > afterCompile 之间
make > finishMake 这几个阶段的可能性是最大的
为什么呢?了解过 C 语言就会知道,make是编译时必须用到的工具
那么我们就在这两个阶段中寻找
直接贴上源码
上面的代码除了 logger 和 错误处理之后,好像什么都没做,怎么就从 make
变成 finishMake
了呢?
问题四:make-finishMake 之间做了什么
还记得之前提到的 Tabable
库吗?也就是 webpack
的事件分发系统,那么我们可以从 make
这个事件绑定的处理函数开始下手
直接全局搜索 make.tapAsync
可以发现都是插件在定义处理函数
其中 EntryPlupin 特别显眼,编译既然要分析依赖,分析之前就要有入口,那么 entryPlupin
显然是必须看的
查看 make.tapAsync("EntryPlugin")
代码可以发现,里面实际上调用了 compilation.addEntry
, 经过多次的查找 compilation.addEntry
-> compilation._addEntryItem
-> compilation.addModuleChain
-> compilation.handleModuleCreation
我们研究一下 handleModuleCreation
handleModuleCreation({ factory, dependencies, originModule, context, recursive = true },callback) {
...
this.factorizeModule({ currentProfile, factory, dependencies, originModule, context },(err, newModule) => {
...
this.addModule(newModule, (err, module) => {
...
this.processModuleDependencies(module, err => {
...
callback(null, module);
});
});
}
);
}
可以看到这里有很多重要步骤,我们从第一开始分析,也就是 this.factorizeModule()
, 点击这个函数,我们最终会看到 this.factorizeQueue.add(options, callback)
那么 factorizeQueue
到底是什么呢? 可以看看他的定义
这时我们应该查看它的处理器 processor
,在 this._factorizeModule
中,我们最终可以看到是 factory.create()
问题五:factory.create 是什么东西
在这里我们需要摸着参数一个个根据调用关系往上找,最终在 addModuleChain
中我们找到了线索,factory
是从 this.dependencyFactories.get(Dep)
得到的
然后线索就中断了 this.dependencyFactories.get(Dep)
是什么呢? 既然有 get 那就应该有 set,经过再次全局搜索(全靠运气),终于在 EntryPlugin 中找到了
factory
就是 normalModuleFactory
那么 factory.create 就是 normalModuleFactory.create
由于 normalModuleFactory 太长了,以下简称 nmf
阶段性小结
目前为止我们了解到
webpack
使用hooks
把主要的阶段固定下来webpack
执行过程中,插件自己选择阶段做事- 入口是由入口插件
EntryPlugin.js
搞定的
理清思路后,继续探究...
问题六:nmf.create 做了什么
源码 nmf.create
create(data, callback) {
...
this.hooks.beforeResolve.callAsync(resolveData, (err, result) => {
...
this.hooks.factorize.callAsync(resolveData, (err, module) => {
...
callback(null, factoryResult);
});
});
}
通过这里我们可以看到实际上触发了两个 hooks
,boforeResolve
和 factorize
, 通过查看对应的事件处理函数,我们发现 factory.tap
里面由重要代码
它触发了 resolve
而 resolve
主要的作用就是收集 loaders
然后它触发了 createModule
得到了 createdMolude
也就是说,factory.create
的作用是收集 loaders 和得到了 module 对象
问题七:addModule 做了什么
既然我们已经清楚了 factory.create
做了什么,那我们再回到 compilation.factorizeModle()
(忘记了可以回看问题四的代码图) , 发现后面的操作是 addModule
和 buildModule
那么 addModule
做了什么呢?
addModule
就是把一个 module
加入到 compilation.modules
里面
回看问题四里面的代码,我们可以看到这个阶段的 addModule
实际上是把,factory.create()
得到的 newModule
加入到 copilation.modules
里面
问题八:buildModule 做了什么
一看这名字,就知道是很重要的一步
buildModule
实际上是调用了 normalModule.build
,normalModule.build
则调用了自身的 doBuild
方法
const { runLoaders } = require("loader-runner");
doBuild(options, compilation, resolver, fs, callback){
// runLoaders从包'loader-runner'引入的方法
runLoaders({
resource: this.resource, // 这里的resource可能是js文件,可能是css文件,可能是img文件
loaders: this.loaders,
}, (err, result) => {
const source = result[0];
const sourceMap = result.length >= 1 ? result[1] : null;
const extraInfo = result.length >= 2 ? result[2] : null;
// ...
})
}
doBuild
函数里面可以看到调用了 runLoaders
方法,作用是通过 loader
将一些非 js 的文件,比如 css,html..., 转成 js(webpack
只能识别 js),runLoaders
之后得到 result
继续下一步处理
接下来就是把 js 转成 ast 代码了
result = this.parser.parse(source);
其中 this.parse.parse
实际上是一个第三方库 acorn
提供的 parse 方法
parse(code, options){
// 调用第三方插件`acorn`解析JS模块
let ast = acorn.parse(code)
...
if (this.hooks.program.call(ast, comments) === undefined) {
this.detectStrictMode(ast.body)
this.prewalkStatements(ast.body)
this.blockPrewalkStatements(ast.body) // 分析依赖
this.walkStatements(ast.body)
}
}
问题九:webpack 如何知道 index.js 依赖了哪些文件的
上一个问题,我们已经分析到了 webpack
会使用 runLoaders
把代码都转成 js,然后通过 acorn
库,将 js 转换成 ast
代码
那么 webpack
如何分析依赖呢?
按照上篇文章的原理分析,应该会 traverse 这个 ast,寻找 import
语句
在源码中可以发现 this.blockPrewalkStatements(ast.body)
对 ImportDeclaration
进行了检查,一旦发现 import 'xxx'
, 就会触发 import
钩子,对应的监听函数会处理依赖,将依赖加入到 module.dependencies
的数组中
问题十:怎么把 module 合并成一个文件
经过上面的分析,我们大概知道 webpack
分为 env > compile > make > seal > emit 这几个阶段
我们可以猜出合并文件会在 seal > emit 阶段之间
在这个阶段调用了 compilation.seal
, 该函数会创建 chunks
、对每个 chunk
进行 codeGeneration
, 然后为每个 chunk
创建 asset
seal 之后就是 emit 阶段,也就是发射,很明显就行文件写出去,最终得到 dist/main.js 和其他 chunk
文件
总结
本文篇幅比较长,非常感谢你阅读到最后,最后简单总结一下 webpack
的基本流程:
- env 和 init 阶段,调用
webpack
函数接收config
配置信息,并apply
所有webpack
内置插件 - 调用
compiler.run
进入编译阶段 - make 阶段,从
entry
为入口,通过loaders
对模块的源代码进行转换成 JS 模块,然后使用acorn
parse 成 ast(抽象语法数),再遍历语法树分析和收集依赖 - seal 阶段,
webpack
将module
转为chunk
,每个chunk
可以是一个模块或多个模块 - emit 阶段,为每个
chunk
创建文件,并写入硬盘
码字不易,如果这篇文章对你有用请点个赞吧
如果有写得不好的地方,欢迎大佬们指导
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!