运行流程

webpack 本质是一个JS Module Bundler, 用于将多个代码模块进行打包。bundler 从一个构建入口触发, 解析代码,分析出代码依赖关系, 然后将依赖的代码模块组合在一起, 在JS bundler 中, 还需要一些胶水代码让多个代码模块可以协同工作, 相会引用。

  1. // entry.js
  2. import { bar } from './bar.js';
  3. // bar.js
  4. const foo = require('./foo.js');
  5. // 递归下去, 直到没有更多的依赖模块, 最终形成一颗模块依赖树

分析出依赖关系后, webpack 会利用JS Function 的特性提供一些代码来将各个模块整合到一起.。即将每一个模块装成一个JS Function,提供一个引用依赖模块的方法。下面例子中的webpackrequier__, 这样做, 既可以避免变量相互干扰, 又能有效控制执行顺序。

  1. // 分别将各个依赖模块的代码用 modules 的⽅式组织起来打包成⼀个⽂件
  2. ================================entry======================================
  3. ================================moudles======================================
  4. // entry.js
  5. modules["./entry.js"] = function () {
  6. const { bar } = __webpack__require__("./bar.js");
  7. };
  8. // bar.js
  9. modules["./bar.js"] = function () {
  10. const foo = __webpack__require__("./foo.js");
  11. };
  12. // foo.js
  13. modules["./foo.js"] = function () {
  14. // ...
  15. };
  16. ================================output===========================
  17. // 已经执⾏的代码模块结果会保存在这里
  18. const installedModules = {};
  19. function __webpack__require__(id) {
  20. // 如果 installedModules 中有就直接获取
  21. // 没有的话从 modules 中获取 function 然后执⾏,
  22. //将结果缓存在 installedModules 中然后返回结果
  23. }

[webpack]webpack4源码分析 - 图1

  1. Compiler webpack 的运行入口, compiler对象代表完整的webpack 环境配置。这个对象在启动webpack 时被一次建立, 并配置好所有可操作的设置, 包括options、loader和plugin。当在webpack 环境中应用一个插件时, 插件将收到此compiler 对象的引用, 可以使用它来访问webpack 的主环境/
  2. Compilation 对象代表了一次资源版本构建。 当运行webpack 开发环境中间件时, 每当检测到一个文件变化, 就会创建一个新的compilation, 从而生成一组新的编译资源。 一个compilation 对象表示当前的模块资源、编译生成环境、变化的文件以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键步骤的回调, 以供插件做自定义处理时选择使用。
  3. Chunk, 即表示chunk 的类, 对于构建时需要的chunk 对象有compilation 创建后保存管理。(webpack中最核心的负责编译的compiler 和负责创建bundle 的compilation都是Tapable的实例)
  4. Module(webpack/lib/module.js), 用于表示代码的基础类, 衍生出很多子类用于处理不同的情况(webpack/lib/normalModule.js)关于代码模块的所有信息都会存在module实例中, 例如dependencies 记录代码模块的依赖。

当一个module 实例被创建后, 比较重要的一步是执行compilation.buildModule 这个方法,它会调用module 实例的build方法来创建module 实例需要的一些东西, 然后自身的runloaders 方法。runloader: loader-runner(webpack/loader-runner), 执行对应的loaders, 将代码源码内容一一交由配置中指定的loader处理后, 再把处理的结果保存起来。

  1. Parser, 基于acorn 来分析 AST 语法树, 解析出代码模块的依赖。
  2. Dependency, 解析时用于保存代代码模块对应的依赖使用的对象。 module 实例的build 方法在执行对应的loader时, 处理完模块代码自身的转换后, 继续调用parser 的实例来解析自身依赖的模块, 解析后的结果放到module.dependencies 中, 首先保存的是依赖的路径, 后续会经由compilation.processModuleDependencies 方法, 再来处理各个依赖模块, 递归区建立整个依赖。
  3. Template, 生成最终代码要使用到的代码模版, 像上述提到的胶水代码久是用对应的Tempalte来生成。

Template 基础类: lib/Template.js
常用的主要Template类: lib/MainTemplate.js

(webpack编译)钩子函数调用执行顺序

手写webpack

核心使用Tapable 实现插件的binding 和 applying [webpack]webpack4源码分析 - 图2

静态资源文件指纹

解决: 静态资源实现增量更新。

  • hash。和整个项⽬⽬的构建相关,只要项⽬⽬⽂⽂件有修改,整个项⽬⽬构建的hash 值就会更更改
  • chunkhash。和 webpack 打包的 chunk 有关,不不同的 entry 会⽣⽣成不不同的chunkhash 值
  • contenthash。根据⽂⽂件内容来定义 hash ,⽂⽂件内容不不变,则 contenthash不不变

watch :轮询判断⽂件的最后编辑时间是否变化,某个⽂件发⽣了变化,并不会⽴
⽴刻告诉监听者,⽽⽽是先缓存起来,等 aggregateTimeout

1. 文件入口

entry

  1. if (/.+\/([a-zA-Z]+-[a-zA-Z]+)(\.entry\.js$)/g.test(item) == true) {
  2. const entrykey = RegExp.$1
  3. _entry[entrykey] = item;
  4. const [dist, template] = entrykey.split(“-");
  5. }


2. 你不知道的module

2.1 webpack 多核支持

  1. const HappyPack = require("happypack");
  2. const os = require("os");
  3. //开辟⼀个线程池
  4. const happyThreadPoll = HappyPack.ThreadPool({ size: os.cpus().length });
  5. module.exports.plugins = [
  6. new HappyPack({
  7. id: "babel",
  8. threadPool: happyThreadPoll,
  9. loaders: [
  10. {
  11. loader: "babel-loader",
  12. },
  13. ],
  14. }),
  15. ];

2.2 loader 的原理分析
image.png
2.3 手写loader

  • use: [‘bar-loader’, ‘foo-loader’] 时, loader 是以相反的顺序执行。
  • 最后的loader最早调用, 传入原始的资源内容(代码/二进制文件/buffer处理)。 第一个loader 最后调用, 期望返回是JS 代码和sourcemap 对象中间的loader执行时, 传入的是上一个 loader 执行的结果。
  • 多个loader 时遵循这样的执行顺序, 但对于大多数单个loader来说无需感知这一点, 只负责处理好接受的内容。
  • 当loader 中的异步处理。 有一些loader 在执行过程中可能依赖外部I/O 的结果, 导致必须使用使用异步的方式处理, 需要在loader 执行时使用this.async() 来标识该 loader 是异步处理, 然后使用this.callback返回loader 处理结果。

2.4 AST 静态语法分析树
AST(抽象语法树), 或者语法树, 是源代码的抽象语法结构的树形表现形式。树上的每个节点都表示源代码中的一种结构。抽象在于, 此处的语法并不表示真是语法中的出现的每个细节。
webpack 优势在于能将所有资源都整合成模块(不仅js 文件)。需要一些loader, 比如url-loader 等来直接在源文件中引用各类资源。最后调用acorn(Esprime)解析经loader处理后的源文件生成抽象语法树AST。

  1. Node {
  2. type: 'VariableDeclaration', // 描述该语句的类型 --变量声明语句
  3. start: 0,
  4. end: 10,
  5. declarations: [Array], // 声明的内容数组, 每一项也是一个对象
  6. type: // 描述该无语的类型
  7. id: // 描述变量名称的对象
  8. type: // 定义
  9. name: // 变量的名字
  10. init: // 初始化变量值的对象
  11. type: // 类型
  12. vale: // 值“is tree” 不带引导
  13. row: // "\"is tree"\" 带引号
  14. kind: 'var' // 变量声明的关关键字 --var
  15. }
  1. var UglifyJs = require('uglify-js')
  2. var code = 'var a = 1;';
  3. var toplevel = UglifyJs.parse(code); // 语法树
  4. var transformer = new UglifyJs.TreeTransformer(function (node) {
  5. if (node instanceof UglifyJs.AST_Number) { // 查找需要修改的叶子节点
  6. node.value = '0x'+Number(node.value).toString(16);
  7. return node; // 返回一个新的叶子节点 替换原叶子节点
  8. }
  9. });
  10. toplevel.transform(transformer);
  11. var ncode = toplevel.print_to_string();
  12. console.log(ncode); // var a=0x1;

3. Plugins

webpack实现插件机制的⼤体⽅式是:
「创建」 —— webpack在其内部对象上创建各种钩⼦;
「注册」 —— 插件将⾃⼰的⽅法注册到对应钩⼦上,交给webpack;
「调⽤」 —— webpack编译过程中,会适时地触发相应钩⼦,因此也就触发了插件的⽅法



loader

loader-utils

acorn

acorn-walk

magic-string

plugin

参考:
https://aotu.io/notes/2020/07/17/webpack-analize/index.html