webpack@5.10.1
webpack-cli@4.2.0
阅读源码小技巧:折叠所有代码,先不看所有的变量声明和 require,要是之后用到再看,直接看主要逻辑。
带着问题看源码
第一个问题
- webpack-cli 是如何调用 webpack 的
- 在 demo 目录运行 webpack-cli,会自动把 src/index.js 打包为 dist/main.js
- 显然会调用 webpack 来打包,那么请问是如何做到的
- 首先当我们运行 webpack-cli 命令的时候,会去执行 bin/cli.js 。
- 折叠文件所有代码,跳过 require 和所有声明,第一个 if 不看,之后的 if else 就需要打开来看了,因为这段话必定要执行,我们先假设 webpack 存在,进入 runCli 这个函数。
- 折叠所有代码,直接看主分支 try catch 里面,主要调用了 cli.run 这个方法。
- 进入 cli.run,第一个方法
this.runOptionGroups(args)
从名字可以看出和配置项相关,所以跳过,最后发现执行了this.createCompiler
这个函数。 - 进入 createCompiler 并折叠函数,我们发现他其实就是调用了 webpack 这个函数。
- 看完源码之后明白
- compiler = webpack(options, callback)
- webpack = require('webpack')
- webpack-cli 就是这么调用 webpack 的
第二个问题
- webpack 是如何分析 index.js 的
- 通过之前自己做的简易打包器,打包器需要先分析并收集依赖,然后打包成一个文件
- 那么 webpack 肯定也做了这件事
- 显然 webpack 也需要分析 AST,不可能用正则来做
- 直接看 webpack 的 package.json 的 main 属性,可以得知入口是 lib/index.js。
- 进入 index.js,主要是导出了一个函数,这个函数又是从 './webpack' 里面的。
- 进入 './webpack',也是主要导出了一个webpack 这个函数,第一个函数
validateSchema
是验证函数(不看),然后是 一个 if else,一般我们用 webpck 的时候,options 一般都不会是一个数组,所以我们看 else,执行了 一个 createCompiler 函数。 - 懵逼了,凭我目前的认知,看不懂这套逻辑了,createCompiler 就是 return 了一个 new Compiler,compiler.hooks.xxx.call 干了什么,猜测主要逻辑应该在这个里面(排除法)。
- 看了源码之后发现
- 看了半天,就发现创建了一个compiler 对象,然后什么都没做
- hooks.xxx.call 是什么鬼?
- 看源码不是好的学习方式,性价比低
- 第一次劝退
hooks.xxx.call 是什么
- Tapable
- 这是 webpack 团队为了写 wepack 而写的一个事件/钩子库(也就是发布订阅)
- 用法
- 定义一个事件/钩子
- this.hooks.eventName = new SyncHook(['arg1', 'arg2']);
- 监听一个事件/钩子
- this.hooks.eventName.tap('监听理由', fn)
- 触发一个事件/钩子
- this.hooks.eventName.call('arg1', 'arg2')
教训:看源码时,遇到不懂的,要快速学会
新的第二个问题
- webpack 的流程是怎么样的
- webpack 把打包分为了哪几个阶段(事件或钩子)
- 看完代码发现
- 至少有 env init run beforeCompile compile compilation make finishMake afterCompile emit 这几个钩子
流程图待补充
第三个问题
- 读取 index.js 并分析和收集依赖是在哪个阶段?
- 用排除法可以知道,肯定不是 env 和 emit,肯定在 beforeCompile 和 afterCompile 之间
- 最有可能是在 make - finishMake 阶段(为什么?)
- 学过 C 语言就会知道,make 是编译时必然会用到的工具,可见很重要
- 验证想法
- 我们发现 make - finishMake 之间什么代码都没有
第四个问题
- make - finishMake 之间,做了什么
- 搜索 make.tap,发现很多监听了 make 事件
- 还是根据排除法,应该是 EntryPlugin(由此发现 webpack 为什么什么都不做呢,因为 webpack 只是把流程安排好,其他事情都交给插件去做,webpack 架构就是事件模型,你要做什么事情,你自己去找相对应的钩子去插入就好了)
- EntryPlugin 的 addEntry 函数就是 make 阶段最重要的事情之一
- 死胡同
- 跟代码跟到 factorizeQueue 就发现没有后续代码了,怎么办?
- 第三次劝退
- 需要补充任务队列知识,任务队列发现有任务会自动执行
第五个问题
- factor.create 是什么东西
- 这个 factor 是哪里来的?
- 是从 factorizeModule(options 的 options.factory 来的)
- 这个 options.factory 是哪里来的?
- 是从 moduleFactory 来的
- moduleFactory 哪里来的?
- 是用 this.dependencyFactories.get(Dep) 得到的
- this.dependencyFactories.get(Dep) 是个啥?
- 你搜 compilation.tap 就知道,他是 normalModuleFactory,简称 nmf
- 结论:factor 就是 nmf,所以 factory.create 就是 nmf.create
第六个问题
- nmf.create 做了什么
- 来到 NormalModuleFactory.js,可以看到 create 的代码
- 只发现一具有用的代码:beforeResolve.call 和 factorize.call
- 搜索两者对应的 tap,发现 factorize.tap 里面有重要代码
- 他触发了 resolve,而 resolve 主要是在收集 loaders
- 然后他触发了 createModule,得到了 createdModule
- 也就是说,nmf.create 得到了一个 module 对象
- 等价于 factor.create 得到了一个 module 对象
- 回想一下,我们怎么找到 factory.create 的?
- 你可以使用 back 功能回来之前的停顿点
- 我们是从 factorizeModule 来到 factory.create 的
- 回来 factorizeModule,发现后续操作是 addModule 和 buildModule
第七个问题
- addModule 做了什么
- 把module 添加到 compilation.modules 里
- 而且还通过检查 id 防止重复添加
第八个问题
- buildModule 做了什么
- 看名字就知道是重要操作,它调用了 module.build()
- 来到 NormalModule.js(猜的,跟 nmf 差不多) 看 build 源码,发现了 runLoaders
- 然后来到 processResult(),发现了 _source = ... 和 _ast = null
- 这是要做什么?显然是要把 _source 变成 _ast 了!
- 这就是我们第二个问题(webpack 如何分析 index.js) 的答案!
- 来到 doBuild 的回调,发现了 this.parser.parse()!
- 终于,着整个过程就是最开始我们的简易打包器的过程(AST、Babel、依赖)
- parse 就是把 code 变成 ast
- 问题来了,parser 是什么,parse() 的源码在哪儿?
- 继续跟代码会发现 parser (javascriptParser.js)来自于acorn 库,需要编译原理知识(涉及到盲区了)
第九个问题
- webpack 如何知道 index.js 依赖了那些文件的
- 目前我们知道 webpack 会对 index.js 进行 parse 得到 ast
- 那么接下来 webpack 应该会 traverse 这个 ast,寻找 import 语句
- 那么相关代码在哪儿?
- 阅读源码发现
- JavascriptParser.js 的 3231 行得到 ast,3260~3264 行 traverse 了 ast
- 其中 blockPreWalkStatement() 对 ImportDeclaration 进行了检查
- 一旦发现 import 'xxx',就会触发 import 钩子,对应的监听函数会处理依赖
- 其中 walkStatements() 对 importExpression 进行了检查
- 一旦发现 import('xxx'),就会触发 importCall 钩子,对应的监听函数也会
第十个问题
- 怎么把 Modules 合并成一个文件的?
- 看 compilation.seal()(猜的,只剩这个阶段了),该函数会创建 chunks、 为每个 chunk 进行 codeGeneration,然后为每个 chunk 创建 asset(搜索 write,发现writeOut)
- seal() 之后,emitAssets()、emitFiles() 会创建文件
- 最终得到 dist/main.js 和其他 chunk 文件
小结
- webpack 怎么分析依赖和打包的
- 使用 JavascriptParser 对 index.js 进行 parse 得到 ast,然后遍历 ast
- 发现依赖声明就将其添加到 module 的 dependencies 或 blocks 中
- seal 阶段,webpack 将 module 转为 chunk,可能会把多个 module 通过 codeGeneration 合并为一个 chunk
- seal 之后,为每个 chunk 创建文件,并写到硬盘上
网友评论