这个仓库会通过 Git 提交历史,一步步把 mini-webpack 搭出来。
目标不是尽快把功能堆满,而是按最适合理解 webpack 的顺序,让同一个核心打包流程慢慢长出来。
换句话说,这个仓库更在意“为什么下一步应该是它”,而不是“还能不能再多加一个功能”。
每一条提交只做一件事,所以这条历史本身就是学习路线。
你可以先看完整代码,再按顺序回看提交,观察同一个 index.js 是怎么一步步长出来的。
常用方式:
git log --oneline --reverse如果你想看某一步的具体变化,可以继续用:
git show <commit-id>如果你想按顺序体验每一步,可以从最早的提交开始,一步步切过去看。
如果目标是学 webpack 的核心,而不是尽快做一个能处理所有文件的打包器,那顺序非常重要。
最容易走偏的方式,是太早去做这些东西:
- 先做 loader 大全
- 先做 plugin 大全
- 先做复杂配置
- 先做压缩、分包、热更新
这样虽然看起来“功能更多”,但会把 webpack 最核心的那条主线打散。
webpack 真正最值得先弄明白的,是这条链:
- 从哪个文件开始
- 怎么发现它引用了谁
- 怎么继续向下找到更多文件
- 怎么给每个文件一个稳定编号
- 怎么把多个文件放进一个最终文件
- 怎么让最终文件自己能运行
- 怎么处理不是普通代码的文件
- 怎么把转换和流程扩展交给外部
只有这条主线通了,后面的 loader、plugin、缓存、分包,才知道该挂在哪、为什么能挂上去。
| 步骤 | 当前问题 | 新增能力 | 为什么重要 |
|---|---|---|---|
| 1 | 打包从哪里开始 | 入口文件和输出文件 | 先跑通最小闭环 |
| 2 | 入口引用了别的文件怎么办 | 找到第一个依赖并合并 | 从单文件走向多文件 |
| 3 | 依赖还能继续引用依赖怎么办 | 递归查找依赖 | 得到完整文件关系 |
| 4 | 同一个文件被多次引用怎么办 | 复用同一个文件和运行结果 | 避免重复打包和重复执行 |
| 5 | JSON 不是代码怎么办 | 先把 JSON 转成代码 | 引出文件转换 |
| 6 | 转换规则写死怎么办 | 外部配置 loader | 让核心保持通用 |
| 7 | loader 需要和打包器沟通怎么办 | loader 上下文 | 让转换过程能传回信息 |
| 8 | 想改打包流程怎么办 | plugin 钩子 | 让外部参与流程 |
| 9 | CSS 不是代码怎么办 | CSS loader | 让样式也能被入口引用 |
flowchart LR
A["入口文件"] --> B["读取内容"]
B --> C["按规则转换"]
C --> D["分析引用"]
D --> E["建立文件关系"]
E --> F["生成最终文件"]
F --> G["运行最终文件"]
先只做一件最小的事:找到入口文件,把它放进最终文件里,并确认最终文件能运行。
这一步故意不处理引用关系,因为现在最重要的问题还不是“有多少文件”,而是“打包工具到底从哪里开始”。
为什么下一步不是直接做 loader 或 plugin?
反例是:如果入口和输出都没跑通,就急着做扩展能力,后面任何问题都分不清是核心流程错了,还是扩展机制错了。
让入口文件引用第一个普通代码文件。
这一版开始解决一个真实问题:入口文件通常不会把所有代码都写在自己里面,它会引用别的文件。
这一步主要做三件事:
- 读入口文件
- 找到入口文件引用了谁
- 把入口和被引用文件一起放进最终文件
为什么下一步不是立刻做 JSON 或 CSS?
反例是:如果连普通代码文件之间怎么互相找到都没弄明白,直接处理更多文件类型,只会把问题混在一起。
让被引用的文件也能继续引用别的文件。
这一版解决的是“只看入口一层不够”的问题。
入口文件引用 foo.js,foo.js 又引用 message.js。打包工具不能只停在入口,而是要顺着这条线继续往下找。
为什么要从当前文件的位置继续找?
反例是:如果永远从入口目录找文件,一旦文件放进子目录,里面的相对引用就会找错地方。
让同一个文件只进入最终文件一次,并且运行时只加载一次。
这一版解决的是“重复引用”的问题。
入口文件直接引用了 message.js,foo.js 也引用了同一个 message.js。打包工具应该知道这其实是同一个文件,而不是复制两份。
为什么不是每次用到都重新执行?
反例是:如果一个文件里有初始化逻辑,每次引用都重新执行,就可能重复初始化,结果会不稳定。
让 JSON 文件也能被入口使用。
这一版解决的是“不是所有被引用的文件都是普通代码”的问题。
JSON 不能直接当作代码执行,所以要先把它变成代码能理解的样子,再继续参与打包。
为什么不是把 JSON 单独扔出去?
反例是:如果代码里写的是 import user from './user.json',但打包工具只是把 JSON 另存一份,最终代码还是不知道 user 从哪里来。
把文件转换规则交给外部配置。
这一版解决的是“转换规则不应该写死在核心里”的问题。
核心打包流程只负责读配置、匹配规则、执行转换。至于 JSON 怎么转,交给外面的转换函数。
为什么转换要发生在找依赖之前?
反例是:JSON 原始内容不是代码,如果先找依赖,解析阶段就已经失败了。要先把它变成代码能理解的样子,后面才能继续分析。
让转换函数也能把信息交回给打包器。
这一版解决的是“转换函数不一定只是改内容”的问题。
比如处理某个文件时,转换函数可能想记录自己处理过谁。打包器给它一个可以沟通的入口,它就不再只是一个孤立函数。
为什么不是只让转换函数返回字符串?
反例是:如果以后转换过程还要记录额外文件、缓存信息或调试信息,只返回字符串就不够用了。
给打包过程加插件入口。
这一版解决的是“不是所有扩展都适合放在文件转换阶段”的问题。
loader 更适合处理文件内容;plugin 更适合参与打包流程,比如在写出最终文件前改变输出位置。
为什么不继续用 loader 做所有事?
反例是:改变输出文件名并不是某个源文件的内容转换,把它塞进 loader 会让职责混乱。
让 CSS 文件也能被入口使用。
这一版解决的是“样式文件不是普通代码”的问题。
CSS 原始内容不能直接在最终文件里执行,所以 loader 会把 CSS 文本包成一段普通代码。最终文件在浏览器里运行时,可以把样式放进页面;在测试里,也能把这段 CSS 当作字符串拿来确认。
为什么 CSS loader 不应该改核心打包流程?
反例是:如果每来一种文件类型都去改 index.js,核心流程会越来越乱。更好的方式是让核心只认“外部规则”,具体怎么转换交给对应 loader。
现在这个 mini-webpack 可以:
- 从入口文件开始打包
- 自动找到普通代码依赖
- 继续向下找到嵌套依赖
- 避免重复打包同一个文件
- 避免重复执行同一个文件
- 通过 loader 处理 JSON
- 通过 loader 处理 CSS
- 通过 plugin 改变输出位置
运行:
npm test它会先生成最终文件,再运行最终文件,确认输出符合预期。