webpack@5.10.1
webpack-cli@4.2.0
阅读源码小技巧:折叠所有代码,先不看所有的变量声明和 require,要是之后用到再看,直接看主要逻辑。
带着问题看源码

第一个问题

  • webpack-cli 是如何调用 webpack 的
  • 在 demo 目录运行 webpack-cli,会自动把 src/index.js 打包为 dist/main.js
  • 显然会调用 webpack 来打包,那么请问是如何做到的
  1. 首先当我们运行 webpack-cli 命令的时候,会去执行 bin/cli.js 。
  2. 折叠文件所有代码,跳过 require 和所有声明,第一个 if 不看,之后的 if else 就需要打开来看了,因为这段话必定要执行,我们先假设 webpack 存在,进入 runCli 这个函数。
  3. 折叠所有代码,直接看主分支 try catch 里面,主要调用了 cli.run 这个方法。
  4. 进入 cli.run,第一个方法 this.runOptionGroups(args) 从名字可以看出和配置项相关,所以跳过,最后发现执行了 this.createCompiler 这个函数。
  5. 进入 createCompiler 并折叠函数,我们发现他其实就是调用了 webpack 这个函数。

阅读全过程

  • 看完源码之后明白
  • compiler = webpack(options, callback)
  • webpack = require(‘webpack’)
  • webpack-cli 就是这么调用 webpack 的

第二个问题

  • webpack 是如何分析 index.js 的
  • 通过之前自己做的简易打包器,打包器需要先分析并收集依赖,然后打包成一个文件
  • 那么 webpack 肯定也做了这件事
  • 显然 webpack 也需要分析 AST,不可能用正则来做

阅读全过程

  1. 直接看 webpack 的 package.json 的 main 属性,可以得知入口是 lib/index.js。
  2. 进入 index.js,主要是导出了一个函数,这个函数又是从 ‘./webpack’ 里面的。
  3. 进入 ‘./webpack’,也是主要导出了一个webpack 这个函数,第一个函数 validateSchema 是验证函数(不看),然后是 一个 if else,一般我们用 webpck 的时候,options 一般都不会是一个数组,所以我们看 else,执行了 一个 createCompiler 函数。
  4. 懵逼了,凭我目前的认知,看不懂这套逻辑了,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 创建文件,并写到硬盘上