调试webpack源码的两种方法
node —inspect-brk
node --inspect-brk node_modules/webpack-cli/bin/cli.js
yarn link
创建软连接,便于运行webpack开发环境的源码
# 切换webpack 和webpack-cli的版本
git reset --hard webpack-cli@4.2.0
$ git reset --hard v5.10.1
$ cd packages/webpack-cli/ 进入根目录
$ yarn link
success Registered "webpack-cli".
info You can now run `yarn link "webpack-cli"` in the projects where you want to use this package and it will be used instead.
Done in 0.36s.
这样后在任何地方运行 yarn link webpack-cli
就都会创建一个文件软连接
这样实现了在一个项目中运行外部文件的webpack-cli
$ node ./node_modules/webpack-cli/bin/cli.js
webpack-cli是如何调用webpack的?
webpack-cli-bin/cli.js
webpack = require('webpack')
compiler = webpack(options, callback)
看cli的源码得知
- webpack-cli是通过调用webpack函数来创建(createCompiler函数)一个编译器
webpack开始
- Compiler类(
./lib/Compiler.js
):webpack的主要引擎,代表的是不变的webpack环境- 在compiler对象记录了完整的webpack环境信息,在webpack从启动到结束,
compiler
只会生成一次。 - 可以在
compiler
对象上读取到webpack config
信息,outputPath
等;
- 在compiler对象记录了完整的webpack环境信息,在webpack从启动到结束,
Compilation类(
./lib/Compilation.js
):代表了一次单一的版本构建和生成资源。我们知道打包器需要先分析并收集依赖,然后打包成一个文件
- webpack是如何做这些处理的呢?
- 通过阅读lib/index.js源码
- 其中发现了大量的
hooks.xxx.call()
代码 - 然后hooks函数是从tapable库中引入的
- 其中发现了大量的
- 那么tapable是什么?
Tapable
这是一个监听和触发事件/钩子函数 的库,就是一个发布订阅系统
用法// 定义一个事件/钩子
this.hooks.eventName = new SyncHook(["arg1", "arg2"]);
// 监听一个事件/钩子
this.hooks.eventName.tap('监听理由', fn)
// 触发一个事件/钩子
this.hooks.eventName.call('arg1', 'arg2')
webpack打包流程分析
调用了大量tapable 库中的hooks事件
- environment
- afterEnvironment
- initialize
- beforeRun
- run
- shouldEmit
- done
- beforeCompile
- afterCompile
index.js分析并收集是在哪个阶段?
- 不是env 和 emit,判断在beforeCompile与 afterCompile之间
- 最有可能在make - finishMake阶段
- 分析后发现EntryPlugin.js 这个插件文件
- webpack就是一个事件模型,webpack本身并不做大量处理,而是将工作通过插件Plugin的形式分出去
factory函数
factory就是nmf
factory.create就是nmf.create的方法
factorizeModule
nmf.create方法做了什么
- 来到 NormalModuleFactory.js,可以看到 create 的代码
- 只发现一句有用的代码:
beforeResolve.call
和factorize.call
- 搜索两者对应的 tap,发现 factorize.tap 里面有重要代码 它触发了 resolve,而 resolve 主要是在收集 loaders
- 然后它触发了 createModule,得到了 createdModule
- 也就是说,nmf.create 得到了一个 module 对象
- 等价于 factory.create 得到了一个 module 对象
- 回到 factorizeModule,发现后续操作是 addModule 和 buildModule
addModule 做了什么
- 把module添加到compilation.modules属性里面
-
buildModule 做了什么
它调用了 module.build()
- 来到 NormalModule.js 看 build 源码-doBuild,发现了 runLoaders() ,运行所有合适的loader
- 然后来到 processResult(),发现了 _source = … 和 _ast = null 这是要做什么?
- 显然是要把 _source 变成 _ast 了!来到 doBuild 的回调,发现了 this.parser.parse()
- parse 就是把 code 变成 ast
- parser 是什么?继续跟代码会发现 parser 来自于 acorn 库,需要编译原理知识
webpack如何确定index依赖文件?
- webpack会对index.js进行parse得到ast抽象语法数
- 然后会在traverse过程中遍历这个ast,寻找import语句
阅读源码后发现
- blockPreWalkStatement() 对ImportDeclaration进行了检查
- 一旦发现 import ‘xxx’,就会触发 import 钩子,对应的监听函数会处理依赖
- 其中 walkStatements() 对 ImportExpression 进行了检查
- 一旦发现 import(‘xxx’),就会触发 importCall 钩子,对应的监听函数也会处理依赖
webpack如何将modules合并成为一个文件的?
- compilation.seal(),该函数会创建 chunks、 为每个 chunk 进行 codeGeneration
- 然后为每个 chunk 创建 asset seal()
- 之后,emitAssets()、emitFiles() 会创建文件(emit 触发)
- 最终得到 dist/main.js 和其他 chunk 文件
看源码时的笔记
environment
afterEnvironment
initialize
beforeRun
run
----this.readRecords---
----this.compile----
beforeCompile
compile
----------this.newCompilation-------
make
finishMake
-----nextTick----
-----compilation.finish-----
finishModules
-----compilation.seal()-----
seal
afterOptimizeDependencies
beforeChunks
------this.addChunk-----
------buildChunkGraph----
afterChunks
optimize
optimizeModules
optimizeChunks
afterOptimizeChunks
optimizeTree
afterOptimizeTree
optimizeChunkModules
------this.codeGeneration-----
------this.createChunkAssets---
processAssets
afterSeal
afterCompile
----onCompiled---
shouldEmit
----nextTick----
----this.emitAssets---
----this.emitRecords---
done
总结
- 调用webpack函数接收config配置信息,并初始化compiler,在此期间会apply所有 webpack 内置的插件;
- 调用
compiler.run
进入模块编译make阶段,- 用
JavascriptParser
实例对象对 index.js 进行 parse编译 得到 ast抽象语法数 - 然后遍历 ast 发现依赖声明就将其添加到 module 的 dependencies 或 blocks 中
- seal 阶段,webpack 将 module 转为 chunk,可能会把多个 module 通过 codeGeneration 合并为一个 chunk seal
- 用
- 最后,为每个 chunk 创建文件,并写到硬盘上