01|如果说第二篇讲的是“Node 世界终于有了秩序”,那第三篇要讲的就是:为什么浏览器没有乖乖照抄
第二篇讲到,CommonJS 最早先在 Node.js 里把模块化做成了真正可执行的制度。
那套制度为什么有力量?
因为它第一次把这些事情一起纳入了运行时秩序:
- 每个文件是模块
- 依赖应该声明
- 接口应该导出
- 运行时负责解析、缓存和拼装
这当然很强。
可历史真正有意思的地方,不在这里。
真正有意思的是:
既然 CommonJS 已经这么像答案了,为什么浏览器世界没有直接照抄?
很多人后来回看这段历史,会把 AMD 的出现写成一次多余分裂。
好像本来已经有了正确答案,前端社区偏要再造一个。
这其实写反了。
更接近真实的说法应该是:
浏览器没有照抄 CommonJS,不是因为它看不懂,而是因为它根本活在另一套现实里。
这就是第三篇真正要讲的起点。
AMD 不是为了和 CommonJS 抬杠才出现的。
它是浏览器世界被自己的物理条件逼出来的一次制度反击。
02|CommonJS 在 Node 里顺,不代表它在浏览器里也顺,因为浏览器模块加载首先是网络问题
这件事必须先讲透。
因为模块化之争里最容易被忽略的一层,就是:
Node 世界的模块加载,本质上更像文件系统问题;浏览器世界的模块加载,本质上首先是网络问题。
这差别非常大。
在 Node.js 里,require('./foo') 背后的想象是:
- 文件就在本地
- 读文件成本相对可控
- 同步装载不会直接把用户卡在页面前
- 启动阶段做一点同步工作是可以接受的
可浏览器不是这样。
浏览器一旦要加载一个模块,背后常常意味着:
- 发请求
- 等网络
- 处理阻塞
- 考虑首屏
- 避免把页面体验拖死
这时如果你还想把 require() 当成一个“立刻返回结果”的同步动作,
问题马上就出来了。
因为浏览器没有答应你“模块一定已经在手边”。
它更常见的现实是:
模块还在路上。
这就是第三篇最关键的现实基础。
不是浏览器社区不喜欢 CommonJS 的写法。
而是它无法假装网络不存在。
03|所以浏览器侧真正想要的,不只是“有模块”,而是“有一种能和异步加载和平相处的模块制度”
这一步特别关键。
因为它解释了为什么 AMD 不是对模块化本身的背叛,反而是对模块化要求更完整之后的结果。
浏览器社区当时真正要解决的,不只是把代码拆文件。
他们还想一起解决这些问题:
- 脚本不要再靠人工排顺序
- 依赖关系要明确
- 模块不要污染全局
- 加载过程要适合浏览器
这最后一条,恰恰就是 Node 世界没那么敏感、浏览器世界却绕不开的点:
模块制度必须和异步加载兼容。
如果兼容不了,那你就只能在两种坏事里选一种:
- 要么继续靠
<script>标签和手工顺序硬撑 - 要么同步加载,把页面和调试体验一起拖慢
所以 AMD 真正抓住的,不是“声明依赖”这么简单。
而是更硬的那个要求:
依赖应该被声明,而且声明方式必须允许加载器先把依赖异步取回来,再按正确顺序执行。
这是浏览器制度和 Node 制度真正分叉的地方。
04|RequireJS 为什么会变成这条路线最重要的代表?因为它不是空想,而是从 Dojo loader 的现实痛感里长出来的
如果你去看 James Burke 自己写的 RequireJS History,会发现这条路并不是纸上设计。
它是从很具体的浏览器加载痛感里长出来的。
Burke 之前做过很多 Dojo Loader 相关工作。
而那套经验让他非常清楚:
- 同步
XHR加载很别扭 eval风格方案调试体验不好- 直接把一切都押在服务器端转译也会把前端开发门槛抬高
也就是说,RequireJS 最早瞄准的,并不是“做一个更学术正确的模块理论”。
它瞄准的是:
能不能给浏览器世界一套足够能跑、足够好调试、足够不依赖重型服务端前置处理的模块加载制度。
这点很重要。
因为它再次说明,JavaScript 的很多制度分叉都不是理念先打架。
而是:
环境先疼,社区才会长出新写法。
从这个角度看,RequireJS 很像浏览器模块制度史里的 Node.js。
不是因为它们长得一样。
而是因为它们都先抓住了真实环境里的硬问题。
05|于是 AMD 真正给出的,不是“另一个 require”,而是 define 这套先声明依赖、后执行模块的浏览器合同
这一点一定要立住。
因为如果只从表面看,很多人会觉得:
AMD 不就是又搞了一套不同写法吗?
但它真正改掉的,其实是执行模型。
CommonJS 那边的直觉是:
代码执行到 require() 时,把依赖拿来,马上继续往下跑。
而 AMD 的核心想法是:
不要假设依赖已经在场。
先把模块定义出来。
先把依赖列表写清楚。
等加载器把这些依赖都搞定,再来执行 factory。
这就是 define([...], function (...) {}) 这套写法背后的真实意义。
它不是为了多包一层函数显得时髦。
而是为了把模块“注册”和“执行”拆开。
这样一来,浏览器加载器才有空间去做它必须做的事情:
- 异步抓脚本
- 并发请求
- 按依赖顺序安排执行
- 尽量不把全局搞脏
所以第三篇里最重要的一层不是 API 长什么样。
而是:
AMD 通过 define() 这层合同,第一次把浏览器的异步现实正式写进了模块制度本身。
06|这也就是为什么 James Burke 会反复强调:AMD 不是在唱反调,而是在说“浏览器就该按浏览器的现实来设计模块”
RequireJS 的官方文档其实讲得很直白。
它对 CommonJS 的态度并不是“那边全错了”。
它真正想说的是:
CommonJS 确实很有价值,
但它最初并没有完全拥抱浏览器里那些改变不了的现实:
- network loading
- inherent asynchronicity
这句话特别关键。
因为它把浏览器侧的立场讲得很清楚:
不是我们反对模块制度,而是我们反对一套假装浏览器不是浏览器的模块制度。
所以 AMD 的历史位置非常微妙。
它既吸收了 CommonJS 的一些优点:
- 字符串 ID
- 显式依赖
- 模块值概念
又主动改掉了最不适合浏览器的那部分默认假设:
- 同步拿依赖
- 过重依赖运行时即时解析
这其实不是背叛。
这是本地化改造。
更准确一点说,是:
浏览器社区第一次把“我们这个环境的物理条件”升格成模块制度设计的起点。
07|所以 AMD 之所以会显得“啰嗦”,不是因为它笨,而是因为它把浏览器加载器原本偷偷干的事,提前写到了台面上
这是 AMD 最容易被误解的一层。
很多人今天看 define(['jquery'], function ($) {}),第一反应是:
太绕了。
不如 require() 直接。
这感觉当然可以理解。
但它背后其实对应的是一个真实交换:
你多写了一点样板,是为了让加载器少猜一点。
在浏览器里,如果依赖关系不提前说清楚,加载器就只能:
- 猜
- 扫描
- 插桩
- 借助额外转换
而这些办法都会带来新的问题:
- 调试体验差
- 工具要求高
- 边界情况多
- 对静态文件开发不友好
所以 AMD 看上去更“声明式”,并不是审美偏好。
而是浏览器世界做出的一个交换:
开发者多声明一点,加载器就能更稳、更早、更异步地把事情安排好。
这也是为什么 AMD 后来会被很多人描述成:
不一定是最漂亮的,
但在当时浏览器环境里,它是“最低阻力路径”。
这判断很重要。
因为它再次提醒你:
模块制度从来不是单纯拼语法手感。
它是在不同环境里,找最能活下来的折中。
08|可 AMD 的真正历史意义,还不只是在浏览器里能跑,它更重要的是:它证明了“浏览器原生没有模块”时,社区也能先造出一套接近制度级的现实
第三篇如果只写成“浏览器为什么要异步”,还是不够。
更值得记住的是,AMD 和 RequireJS 一起证明了一件事:
即便语言标准和浏览器原生能力都还没到位,社区依然可以先造出一套足够稳定、足够可推广、足够能支撑大型前端项目的模块现实。
这非常重要。
因为它直接影响了后面好多年 JavaScript 世界的心理:
大家越来越习惯一件事:
标准可以慢,社区不会等。
只要现实已经够痛,
工具、规范、加载器、构建器就会先长出来。
AMD 的成名,本质上就是这种社区能力的一次大规模展示。
它让大家第一次看到:
- 浏览器模块不是只能停留在技巧层
- 它也可以被做成一种广泛传播的工程制度
- 而且还能反过来影响后来标准层怎么想问题
这也是为什么 RequireJS 官方文档反复强调一个意思:
先在今天的浏览器里把可工作的模块系统跑起来,才能为未来的标准模块制度积累真实世界经验。
翻成人话就是:
先在现实里把它跑起来,未来标准才更有可能不脱离地面。
09|为什么 AMD 看起来像分裂,背后却是浏览器在自立门户
把前面这些线叠一起看,第三篇最重要的判断可以压成一句:
AMD 不是社区故意分裂,它是浏览器现实拒绝被 Node 规则统治后的制度自立。
这里的重点不在“拒绝”这两个字有多戏剧化。
而在于它说明:
JavaScript 模块化从来不是单中心演化。
CommonJS 先在 Node 里做成制度,并不自动意味着浏览器必须服从。
浏览器有自己的约束。
自己的性能问题。
自己的加载机制。
自己的调试需求。
当这些现实足够硬时,它就一定会反过来塑造模块 API。
而 AMD 正是这次反塑造最典型的产物。
它不是终局。
但它是一个非常关键的中途站。
因为没有它,浏览器世界对模块制度的要求就不会被说得这么明确。
后面的 bundler、ESM、甚至很多关于静态依赖和异步加载的讨论,也都会少掉一大块现实基础。
10|AMD 第一次把浏览器的异步现实写进了模块制度
模块化江湖的第三篇,最值得记住的,不是“define() 和 require() 谁更好记”这种表层区别。
更值得记住的是:
AMD 出现时真正改变的,不只是写法,而是浏览器世界第一次把自己的异步加载现实,正面写进了模块制度。
RequireJS 最重要的历史贡献,也不只是“它曾经很流行”。
更是:
它让浏览器侧第一次拥有了一套不靠全局变量、不靠手工排 <script>、也不必强依赖服务端预处理,就能相对系统地组织模块依赖的现实方案。
所以第三篇如果只记一句,就记这句:
AMD 不是社区为了和 CommonJS 对着干才出现的,它是浏览器现实在说:如果模块制度要在这里活下去,那它就必须先学会和异步加载和平相处。
编者注(事实核对):文中关于 RequireJS 与 AMD 的起源,主要依据 James Burke 在 RequireJS History 中对 Dojo loader、RunJS、Transport/C、以及 AMD 形成过程的回顾。关于 AMD 的设计动机,主要依据 Why AMD? 与 Why Web Modules? 中对浏览器网络加载、异步性、调试与无服务端预处理需求的说明。关于 AMD API 本身,主要依据 amdjs 维护的 AMD 规范文档,以及 CommonJS wiki 中保留的 Modules/AsynchronousDefinition 历史页面。正文将 AMD 概括为“浏览器现实对 Node 规则的一次制度反击”,属于基于其浏览器适配目标与后续历史影响做出的总结。
关键人物速览
- James Burke:
RequireJS的发起者。第三篇里他代表的是“浏览器模块制度不能假装网络不存在”的那条线。 - Kris Zyp:
AMDAPI 形成过程中的关键人物之一。理解它怎样从Transport/C往真正的异步模块定义推进,绕不开他。 - Tom Robinson:AMD 讨论过程中的重要参与者之一。理解这套 API 怎样在现实工具链与语法便利之间折中,他这条线很典型。
参考与延伸阅读
RequireJS History
https://requirejs.org/docs/history.htmlWhy Web Modules?
https://requirejs.org/docs/why.htmlRequireJS API
https://requirejs.org/docs/apiAMD API Specification
https://github.com/amdjs/amdjs-api/blob/master/AMD.mdCommonJS: Modules/AsynchronousDefinition
https://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition
下篇预告:bundler 为什么成了临时宪法。