一、为什么要写这个系列?
谨以此专栏向我的老板以及像他一样致力于开源繁荣的大佬致敬!
首先排除摸鱼,因为最近忙死,几乎无鱼可摸。主要两点原因吧,说来有趣:
1. 致敬我的老板
如果你有个框架作者的老板
是种什么感觉?是的,我的老板就是框架作者!要说周会唯一开心的事儿莫过于他第一个汇报工作,他的汇报总是带着技术分享的,他总是说就是随便讲讲,你会发现他讲的都是思考,都是原理,都是源码,甚至会分析这么干的优劣,还会带着更好的实现思路。
每次请教问题,他都能非常流利的回答,甚至一些官网文档都没提及的特性,它都能如数家珍般的娓娓道来。佩服之余,我觉得我也应该再进一步!
来了我司之后是我的老板让我看到确实有一大帮人在看源码、写源码,并且将技术付诸于业务实践,并非无效卷!g
2. 总结+鞭策
我在动笔之前,webpack v4 的前半部分源码我看了大概有5/6遍了,持续时长超过2年,中间因为种种原因被打断过好几次,看了忘,忘了看,反反复复的。此外,在 buildModule 递归收集依赖的地方总是会卡住,上周末我看了一个下午,终于递归收集依赖的部分攻克了。
看了很多很多文章,好多文章把关键点都省略掉了,这部分关键点省略之后是看不懂的,同时为了给自己定个目标也是为了好好的梳理,我决定动笔写个专栏,秉持着写只要看就能懂
的源码分享文章,把自己遇到的问题平铺直叙的写出来。
立个 FLAG,2024年内完成这个专栏,我预计应该会在 100 篇左右,预祝各位看官老爷早日成为尊贵的奥迪车主!!!
二、Demo 准备
本专栏讲述的是 webpack v5.x 的源码,至于为啥不讲 webpack v4,答案很简单:我的 webpack 4 没看完😂
2.1 demo 仓库地址
2.2 demo 依赖信息
上 package.json 文件:
json
{
"name": "w5-itm",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"build": "webpack",
"watch": "webpack --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.20.2",
"@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.20.2",
"babel-loader": "^9.1.0",
"clean-webpack-plugin": "^4.0.0",
"html-webpack-plugin": "^5.5.0",
"webpack": "^5.75.0",
"webpack-cli": "^4.10.0",
"webpack-virtual-modules": "^0.5.0"
}
}
2.2 webpack.config.js 配置
这里提一点,我们的配置是个动态的过程,我们以打包一个 bundle 为例子开始,主要分析 webpack 如何从模块变成 bundle 的过程。
在这个过程中,后面这个配置文件是会成长的,最初的配置就是下面这个样子,其中 loader 部分我们只配置了 babel-loader 和 一个我们自己写的测试 loader!
js
module.exports = {
entry: {
bundle1: './src/index.js',
// bundle2: './src/a/d.js'
},
cache: {
type: 'filesystem',
cacheDirectory: path.join(__dirname, './.cache'),
buildDependencies: {
config: [__filename]
}
},
// mode: 'development',
mode: 'production',
devtool: 'source-map',
output: {
filename: 'bundle.[chunkhash:4].js',
path: path.resolve(__dirname, './dist')
},
recordsPath: path.join(__dirname, 'records.json'),
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [
[ // babel transform-runtime 暂时屏蔽,会额外多处很多模块,不利于看到现状
'@babel/plugin-transform-runtime',
// {
// corejs: 3
// }
// ]
]
}
}
]
}
]
},
resolve: {
alias: {
Src: path.resolve(__dirname, './src')
},
extensions: ['.js', '.jsx', '.json'],
fallback: {
assert: require.resolve('assert'),
buffer: require.resolve('buffer')
},
},
plugins: [
new CleanWebpackPlugin()
]
}
2.3 src 目录结构
src 下的目录结构如下,index.js 是整个构建的主入口!
text
.
├── index.css
├── index.html
├── index.js
├── ok.js
└── some-dir
├── a.js
├── b.js
├── c.js
├── d.js
├── e.js
└── f.js
三、调试环境准备
调试环境依旧使用我的 Webstorm 调试环境,不需要配置,开箱即用,如果你 vsc 用户,自己搜一下 vscode 配置 node.js 断点调试(我一直用 webstorm,因为真的太好用了)
3.1 在 Webstorm 中加入断点
你只需要在要调试的代码行号处点击一下即可,点击后行号处出现一个红点,表示断点已经生成:
3.2 启动调试
webpack 调试不能通过右键 debugger 调试某个文件,因为这个文件不是入口,我们需要调试整个构建流程,因此需要以调试方式运行构建命令,你需要在 package.json 中加入 webpack 命令:
json
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"build": "webpack",
"watch": "webpack --watch"
},
我们接下来要调试 build 这个命令,你会发现 webstorm 在 build 行号处有个绿色小三角,点击这个三角,在弹出菜单中选择 Debug 'build'
当代码运行到断点处就会停住,如下图:
四、webpack 基础知识
在开启正式的代码阅读之前,我们需要你有以下基础知识,相信这一波就够劝退一大票人的:
4.1 Tapable 和 Hook
Tapable
是个发布订阅的事件库。与 event-emitter 不同的是,这个库提供了一种钩子机制(Hook),相当于是个事件名。这些 hook 有的是同步的(SyncHook),有的是异步的(SsyncHook),有的是串行的(SeriesHook),有的是并行(ParallelHook)的,有的有返回(WaterfallHook)值,有的是有熔断(BailHook)的....
关于 Tapable 已经有很多文章再说了,虽然这里不再赘述,但是我们希望你有这部分基础!
4.1 webpack 中的常用名词及含义
-
webpack 生命周期
:webpack
基于Tapable
库实现,webpack 中设计了很多 hook,这些钩子可以被任意订阅,而 webpack 的核心工作流也是通过指定的节点去触发指定 hook 来推进的。这些 hook 被称为 webpack 生命周期 hook。 -
plugin
:插件,上面介绍 hook 的时候说了,webpack 内部注册了很多 hook 供订阅,那么订阅这些钩子的一方实现某些固定的功能的就是一个 webpack 插件了。订阅某个流程节点钩子,webpack 当执行流推进到这个节点时,就会触发这个钩子并且传入一些重要的对象如 Compiler/Compilation/NormalModuleFactory/Parser... -
Compiler: 编译器,由 webpack 创建的编译器对象,继承自 Tapable,整个构建声明周期之后一份,负责调度 webpack 顶层的生命周期 hook:beforeRun,run,compilation,thisCompilation,compile,make,emit....(这些钩子有个印象就行,后面会细讲)
-
Compilation: 由 Compiler 创建,同样继承自 Tapable,负责具体的构建工作,比如创建 Module,生成 Module Graph,Seal,Render 这些具体的构建工作细节。
-
Resolver:资源(loader、代码模块)路径解析器,继承自 Tapable,负责解析被引用到代码模块、loader 模块的资源路径,webpack 的 Resolver 基于 enhanced-resolve 这个库创建
-
ModduleFactory:模块工厂类,继承自 Tapable,用于创建模块对象,我们手写的 js 模块是文本文件,webpack 通过 ModuleFactory 提供的工厂函数将我们写的 js 代码变成 Module 对象,期间会调用对应的 loader 进行预处理。这里我们主要讨论 NormalModuleFactory(NMF);
-
Parser:代码转 AST 的解析器,webpack 使用的是 acorn,Parser 同样继承自 Tapable。解析的重要意义在于通过分析 AST 找到这个模块依赖的其他模块,进行依赖收集。当然,不同的 AST 解析目的不一,这里强调的是 webpack 的 AST 解析,如果是 Babel 的 AST 解析自然是为了转义!
4.2 webpack 构建流程
- 首先 webpack 通过 webpack-cli 启动,期间会整合命令行参数;
- 然后通过 webpack 创建 Compiler 对象(这个过程伴随着 Compiler 的顶层生命周期 hook 的注册);
- Compiler 创建 Compilation 对象(这个过程伴随着 Compilation 生命周期 hook 的注册);
- 接着通过 Compilation 的生命周期开启构建流程,直到最后生成 bundle 文件!
五、总结
本文详述了后续代码的 demo 和 webpack 源码阅读的基础:
- Tapable 库及 hook 机制;
- webpack 基础概念及作用;
- webpack 整个运行流程;