Jacky's blog Jacky's blog
首页
  • 编码专题
  • 深入浅出 Vite
  • 深入浅出 babel
  • 快速上手API
  • 深入浅出 react
  • Node

    • code-notebook
  • 状态管理

    • redux
  • 前端工程化

    • Wepack
  • React源码

    • React源码
  • 组件库封装

    • 组件库
  • 开发工具

    • Vscode 插件
  • 项目展示
  • 案例中心 (opens new window)
  • First Project
  • 基础算法题
  • 链表题
  • 动态规划
  • 双指针
  • 递归
  • 数据结构
  • 前端学习计划 (opens new window)
  • 技术随笔
  • 转载文章
  • 包管理工具
  • 前端学习周报
  • VSCode插件
  • Promise 专题
  • 函数技巧
  • React 专题
  • 配置文件

    • TSCONFIG-配置 (opens new window)
    • NGINX-配置 (opens new window)
    • 正则规则查询手册 (opens new window)
    • Lint 配置 (opens new window)
  • 教程

    • GIT-教程
    • NPM SCRIPTS-工作流 (opens new window)
    • DOCKER-教程 (opens new window)
    • LERNA-教程 (opens new window)
    • GIT-常用操作整理 (opens new window)
  • VSCode

    • LAUNCH.JSON (opens new window)
  • 指令

    • NPM 指令 (opens new window)
    • NVM 指令 (opens new window)
    • Nginx 指令 (opens new window)
    • YARN 指令 (opens new window)
    • PNPM 指令 (opens new window)
  • 库

    • FS-EXTRA 库 (opens new window)
    • NODE 库-PATH (opens new window)
  • 永远的神

    • 魔法师卡颂-自顶向下学 React 源码 (opens new window)
    • 全栈潇晨 (opens new window)
    • 博客-程序员山月-Daily (opens new window)
    • 淘系前端:冴羽 (opens new window)
  • 系列文章

    • 《图解HTTP》 (opens new window)
    • 《ES6标准入门》 (opens new window)
    • 《现代JavaScript教程》 (opens new window)
    • 《深入浅出Webpack》 (opens new window)
    • VSCode 插件系列:小茗同学 (opens new window)
    • JEST 教程 (opens new window)
    • 前端精读周刊:各种精读系列 (opens new window)
    • 一文吃透系列 (opens new window)
    • 图解 REACT 原理 (opens new window)
  • 实用网站

    • MDN (opens new window)
    • CAN I USE (opens new window)
    • TYPESCRIPT-ESLint-RULES (opens new window)
    • ESLint-RULES (opens new window)
    • FRONT-END TREND (opens new window)
    • NPM TREND (opens new window)
    • 在线分析 Node 依赖 (opens new window)
    • FIND NPM (opens new window)
    • CODE PEN (opens new window)
    • 印记中文 (opens new window)
    • TOOL.LU (opens new window)
    • 阮一峰-网道 (opens new window)
    • DIGITAL OCEAN (opens new window)
    • DEVDOCS.IO (opens new window)
    • JOI (opens new window)
  • 算法

    • 小浩算法 (opens new window)
    • LABULADONG 的算法小抄 (opens new window)
    • 力扣 SOLUTION (opens new window)
    • HACKER RANK (opens new window)
    • 代码随想录 (opens new window)
  • 博客系列

    • 美团大佬 (opens new window)
    • 蜡笔小伟 (opens new window)
    • 优秀博客1 (opens new window)
    • 优秀博客2-umi (opens new window)
    • 优质博客 (opens new window)
  • CSS

    • CSS-EASING 库 (opens new window)
    • ROUGH.JS (opens new window)
    • CSS 网站收集
    • UNOCSS (opens new window)
  • 前端

    • PROMISE (opens new window)
    • UNDERSCORE.JS (opens new window)
    • study with BGM (opens new window)
    • nginx【B站视频】 (opens new window)
    • 机器学习
    • Js基础
  • 掘金已购课程

    • 前端自动化测试精讲 (opens new window)
    • 深入浅出 Vite (opens new window)
    • 现代 Web 布局 (opens new window)
    • 前端算法与数据结构 (opens new window)
    • 基于 Vite 的 SSG 框架开发实战 (opens new window)
    • SSR 实战:官网开发指南 (opens new window)
    • WebGL 入门与实践 (opens new window)
    • 玩转 CSS 的艺术之美 (opens new window)
    • 前端调试通关秘籍 (opens new window)
    • React 进阶实践指南 (opens new window)
    • TypeScript 全面进阶指南 (opens new window)
    • 前端缓存技术与方案解析 (opens new window)
    • npm scripts 前端工作流 (opens new window)
    • Webpack5 核心原理与应用实践 (opens new window)
  • 购物车

    • 张鑫旭-技术写作指南 (opens new window)
    • 深入剖析 Node.js 底层原理 (opens new window)
    • 前端开发者的现代 C++ 课 (opens new window)
    • 从前端到全栈 (opens new window)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Jacky Wang

行到水穷处,坐看云起时
首页
  • 编码专题
  • 深入浅出 Vite
  • 深入浅出 babel
  • 快速上手API
  • 深入浅出 react
  • Node

    • code-notebook
  • 状态管理

    • redux
  • 前端工程化

    • Wepack
  • React源码

    • React源码
  • 组件库封装

    • 组件库
  • 开发工具

    • Vscode 插件
  • 项目展示
  • 案例中心 (opens new window)
  • First Project
  • 基础算法题
  • 链表题
  • 动态规划
  • 双指针
  • 递归
  • 数据结构
  • 前端学习计划 (opens new window)
  • 技术随笔
  • 转载文章
  • 包管理工具
  • 前端学习周报
  • VSCode插件
  • Promise 专题
  • 函数技巧
  • React 专题
  • 配置文件

    • TSCONFIG-配置 (opens new window)
    • NGINX-配置 (opens new window)
    • 正则规则查询手册 (opens new window)
    • Lint 配置 (opens new window)
  • 教程

    • GIT-教程
    • NPM SCRIPTS-工作流 (opens new window)
    • DOCKER-教程 (opens new window)
    • LERNA-教程 (opens new window)
    • GIT-常用操作整理 (opens new window)
  • VSCode

    • LAUNCH.JSON (opens new window)
  • 指令

    • NPM 指令 (opens new window)
    • NVM 指令 (opens new window)
    • Nginx 指令 (opens new window)
    • YARN 指令 (opens new window)
    • PNPM 指令 (opens new window)
  • 库

    • FS-EXTRA 库 (opens new window)
    • NODE 库-PATH (opens new window)
  • 永远的神

    • 魔法师卡颂-自顶向下学 React 源码 (opens new window)
    • 全栈潇晨 (opens new window)
    • 博客-程序员山月-Daily (opens new window)
    • 淘系前端:冴羽 (opens new window)
  • 系列文章

    • 《图解HTTP》 (opens new window)
    • 《ES6标准入门》 (opens new window)
    • 《现代JavaScript教程》 (opens new window)
    • 《深入浅出Webpack》 (opens new window)
    • VSCode 插件系列:小茗同学 (opens new window)
    • JEST 教程 (opens new window)
    • 前端精读周刊:各种精读系列 (opens new window)
    • 一文吃透系列 (opens new window)
    • 图解 REACT 原理 (opens new window)
  • 实用网站

    • MDN (opens new window)
    • CAN I USE (opens new window)
    • TYPESCRIPT-ESLint-RULES (opens new window)
    • ESLint-RULES (opens new window)
    • FRONT-END TREND (opens new window)
    • NPM TREND (opens new window)
    • 在线分析 Node 依赖 (opens new window)
    • FIND NPM (opens new window)
    • CODE PEN (opens new window)
    • 印记中文 (opens new window)
    • TOOL.LU (opens new window)
    • 阮一峰-网道 (opens new window)
    • DIGITAL OCEAN (opens new window)
    • DEVDOCS.IO (opens new window)
    • JOI (opens new window)
  • 算法

    • 小浩算法 (opens new window)
    • LABULADONG 的算法小抄 (opens new window)
    • 力扣 SOLUTION (opens new window)
    • HACKER RANK (opens new window)
    • 代码随想录 (opens new window)
  • 博客系列

    • 美团大佬 (opens new window)
    • 蜡笔小伟 (opens new window)
    • 优秀博客1 (opens new window)
    • 优秀博客2-umi (opens new window)
    • 优质博客 (opens new window)
  • CSS

    • CSS-EASING 库 (opens new window)
    • ROUGH.JS (opens new window)
    • CSS 网站收集
    • UNOCSS (opens new window)
  • 前端

    • PROMISE (opens new window)
    • UNDERSCORE.JS (opens new window)
    • study with BGM (opens new window)
    • nginx【B站视频】 (opens new window)
    • 机器学习
    • Js基础
  • 掘金已购课程

    • 前端自动化测试精讲 (opens new window)
    • 深入浅出 Vite (opens new window)
    • 现代 Web 布局 (opens new window)
    • 前端算法与数据结构 (opens new window)
    • 基于 Vite 的 SSG 框架开发实战 (opens new window)
    • SSR 实战:官网开发指南 (opens new window)
    • WebGL 入门与实践 (opens new window)
    • 玩转 CSS 的艺术之美 (opens new window)
    • 前端调试通关秘籍 (opens new window)
    • React 进阶实践指南 (opens new window)
    • TypeScript 全面进阶指南 (opens new window)
    • 前端缓存技术与方案解析 (opens new window)
    • npm scripts 前端工作流 (opens new window)
    • Webpack5 核心原理与应用实践 (opens new window)
  • 购物车

    • 张鑫旭-技术写作指南 (opens new window)
    • 深入剖析 Node.js 底层原理 (opens new window)
    • 前端开发者的现代 C++ 课 (opens new window)
    • 从前端到全栈 (opens new window)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Node

  • VSCode插件

  • Webpack

    • webpack源码分析一:AST语法
    • webpack源码分析二:模块化语法
    • webpack源码分析三:源码分析
      • 0.前言
      • 1.调试 webpack:如何搭建一个调试 DEMO
        • 方案1:使用 link + .launch 调试方法
        • 方案2:使用 .launch + node 脚本编写
        • 方案3:使用 chrome 调试
      • 2.源码解读-梳理Hooks函数
      • 3.带着问题看源码
        • 3.1 webpack 启动方式
        • 3.2 寻找 webpack 编译起点:Entry
        • 3.3 webpack 解析阶段
        • 3.4 如何把 modules 合并为一个文件?
      • 重点概念解析
      • 参考资料
    • webpack 配置[TODO]
    • 如何利用requireContext实现批量导入
  • Redux

  • React源码

  • 组件库

  • React高阶系列

  • UMI插件

  • 前端工程化

  • 单元测试vitest

  • 重点技术
  • Webpack
wangjiasheng
2022-04-05
目录

webpack源码分析三:源码分析

# 0.前言

在前两篇中基本已经将 mini-webpack 基础原理实现,从本篇文章开始 webpack 源码之旅。

# 1.调试 webpack:如何搭建一个调试 DEMO

# 方案1:使用 link + .launch 调试方法

项目整体结构如下:

.
├── .vscode
│   ├── launch.json
├── demo
│   ├── dist
│   ├── index.js
│   ├── node_modules
│   └── package.json
└── webpack
    └── 此处可再安装 `webpack-cli`(非必要)
1
2
3
4
5
6
7
8
9
10

由于无法在node_modules 中进行调试,因此第一个方法就是使用软链接的方式替换掉原有 module,步骤如下:

  1. 准备源码工程文件,如 webpack (opens new window)、webpack-cli (opens new window)。

  2. 将源码工程下载后,需要使用git 工具回溯到指定版本,如 webpack v5.10.1

    git reset --hard v5.10.1
    
    1

    webpack 库所有的版本号中,需要在 Tags 中查找。

  3. 在 webpack 源码工程下,建立全局软链接

    yarn link 
    
    1
  4. 在测试工程下,link 注册的源码文件

    yarn link webpack
    
    1

    对于软链接的说明和使用方式,见之前我写的博客:《npm link的用法》 (opens new window)、《软链接与硬链接》 (opens new window)。

  5. 在当前项目新建.vscode文件夹,并创建launch.json 文件。

    {
        "version": "0.2.0",
        "configurations": [
            {
                "type": "pwa-node",
                "request": "launch",
                "name": "Launch Program",
                "skipFiles": [
                    "<node_internals>/**"
                ],
                "program": "${workspaceFolder}/demo/node_modules/.bin/webpack",
                "args": ["./demo/src/index.js"],
                "runtimeArgs": [
                    "--preserve-symlinks",
                    "--preserve-symlinks-main"
                ],
                "trace": true            
            }
        ]
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    参数说明:

    1. type:指定脚本执行环境,对于javascript 而言,一般可选chrome(需要搭配devtools for Chrome 插件) 和 pwa-node 。

    2. request:launch 或 attach 。

    3. name:随便写,主要也是自己看。

    4. program:需要使用 node 执行的脚本,上面是触发 npx webpack

    5. agrs:此处填写的是 webpack 解析的指定文件,默认为src/index.js

    6. runtimeArgs:这里的参数最难,google 好久才找到--preserve-sumlinks-main 这个参数,当需要调试 node_modules 中的 modules时,并且当前module 是以软链接的形式提供的。

      经测试,使用该方案调试模块时,无法通过在测试文件左侧打红点断点,而只有手动在测试代码中添加 debugger 时,才会进入调试模式。 --preserve-symlinks-main 字段介绍:node官网 (opens new window)

# 方案2:使用 .launch + node 脚本编写

此部分学习自来源于 掘金大佬:依柳诚 (opens new window) 的文章,对于调试 webpack 是非常好的教程,但是必须提前学习当前代码的使用方式,对初学者想看懂源码的还不是很友好。

项目的整体结构如下:

webpack 源码工程:
debug-|
      |--dist    // 打包后输出文件
      |--src
         |--index.js   // 源代码入口文件
      |--package.json  // debug时需要安装一些loader和plugin
      |--start.js      // debug启动文件
      |--webpack.config.js  // webpack配置文件
1
2
3
4
5
6
7
8

debug/start.js 的代码:相当于把 webpack 中创建 compiler 对象并运行的过程抽离出来了,无需经过 webpack-cli 。

//***** debug/start.js *****
const webpack = require('../lib/index.js');  // 直接使用源码中的webpack函数
const config = require('./webpack.config');
const compiler = webpack(config);
compiler.run((err, stats)=>{
    if(err){
        console.error(err)
    }else{
        console.log(stats)
    }
});
1
2
3
4
5
6
7
8
9
10
11

对应的 launch.json 脚本为:

{
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "启动webpack调试程序",
            "program": "${workspaceFolder}/debug/start.js"
        }
    ]
}
1
2
3
4
5
6
7
8
9
10

# 方案3:使用 chrome 调试

使用 chrome 的 debug 模块,具体可见博客 《如何调试 node 代码》 (opens new window)

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

# 2.源码解读-梳理Hooks函数

幕布地址:https://www.mubucm.com/doc/GomYIGiRxQ

# 3.带着问题看源码

# 3.1 webpack 启动方式

这个问题也等价于:webpack-cli 与 wabpack 的区别。

调用webpack的方式一般有以下两种方式:

# 1. 终端启动:使用 webpack-cli 脚手架

在终端中通过 webpack-cli 脚手架启动,以下几种写法均可:

./node_modules/.bin/webpack-cli  # 原始版
./node_modules/webpack-cli/bin/cli.js # 执行 webpack-cli 
npx webpack-cli # 简化写法
1
2
3

以上文件默认找的 src/index.js 文件,完整写法:

npx webpack-cli ./src/index.js --config ./webpack.config.js
1

注:在测试的时候发现,上述的 webpack-cli 也可以简写为 webpack ,但实际调用的仍是webpcak-cli的脚本,这点可以在webpack-cli/bin/cli.js 脚本中打断点验证,后续会从源码角度进行验证。

# 2. 脚本启动:直接调 webpack

在 Node 脚本中,可以直接 require 的方式:

const webpack = require('../lib/index.js')  // 直接使用源码中的webpack函数
const config = require('./webpack.config')
const compiler = webpack(config);
compiler.run((err, stats)=>{
    if(err){
        console.error(err)
    }else{
        console.log(stats)
    }
});
1
2
3
4
5
6
7
8
9
10

# 3. 源码:webpack-cli 与 webpack 的关系

从名称即可看出两者的职责是不同的,cli全称为command Line Interface ,即命令行界面。webpack-cli 赋予终端以更灵活的方式调用webpack ,如下:

# 以 生产模式 打包应用
npx webpack --mode="production"
1
2

当然,复杂的配置更推荐使用 webpack.config.js 文件配置 WebPack的参数。发送到 CLI 的任何参数都将映射到配置文件中的相应参数。

# 源码解析1:为啥 npx webpack 与 npx webpack-cli 等价

在 webpack/bin/ 中本质调用还是 webpack-cli,第一步就会去检测 webpack-cli 的安装情况,如果没有安装还会自动会当前使用的包管理器(npm、pnpm、npm)提示你去下载:

const cli = {
	name: "webpack-cli",
	installed: isInstalled("webpack-cli"),
	url: "https://github.com/webpack/webpack-cli" // 地址提前准备好
};
if (!cli.installed){
	console.err("你需要去装 cli 脚本了!")
  console.log("需不需要我帮你去装?(yes/no)") => 一堆安装逻辑
}else{
  require("webpack-cli")
}
1
2
3
4
5
6
7
8
9
10
11
# 源码解析2:webpack-cli 本质调用的还是webpack

技巧:由于 webpack 文件过于庞大,可以查阅 package.json 文件中的 main字段寻找当前模块的入口文件。

在 webpack-cli 的bin/cli.js 的代码:

if (packageExists('webpack')) {
    runCLI(rawArgs);
} else {...}
1
2
3

而 runCLI 是bootstrap.js 中的代码:

const runCLI = ()=>{
  try{
    const cli = new WebpackCLI();
    ....
    await cli.run(parsedArgsOpts, core);
  }catch(err){...}
}
1
2
3
4
5
6
7

上面的代码,通过WebpackCLI 创建了一个cli,并执行这个类的run 方法,如下:

const webpack = packageExists('webpack') ? require('webpack') : undefined;
...
class WebpackCLI{
  async run(){
    let compiler; // 构建一个编译器
    compiler = this.createCompiler(options, callback);
  }
  ....
  createCompiler(){
    let compiler;
    try {
      compiler = webpack(options, callback);
    } catch (error) {...}
    return compiler;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

由上可知,run 方法即调用WebpackCLI此类的 createCompiler 方法构造了一个编译器,而编译器的构造底层是借助 require("webpack") ,即 webpack 模块实现的。

总结一下:webpack-cli 通过调用 webpack 模块构建了一个编译器。

# 3.2 寻找 webpack 编译起点:Entry

根据第2章的内容可知,在 compile 函数中出现的钩子有:

beforeCompile --> compile --> make --> finishMake--> afterCompile。

根据经验,猜测入口分析流程应位于 make ->finishMake 之间,而两者之间并无代码,于是需要反向查找make钩子是在哪里注册的。

通过搜索关键词 hooks.make.tapAsync 找到了 lib/EntryPlugin.js中。

由于搜索出的结果很多,需要一个一个比对寻找到与入口Entry 有关的文件。

按照以下函数调用链条一层一层找:

compiler.hooks.make.tapAsync("EntryPlugin") |this指的是 compilation →\rightarrow→ this.addEntry() →\rightarrow→ this._addEntryItem() →\rightarrow→ this.addModuleChain() →\rightarrow→ this.handleModuleCreation() →\rightarrow→ this.factorizeModule() →\rightarrow→ this.factorizeModule() →\rightarrow→ this.addModule() →\rightarrow→ this.buildModule()

涉及两个文件 lib/EntryPlugin、lib/Compilation

重点模块解读:addModuleChain

......
const moduleFactory = this.dependencyFactories.get(Dep);
this.handleModuleCreation(factory: moduleFactory,....){.....}
1
2
3

通过后续分析,我们逐渐意识到工厂模式是后续所有步骤的理论基础。

其中,this 是compilation,通过在 EntryPlugin 中搜索compilation.dependencyFactories.set 可以发现如下代码:

compiler.hooks.compilation.tap(
  "EntryPlugin",
  (compilation, { normalModuleFactory }) => {
    compilation.dependencyFactories.set(
    EntryDependency,
    normalModuleFactory
  );
  }
);
1
2
3
4
5
6
7
8
9

由上可知,后续的 factory 即为 normalModuleFactory ,一般简称为 nmf 对象。

重点模块解读:handleModuleCreation:处理模块创建

// 函数定义:
handleModuelCreation{ factory, dependencies,...},callback){
  const moduleGraph = this.moduleGraph; // 找到 depRelation
  const currentProfile = ... // Profile 与性能有关,可以忽略
  this.factorizeModule(...){    // 工厂化依赖
    	this.addModule(...){newModule, (err,module)=>{
        ...
        // 将依赖添加到 depRelation 中
        for (let i = 0; i < dependencies.length; i++) {
          const dependency = dependencies[i];
          moduleGraph.setResolvedModule(originModule, dependency, module);
        }
        this.buildModule(module,err=>{....})
      }}
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上述我们得到了一条非常关键的函数调用链条:

factorizeModule() →\rightarrow→ addModuel() →\rightarrow→ buildModule()

再细化下各个函数的调用栈:

factorizeModule :factorizeQueue →\rightarrow→ _factorizeModule →\rightarrow→ factory.create()

函数内部本质就是一个工厂队列, 加入到此队列中的函数,对应只需要见处理函数为:processor

// factorizeModule 函数中
this.factorizeQueue = new AsyncQueue({
  name: "factorize",
  parallelism: options.parallelism || 100,
  processor: this._factorizeModule.bind(this)
});

// _factorizeModule 函数中:
_factorizeModule(...) {
		...
		factory.create(...){...} // 由此引出一个关键函数 factory.create
		... 
}
1
2
3
4
5
6
7
8
9
10
11
12
13

前述中已说明 factory 即为 nmf 对象,于是我们找到NormalModuleFactory.js 文件:

重点模块解读:NormalModuleFactory.create() 创建一个新模块

涉及到的 Hooks 调用次序为:hooks.beforeResolve -> hooks.factorize -> hooks.afterResolve ->hooks.createModule

// 最后一次调用:
this.hooks.createModule.callAsync(
  createData,
  resolveData,
  (err, createdModule) => {
    if (!createdModule) {
			......
      createdModule = new NormalModule(createData); // createModule
    }
    .... 
    return callback(null, createdModule); // 这里 null-> error,createModule->newModule
  }
);
1
2
3
4
5
6
7
8
9
10
11
12
13

重点模块解读:addModuel 接受 factorizeModule(也即,nmf.create()) 传递而来的 createModule

this.factorizeModule(...,(err,newModule)){
  this.addModule(newModule, (err, module) => {})
}
1
2
3

函数调用链:addModuleQueue -> new AsyncQueue -> processor: this._addModule

核心代码:

class Compilation {
  constructor(){
    this.modules = new Set();
    this._modules = new Map(); // _modules 私有变量标识当前 module 是否已经被添加过
    ...
  }
  ...
  this._addModule(){
      const identifier = module.identifier();  // 读取 module 唯一的id
      const alreadyAddedModule = this._modules.get(identifier); 
      if (alreadyAddedModule) {                // 如果添加过 module 则弹出
        return callback(null, alreadyAddedModule);
      }
      this._modulesCache.get(identifier, null, (err, cacheModule) => {
        this._modules.set(identifier, module); // 标识 module 已处理过
        this.modules.add(module);              // 将 module 存入 compilation.modules
      }
  }
  ...
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

重点模块解读:buildModule

函数调用链:buildQueue -> new AsyncQueue -> processor: this._buildModule.bind(this)->hooks.buildModule->module.build()-> this.doBuild ->hooks.succeedModule/hooks.failedModule

当前步骤是构建的重要步骤,难点在于module.build 这里的 module 是什么?NormalModule实例化

这里就不详细演示了,耐心点往上翻就会发现这里createdModule = new NormalModule(createData);

于是我们看下:NormalModule.build 做了什么?

build(...){
    this._source = null; // 源代码存放位置
    this._ast = null;    // 初始化 ast 树
    ....
    return this.doBuild(...){
  		const handleParseError = err=>{};
  		const handleParseResult = ()=>{return handleBuildDone}; // 这种写法完全可以避免回调地狱
  		const handleBuildDone = ()=>{};
  		const noParseRule = options.module && options.module.noParse;//控制当前module是否会被解析
      // 开始解析 ast 树
  		let result;
      try {
        result = this.parser.parse(this._ast || this._source.source(), { // parse阶段:_source=>_ast
         		......
        });
      } catch (e) {
        handleParseError(e);
        return;
      }
  		handleParseResult(result);
		}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

再来看下 doBuild 函数定义中,有什么值得关注的事件?

doBuild(){
  // 获取 compilation 的 Hooks
  const hooks = NormalModule.getCompilationHooks(compilation);
 	hooks.beforeLoaders.call(this.loaders, this, loaderContext);
  runLoaders(
			{
				resource: this.resource, // 加载初始代码,如 案例1中的 `es6Code`
				loaders: this.loaders,   // 将各种 loaders 加载进来。
				context: loaderContext,
				readResource: (resource, callback) => {
					const scheme = getScheme(resource); // scheme 一般指协议
					if (scheme) {
						.......
					} else {
            // 正常走此回调
						fs.readFile(resource, callback); // 加载外部资源
					}
				}
			},
			(err, result) => {...}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在 doBuild 大致做两件事:

  1. hooks.beforeLoaders:触发定义在 loaders 之前的所有事件。

  2. runLoaders:触发 loaders 阶段,这步的含义在于,webpack 只能读取js文件,通过 loader 处理非 js 文件。

    此阶段还有一个任务:读文件,即 resource 。

最后,handleModuleCreation 会将上述所有的产生的队列Queue关闭:

this.handleModuleCreation(
  {
    factory: moduleFactory,
    dependencies: [dependency],
    originModule: null,
    context
  },
  err => {
    if (err && this.bail) {
      callback(err);
      this.buildQueue.stop();
      this.rebuildQueue.stop();
      this.processDependenciesQueue.stop();
      this.factorizeQueue.stop();
    } else {
      callback();
    }
  }
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 3.3 webpack 解析阶段

解析阶段:将经上述 loaders 转化后的代码,解析为AST 语法树。

由上可知,对 sourceCode 进行 parse 的阶段是在 doBuild 函数调用 parser.parse 方法 ,其中parser ,即解析器。具体的代码可以在 lib/javascript/JavaScriptParser.js 文件中找到:

const { Parser: AcornParser } = require("acorn");
1

在 Webpack 中并未自己实现一个 parser,而是借助 acorn 的 parser 分析 JS。

在 JavaScriptParser.js 文件中我们可以验证 parse 是如何进行模块收集的:

// 遍历声明 :Block pre walking iterates the scope for【block variable declarations】
blockPreWalkStatements(statements) {
  for (let index = 0, len = statements.length; index < len; index++) {
    const statement = statements[index];
    this.blockPreWalkStatement(statement);
  }
}

blockPreWalkStatement(statement) {
  this.statementPath.push(statement);
  if (this.hooks.blockPreStatement.call(statement)) {
    this.prevStatement = this.statementPath.pop();
    return;
  }
  switch (statement.type) {
    // 对应着 动态导入: `import("....")`
    case "ImportDeclaration":
      this.blockPreWalkImportDeclaration(statement); 
      break;
    case "ExportAllDeclaration":
      this.blockPreWalkExportAllDeclaration(statement);
      break;
    
    // 对应这默认导入: `import a from "a.js"`
    case "ExportDefaultDeclaration":
      this.blockPreWalkExportDefaultDeclaration(statement);
      break;
      
    // 对应着声明导入: `import {a} from "a.js"`
    case "ExportNamedDeclaration":
      this.blockPreWalkExportNamedDeclaration(statement);
      break;
    case "VariableDeclaration":
      this.blockPreWalkVariableDeclaration(statement);
      break;
    case "ClassDeclaration":
      this.blockPreWalkClassDeclaration(statement);
      break;
  }
  this.prevStatement = this.statementPath.pop();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

根据不同的import情况,会触发不同的hooks钩子函数,这些钩子的监听函数代码保存在 lib/dependencies/HarmonyExportDependencyParserPlugin.js 中,目的是收集各个模块的依赖,将其记录在 module.denpendencies 数组中。

# 3.4 如何把 modules 合并为一个文件?

  1. 生成 chunk 阶段

    在compilation.seal() 阶段,函数会创建chunks、并为每个 chunk 进行 codeGeneration,然后为每一个 chunk 创建 assets。

  2. 生成 assets 阶段

    在第二章可知在 onCompiled 阶段,存在一个重要的函数emitAssets(),emit 意为发射,Assets 意为资产,此处则指的是合并生成后的文件,主要逻辑如下:

    emitAssets(compilation,callback){
      let outputPath; // 指定输出路径
      const emitFiles = err =>{
       	...
        const processExistingFile = stats => {
          const content = getContent(); // 获取内容
          return this.outputFileSystem.readFile(...,()=>{ // 进入 read 阶段
            	return doWrite(content)   // 进入 write 阶段
          }) 
        }
        const doWrite = content =>{
          // 执行 写 操作
          this.outputFileSystem.writeFile(targetPath, content, err => {})
        }
      };
      // 在 emit 阶段做了两件事:
      // 1. 获取输出路径
      // 2. 创建输出文件(具体写的内容,看emitFiles回调)
      this.hooks.emit.callAsync(compilation, err => {
    			if (err) return callback(err);
    			outputPath = compilation.getPath(this.outputPath, {});
    			mkdirp(this.outputFileSystem, outputPath, emitFiles);
    		});
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

# 重点概念解析

  1. Dependency Graph 的概念:Dependency Graph (opens new window)

# 参考资料

  1. https://juejin.cn/post/6844903987129352206
  2. Compilation Hooks列表 (opens new window)
编辑 (opens new window)
#Webpack
上次更新: 2022/06/26, 23:06:00
webpack源码分析二:模块化语法
webpack 配置[TODO]

← webpack源码分析二:模块化语法 webpack 配置[TODO]→

最近更新
01
如何理解浏览器的 user agent 用户代理的含义?
11-05
02
浏览器事件循环机制
10-31
03
浏览器页面渲染机制【2023】
10-15
更多文章>
Theme by Vdoing | Copyright © 2020-2023
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式