调试webpack源码的两种方法

node —inspect-brk

node --inspect-brk node_modules/webpack-cli/bin/cli.js

yarn link

创建软连接,便于运行webpack开发环境的源码

  1. # 切换webpack webpack-cli的版本
  2. git reset --hard webpack-cli@4.2.0
  3. $ git reset --hard v5.10.1
  4. $ cd packages/webpack-cli/ 进入根目录
  5. $ yarn link
  6. success Registered "webpack-cli".
  7. 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.
  8. Done in 0.36s.

这样后在任何地方运行 yarn link webpack-cli就都会创建一个文件软连接
image.png

这样实现了在一个项目中运行外部文件的webpack-cli

  1. $ node ./node_modules/webpack-cli/bin/cli.js

不过windows并不完全支持软连接,有bug

webpack-cli是如何调用webpack的?

webpack-cli-bin/cli.js

  1. webpack = require('webpack')
  2. compiler = webpack(options, callback)

看cli的源码得知

  • webpack-cli是通过调用webpack函数来创建(createCompiler函数)一个编译器

webpack-cli如何调用webpack.xmind

webpack开始

  • Compiler类(./lib/Compiler.js):webpack的主要引擎,代表的是不变的webpack环境
    • 在compiler对象记录了完整的webpack环境信息,在webpack从启动到结束,compiler只会生成一次。
    • 可以在compiler对象上读取到webpack config信息,outputPath等;
  • Compilation类(./lib/Compilation.js):代表了一次单一的版本构建和生成资源。

    • compilation编译作业可以多次执行,比如webpack工作在watch模式下,每次监测到源文件发生变化时,都会重新实例化一个compilation对象。
    • 一个compilation对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。

      分析index.js入口文件

  • 我们知道打包器需要先分析并收集依赖,然后打包成一个文件

  • webpack是如何做这些处理的呢?
  • 通过阅读lib/index.js源码
    • 其中发现了大量的hooks.xxx.call()代码
    • 然后hooks函数是从tapable库中引入的
  • 那么tapable是什么?

    Tapable

    这是一个监听和触发事件/钩子函数 的库,就是一个发布订阅系统
    用法
    1. // 定义一个事件/钩子
    2. this.hooks.eventName = new SyncHook(["arg1", "arg2"]);
    3. // 监听一个事件/钩子
    4. this.hooks.eventName.tap('监听理由', fn)
    5. // 触发一个事件/钩子
    6. 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.callfactorize.call
  • 搜索两者对应的 tap,发现 factorize.tap 里面有重要代码 它触发了 resolve,而 resolve 主要是在收集 loaders
  • 然后它触发了 createModule,得到了 createdModule
  • 也就是说,nmf.create 得到了一个 module 对象
  • 等价于 factory.create 得到了一个 module 对象
  • 回到 factorizeModule,发现后续操作是 addModule 和 buildModule

addModule 做了什么

  • 把module添加到compilation.modules属性里面
  • 检查id防止重复添加

    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 文件

看源码时的笔记

  1. environment
  2. afterEnvironment
  3. initialize
  4. beforeRun
  5. run
  6. ----this.readRecords---
  7. ----this.compile----
  8. beforeCompile
  9. compile
  10. ----------this.newCompilation-------
  11. make
  12. finishMake
  13. -----nextTick----
  14. -----compilation.finish-----
  15. finishModules
  16. -----compilation.seal()-----
  17. seal
  18. afterOptimizeDependencies
  19. beforeChunks
  20. ------this.addChunk-----
  21. ------buildChunkGraph----
  22. afterChunks
  23. optimize
  24. optimizeModules
  25. optimizeChunks
  26. afterOptimizeChunks
  27. optimizeTree
  28. afterOptimizeTree
  29. optimizeChunkModules
  30. ------this.codeGeneration-----
  31. ------this.createChunkAssets---
  32. processAssets
  33. afterSeal
  34. afterCompile
  35. ----onCompiled---
  36. shouldEmit
  37. ----nextTick----
  38. ----this.emitAssets---
  39. ----this.emitRecords---
  40. done

总结

  1. 调用webpack函数接收config配置信息,并初始化compiler,在此期间会apply所有 webpack 内置的插件;
  2. 调用compiler.run进入模块编译make阶段,
    1. JavascriptParser实例对象对 index.js 进行 parse编译 得到 ast抽象语法数
    2. 然后遍历 ast 发现依赖声明就将其添加到 module 的 dependencies 或 blocks 中
    3. seal 阶段,webpack 将 module 转为 chunk,可能会把多个 module 通过 codeGeneration 合并为一个 chunk seal
  3. 最后,为每个 chunk 创建文件,并写到硬盘上