01|如果说第一篇讲的是“旧办法为什么撑不住了”,那第二篇要讲的就是:第一套真制度为什么先长在 Node 里
第一篇讲到,JavaScript 在没有正式模块制度的时候,并不是完全不会组织代码。
它有过很多自救办法:
- 命名空间
- 对象字面量
- 闭包
- IIFE
- module pattern
这些办法都很聪明。
可它们共同的上限也很明显:
它们能改善隔离,却不能真正制度化依赖管理。
你还是得自己安排加载顺序。
你还是得自己记住谁依赖谁。
你还是得在没有统一规则的情况下,把一堆文件勉强拼成一个可运行整体。
所以模块化江湖到了第二篇,就必须进入一个新的历史阶段:
第一套真正可工作的、能让大量项目直接照着写的模块制度,到底是怎么出现的?
答案不是浏览器先给的。
而是 Node.js 世界里的 CommonJS。
这点很关键。
因为它说明 JavaScript 模块化最早的突破,不是语言标准先觉醒。
也不是浏览器厂商先给了解法。
而是:
服务器侧运行时先碰到了更痛的现实,于是它率先把“模块”这件事做成了真正的日常制度。
02|CommonJS 之所以会先赢,不是因为它最完美,而是因为服务器世界比浏览器更急着要一套秩序
如果只看今天的结果,很容易误以为 CommonJS 是因为设计特别高明,才会先流行起来。
其实不完全是。
它当然有自己的清晰之处:
require()exportsmodule
这些东西都很直接。
可它最早真正占到便宜的地方,并不是“抽象更美”。
而是:
服务器和本地运行时环境,比浏览器更早进入“没有模块制度就真的很难继续写”的阶段。
为什么?
因为浏览器那边虽然也乱,但早期很多脚本还勉强能靠页面顺序和小规模代码撑着。
而服务端和命令行环境一旦开始认真写程序,需求会立刻变得更硬:
- 文件要拆分
- 功能要复用
- 库要引用
- 代码要长期维护
这时候,如果还停留在“大家尽量别重名”的阶段,就已经不够了。
所以 CommonJS 先冒出来,首先不是文化偶然。
它是环境需求先顶出来的。
Node 世界不是更高贵,它只是更早疼到必须立规矩。
03|而 CommonJS 真正提供的,也不是某种玄妙理论,它给的是一套足够朴素、足够能跑的模块合同
这一点一定要写清楚。
因为 CommonJS 最早吸引人的地方,不是复杂,而是简单。
它给出的那套合同,核心其实非常朴素:
- 每个文件就是一个模块
- 用
require(id)拿别的模块 - 用
exports/module.exports暴露自己的接口
这一下,很多以前只能靠约定硬撑的事情,突然第一次有了统一写法。
比如:
- 依赖不再只存在脑子里,而是写进代码里
- 对外接口不再靠“谁往全局挂了什么”,而是靠明确导出
- 文件边界第一次被默认当成模块边界
这特别重要。
因为它意味着 JavaScript 社区第一次拥有了一套:
不是靠编码技巧,而是靠共同合同来组织代码的办法。
而且 CommonJS 规范从一开始就说得很直白:
模块需要顶层私有作用域,需要导入别的模块的单例对象,也需要导出自己的 API。
这不是小修小补了。
这是第一次有人认真在说:
JavaScript 里的“模块”应该被当成一个正式制度单位来对待。
04|Node.js 为什么会和这套合同贴得这么紧?因为它天然就活在“文件系统 + 同步装载”的世界里
这才是第二篇真正的主冲突。
CommonJS 并不是凭空赢的。
它之所以在 Node.js 里看起来特别自然,是因为二者在现实假设上几乎严丝合缝。
Node.js 运行在什么环境里?
- 本地文件系统
- 服务端进程
- 非浏览器页面
- 没有
<script>标签排队那套历史包袱
在这种环境里,很多事情突然变得简单:
如果模块就是文件,
那就从文件系统里读。
如果依赖写成 require('./foo'),
那就按相对路径找。
如果模块第一次加载后应当复用,
那就缓存起来。
也就是说,CommonJS 最核心的几条直觉,在 Node.js 里几乎都成立:
- 文件天然就是模块容器
- 同步加载在启动和服务端场景里是可接受的
- 缓存单例模块很合理
- 相对路径解析很自然
这时候,模块制度就不再像前一篇那些“聪明补丁”。
它开始真的变成一种运行时基础设施。
所以第二篇必须记住一句:
CommonJS 不是抽象地赢了,它是先在最适合它的现实里赢了。
05|这也是为什么 Node 里的 CommonJS 看上去那么顺:它第一次把“每个文件都有自己边界”做成了默认现实
这一点特别容易被今天的人低估。
因为我们太习惯文件级模块了。
可在 JavaScript 前史里,这不是默认常识。
Node.js 的 CommonJS 机制有一个非常强的后果:
每个文件默认就是一个独立模块。
Node 官方文档后来写得很清楚:
- each file is treated as a separate module
exports是module.exports的简写- 模块代码会先被 wrapper function 包起来再执行
这意味着什么?
意味着以前第一篇里那些靠 IIFE、闭包辛苦换来的边界感,在这里突然变成了默认配置。
你不必先写一层自执行函数才获得私有作用域。
运行时直接就帮你做了这件事。
这其实是非常大的心理跃迁。
因为它让 JavaScript 开发者第一次不必再把“避免泄漏到全局”当成额外技巧。
从这里开始,反而是全局暴露才成了例外。
而模块边界成了默认。
这就是制度化和技巧化最大的差别。
技巧是你得主动维持。
制度是环境帮你维持。
06|所以 CommonJS 在 Node 世界真正解决的,不只是导入导出,而是把依赖、边界、缓存和入口这些事一起纳入了秩序
这也是为什么第二篇不能只写成“require / exports 教程”。
那太轻了。
CommonJS 在 Node.js 里真正厉害的地方,是它一次性把几件之前一直散着的问题,一起纳入了同一套秩序:
- 模块边界:每个文件就是一个模块
- 依赖声明:通过
require()明确写出 - 导出接口:通过
exports/module.exports - 运行缓存:第一次加载后缓存,后续复用
- 主模块识别:
require.main/module
这很关键。
因为现代模块制度真正有力量,不是某条语句长得漂亮。
而是它把“程序如何由多个单元拼起来”这件事,变成了可预测的公共规则。
在第一篇里,很多事还是靠人记。
到了 CommonJS 这里,很多事第一次开始能靠环境保证。
这才叫秩序。
所以 CommonJS 的历史意义,不只是它让 JavaScript 可以拆文件。
更是:
它第一次让 JavaScript 世界相信,模块可以不是写作风格,而是运行时承诺。
07|但也正因为它太贴 Node 世界,它从一开始就埋下了一个问题:这套秩序并不天然属于浏览器
这件事一定不能漏。
否则第二篇就会把 CommonJS 写成“正确答案本来就已经出现,只是后来大家绕远路”。
事实不是这样。
CommonJS 在 Node.js 里很顺,
可它顺的前提恰恰也是它的边界:
它默认的是服务器 / 本地运行时现实,而不是浏览器现实。
浏览器世界和 Node 世界最大的不一样在于:
- 浏览器模块加载常常意味着网络请求
- 页面初始化对加载顺序和首屏更敏感
- 同步装载在浏览器里代价很高
- 用户面对的是下载和阻塞,而不只是本地读文件
所以 require() 这套在 Node 里很自然的直觉,到了浏览器就会开始别扭。
这里最重要的不是技术细节,而是历史判断:
CommonJS 先赢,证明的是它先找到了自己的主场;并不等于它天生就是全 JavaScript 世界的终极答案。
这一点,后面 AMD 为什么会出现,就完全建立在这里。
不是大家故意分裂。
而是浏览器现实根本不会自动服从 Node 那套假设。
08|为什么 CommonJS 不是先统一世界,而是先把 Node 世界变成了世界
把前面这些线叠一起看,第二篇最重要的判断可以压成一句:
CommonJS 不是先统一了天下,它是先把 Node 这个世界治理成了世界。
这句话什么意思?
就是说,它最先赢下的,不是所有运行时。
它最先赢下的是一个足够重要、足够会扩张、后来足够影响工具链和生态的主场。
一旦 Node.js 起势,后果就会非常大:
- 大量服务端代码开始默认采用
CommonJS - npm 生态围着这套模块制度长
- 后来的构建工具、CLI、脚本体系也被它深度影响
这时,CommonJS 就不再只是“其中一种模块写法”。
它开始变成一个现实压力源。
因为任何后来者如果想统一 JavaScript 模块世界,都不能假装它不存在。
这也就是为什么今天你明明已经活在 ESM 时代,
却仍然不断撞见:
requiremodule.exportscjs- 双格式发布
因为 CommonJS 当年不是短暂占位。
它是真的把一大块世界先建起来了。
09|这也就是为什么下一篇必须谈 AMD:不是 CommonJS 失败了,而是浏览器现实根本没有答应按 Node 规则来
如果第二篇只停在“Node 这边已经有答案了”,那故事就断了。
因为真正的历史后续并不是:
既然 CommonJS 这么顺,那浏览器照抄就好了。
真正发生的是:
浏览器并没有照抄。
这不是因为浏览器社区不够聪明。
也不是因为他们没看到 CommonJS 的好处。
而是因为他们面对的问题不一样:
- 他们在乎异步加载
- 他们在乎网络请求成本
- 他们在乎页面阻塞
- 他们在乎直接在浏览器里把模块跑起来
所以从第二篇往第三篇走,最自然的过渡就是:
CommonJS 先证明了模块制度是可以成立的。
但它也同时证明了:
一个在 Node 世界顺得不得了的制度,未必能原封不动搬到浏览器里。
而这,正是 AMD / RequireJS 会长出来的根本原因。
10|先把模块制度化的,不是标准,而是 Node 现实
模块化江湖的第二篇,最值得记住的,不是“require() 比 import 更古老”这种表层事实。
更值得记住的是:
真正先把 JavaScript 模块化制度化的,不是浏览器,也不是标准委员会,而是 Node.js 和它所代表的服务器运行时现实。
CommonJS 最重要的历史贡献,也不只是给了大家一套导入导出写法。
更是:
它第一次让“每个文件都是模块、依赖应该被声明、接口应该被导出、运行时应当负责拼装这些关系”变成了大量开发者每天都能直接依赖的现实。
所以第二篇如果只记一句,就记这句:
CommonJS 先赢,不是因为它天然就是全世界的标准答案,而是因为它先在最适合自己的 Node 世界里,把模块化从聪明技巧做成了真正可执行的制度。
编者注(事实核对):文中关于 CommonJS 模块合同的描述,主要依据 CommonJS 规范 wiki 中 Modules/1.0、Modules/1.1 与 Modules/1.1.1 对 require、exports、module 的定义。关于 Node.js 对 CommonJS 的采用与运行方式,主要依据 Node 官方 Modules: CommonJS modules 文档,其中明确说明“each file is treated as a separate module”、exports 与 module.exports 的关系,以及 module wrapper 的存在。关于 Ryan Dahl 早期在 JSConf.eu 2009 中提到 Node “uses the CommonJS module system”的表述,主要依据演讲视频与配套幻灯片。正文将 CommonJS 概括为“先把 Node 世界治理成了世界”,属于基于其在服务器端、npm 生态与后续工具链中的历史影响做出的总结。
关键人物速览
- Ryan Dahl:
Node.js的发起者。第二篇里他代表的是“为什么服务器运行时会率先需要一套正式模块制度”的那条线。 - Kevin Dangoor:
CommonJS早期推动者之一。理解为什么这场秩序建设不是 Node 一家单打独斗,绕不开这条社区协作线。 - Kris Kowal:
CommonJS讨论与模块规范推进的重要人物之一。理解require/exports背后那套“模块合同”怎么逐步定型,绕不开他。
参考与延伸阅读
CommonJS Modules/1.0
https://wiki.commonjs.org/wiki/Modules/1.0CommonJS Modules/1.1
https://wiki.commonjs.org/wiki/Modules/1.1CommonJS Modules/1.1.1
http://wiki.commonjs.org/index.php?oldid=2934&title=Modules%2F1.1.1CommonJS Modules Overview
https://wiki.commonjs.org/wiki/ModulesNode.js Docs: CommonJS modules
https://nodejs.org/api/modules.htmlRyan Dahl: Original Node.js presentation
https://www.youtube.com/watch?v=ztspvPYybIYRyan Dahl JSConf 2009 slides
https://s3.amazonaws.com/four.livejournal/20091117/jsconf.pdf
下篇预告:AMD / RequireJS 为什么会在浏览器世界各自长出来。