01|如果说第一篇讲的是“旧办法为什么撑不住了”,那第二篇要讲的就是:第一套真制度为什么先长在 Node 里

第一篇讲到,JavaScript 在没有正式模块制度的时候,并不是完全不会组织代码。

它有过很多自救办法:

  • 命名空间
  • 对象字面量
  • 闭包
  • IIFE
  • module pattern

这些办法都很聪明。

可它们共同的上限也很明显:

它们能改善隔离,却不能真正制度化依赖管理。

你还是得自己安排加载顺序。

你还是得自己记住谁依赖谁。

你还是得在没有统一规则的情况下,把一堆文件勉强拼成一个可运行整体。

所以模块化江湖到了第二篇,就必须进入一个新的历史阶段:

第一套真正可工作的、能让大量项目直接照着写的模块制度,到底是怎么出现的?

答案不是浏览器先给的。

而是 Node.js 世界里的 CommonJS

这点很关键。

因为它说明 JavaScript 模块化最早的突破,不是语言标准先觉醒。

也不是浏览器厂商先给了解法。

而是:

服务器侧运行时先碰到了更痛的现实,于是它率先把“模块”这件事做成了真正的日常制度。


02|CommonJS 之所以会先赢,不是因为它最完美,而是因为服务器世界比浏览器更急着要一套秩序

如果只看今天的结果,很容易误以为 CommonJS 是因为设计特别高明,才会先流行起来。

其实不完全是。

它当然有自己的清晰之处:

  • require()
  • exports
  • module

这些东西都很直接。

可它最早真正占到便宜的地方,并不是“抽象更美”。

而是:

服务器和本地运行时环境,比浏览器更早进入“没有模块制度就真的很难继续写”的阶段。

为什么?

因为浏览器那边虽然也乱,但早期很多脚本还勉强能靠页面顺序和小规模代码撑着。

而服务端和命令行环境一旦开始认真写程序,需求会立刻变得更硬:

  • 文件要拆分
  • 功能要复用
  • 库要引用
  • 代码要长期维护

这时候,如果还停留在“大家尽量别重名”的阶段,就已经不够了。

所以 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.jsCommonJS 机制有一个非常强的后果:

每个文件默认就是一个独立模块。

Node 官方文档后来写得很清楚:

  • each file is treated as a separate module
  • exportsmodule.exports 的简写
  • 模块代码会先被 wrapper function 包起来再执行

这意味着什么?

意味着以前第一篇里那些靠 IIFE、闭包辛苦换来的边界感,在这里突然变成了默认配置。

你不必先写一层自执行函数才获得私有作用域。

运行时直接就帮你做了这件事。

这其实是非常大的心理跃迁。

因为它让 JavaScript 开发者第一次不必再把“避免泄漏到全局”当成额外技巧。

从这里开始,反而是全局暴露才成了例外。

而模块边界成了默认。

这就是制度化和技巧化最大的差别。

技巧是你得主动维持。

制度是环境帮你维持。


06|所以 CommonJS 在 Node 世界真正解决的,不只是导入导出,而是把依赖、边界、缓存和入口这些事一起纳入了秩序

这也是为什么第二篇不能只写成“require / exports 教程”。

那太轻了。

CommonJSNode.js 里真正厉害的地方,是它一次性把几件之前一直散着的问题,一起纳入了同一套秩序:

  • 模块边界:每个文件就是一个模块
  • 依赖声明:通过 require() 明确写出
  • 导出接口:通过 exports / module.exports
  • 运行缓存:第一次加载后缓存,后续复用
  • 主模块识别:require.main / module

这很关键。

因为现代模块制度真正有力量,不是某条语句长得漂亮。

而是它把“程序如何由多个单元拼起来”这件事,变成了可预测的公共规则。

在第一篇里,很多事还是靠人记。

到了 CommonJS 这里,很多事第一次开始能靠环境保证。

这才叫秩序。

所以 CommonJS 的历史意义,不只是它让 JavaScript 可以拆文件。

更是:

它第一次让 JavaScript 世界相信,模块可以不是写作风格,而是运行时承诺。


07|但也正因为它太贴 Node 世界,它从一开始就埋下了一个问题:这套秩序并不天然属于浏览器

这件事一定不能漏。

否则第二篇就会把 CommonJS 写成“正确答案本来就已经出现,只是后来大家绕远路”。

事实不是这样。

CommonJSNode.js 里很顺,

可它顺的前提恰恰也是它的边界:

它默认的是服务器 / 本地运行时现实,而不是浏览器现实。

浏览器世界和 Node 世界最大的不一样在于:

  • 浏览器模块加载常常意味着网络请求
  • 页面初始化对加载顺序和首屏更敏感
  • 同步装载在浏览器里代价很高
  • 用户面对的是下载和阻塞,而不只是本地读文件

所以 require() 这套在 Node 里很自然的直觉,到了浏览器就会开始别扭。

这里最重要的不是技术细节,而是历史判断:

CommonJS 先赢,证明的是它先找到了自己的主场;并不等于它天生就是全 JavaScript 世界的终极答案。

这一点,后面 AMD 为什么会出现,就完全建立在这里。

不是大家故意分裂。

而是浏览器现实根本不会自动服从 Node 那套假设。


08|为什么 CommonJS 不是先统一世界,而是先把 Node 世界变成了世界

把前面这些线叠一起看,第二篇最重要的判断可以压成一句:

CommonJS 不是先统一了天下,它是先把 Node 这个世界治理成了世界。

这句话什么意思?

就是说,它最先赢下的,不是所有运行时。

它最先赢下的是一个足够重要、足够会扩张、后来足够影响工具链和生态的主场。

一旦 Node.js 起势,后果就会非常大:

  • 大量服务端代码开始默认采用 CommonJS
  • npm 生态围着这套模块制度长
  • 后来的构建工具、CLI、脚本体系也被它深度影响

这时,CommonJS 就不再只是“其中一种模块写法”。

它开始变成一个现实压力源。

因为任何后来者如果想统一 JavaScript 模块世界,都不能假装它不存在。

这也就是为什么今天你明明已经活在 ESM 时代,

却仍然不断撞见:

  • require
  • module.exports
  • cjs
  • 双格式发布

因为 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.0Modules/1.1Modules/1.1.1requireexportsmodule 的定义。关于 Node.jsCommonJS 的采用与运行方式,主要依据 Node 官方 Modules: CommonJS modules 文档,其中明确说明“each file is treated as a separate module”、exportsmodule.exports 的关系,以及 module wrapper 的存在。关于 Ryan Dahl 早期在 JSConf.eu 2009 中提到 Node “uses the CommonJS module system”的表述,主要依据演讲视频与配套幻灯片。正文将 CommonJS 概括为“先把 Node 世界治理成了世界”,属于基于其在服务器端、npm 生态与后续工具链中的历史影响做出的总结。


关键人物速览

  • Ryan DahlNode.js 的发起者。第二篇里他代表的是“为什么服务器运行时会率先需要一套正式模块制度”的那条线。
  • Kevin DangoorCommonJS 早期推动者之一。理解为什么这场秩序建设不是 Node 一家单打独斗,绕不开这条社区协作线。
  • Kris KowalCommonJS 讨论与模块规范推进的重要人物之一。理解 require / exports 背后那套“模块合同”怎么逐步定型,绕不开他。

参考与延伸阅读

  1. CommonJS Modules/1.0
    https://wiki.commonjs.org/wiki/Modules/1.0

  2. CommonJS Modules/1.1
    https://wiki.commonjs.org/wiki/Modules/1.1

  3. CommonJS Modules/1.1.1
    http://wiki.commonjs.org/index.php?oldid=2934&title=Modules%2F1.1.1

  4. CommonJS Modules Overview
    https://wiki.commonjs.org/wiki/Modules

  5. Node.js Docs: CommonJS modules
    https://nodejs.org/api/modules.html

  6. Ryan Dahl: Original Node.js presentation
    https://www.youtube.com/watch?v=ztspvPYybIY

  7. Ryan Dahl JSConf 2009 slides
    https://s3.amazonaws.com/four.livejournal/20091117/jsconf.pdf


下篇预告:AMD / RequireJS 为什么会在浏览器世界各自长出来。