CommonJS 和 Node.js

从诞生之初起,JavaScript 也会部署在服务端平台上,提供基本的脚本功能。然而每个平台都有所不同,提供了自己特有的 JavaScript API。在 JavaScript 诞生的前 15 年里,并没有一个通用的、领域独立的、可互操作的非浏览器 JavaScript 应用环境。2009 年 1 月,曾在 Adobe 和 Mozilla 工作过的 Khan Academy 开发者 Kevin Dangoor 决定改变这种状况。他写了一篇博客文章 [Dangoor 2009] 描述了这些问题,并邀请服务端 JavaScript 社区通过在线讨论组和 Wiki 参与到解决问题中来。一年后,他在一篇后续的博文 [Dangoor 2010] 中,将自己最初希望创造的东西总结为如下:

  • 一个模块系统(module system)
  • 一个跨解释器的标准库
  • 若干个标准接口
  • 一个包系统(package system)
  • 一个包仓库(package repository)

在一周内,有 224 名成员加入了讨论组 [Kowal 2009a],其中许多人表示有兴趣为这个项目做出贡献。这个提议最初被称为 ServerJS,但在 2009 年 8 月更名为 CommonJSg,因为这一技术的适用性已经超出了服务端的范畴。提议的重点在于编写规范,而非实现。

到 2009 年 4 月,小组获得了一份初步的模块规范 [CommonJS Project 2009]。这个 CommonJS 模块规范主要基于 Kris Kowal 和 Ihab Awad 的设计 [2009a]。一个 CommonJS 模块就是一个 JavaScript 函数体,其作用域内包括了多个变量绑定,这些绑定使得函数体内的代码能与其他模块进行交互。这一能力是由一个同步模块加载器实现的。模块加载器会获取模块的源码,用一个骨架函数定义包裹它们,接着解析并调用合成函数(synthesized function)来初始化该模块,并初始化它到其他模块的连接。如图 28 所示,模块级的作用域声明会成为合成函数的局部变量。模块系统的控制钩子则作为函数参数暴露出来,其值由加载器提供。require 参数是一个函数,它同步地对所请求的模块执行上下文加载过程,并返回其 exports 的值78。默认情况下,exports 的值是一个由加载器提供的对象。在 CommonJS 中,从模块的名称,到实际的 exports 值,再到模块所导出属性的名称和值,都可以被动态生成。这使得想要预先获知程序「需要哪些模块,以及有哪些实体在这些模块之间共享」变得困难,有时这甚至是无法实现的。

  1. // moda.js - 源码
  2. var modp = require("modp");
  3. exports.n = modp.p++;
  4. exports.modName = "prefix" + exports.n;
  5. // modb.js - 源码
  6. var modx = require(require("moda").modName);
  7. var propName = Object.keys(modx)[0];
  8. exports[propName] = modx[propName];
  9. // moda.js - CJS 展开后
  10. (function (exports, require, module) {
  11. var modp = require("modp");
  12. exports.n = modp.p++;
  13. exports.modName = "prefix" + exports.n;
  14. });
  15. // modb.js - CJS 展开后
  16. (function (exports, require, module) {
  17. var modx = require(require("moda").modName);
  18. var propName = Object.keys(modx)[0];
  19. exports[propName] = modx[propName];
  20. });

图 28. CommonJS 模块被模块加载器转换成了「实现模块模式的函数」。模块之间的共享,是通过动态构造出的 exports 对象上的属性来实现的。

CommonJS 模块的早期使用者之一,就是 2009 年初由 Ryan Dahl 开发的 Node.jsg。在其设想中,Node.js 是个用于通过 JavaScript 构建服务端应用的开源平台,其能力足以处理大量的客户端同时连接。Node.js 支持一种异步的 I/O 模型,并为此提供了一个带有库的 JavaScript 编程环境。它连接起了常见的 POSIX 接口、JavaScript 回调,以及简化的浏览器事件循环,其整体实现主要包含了谷歌的 V8 JavaScript 引擎、一个 CommonJS 模块加载器,以及一组 C 语言实现的模块。这些模块提供了许多平台接口的非阻塞版本,包括 POSIX API 和其他高层面的文件和网络操作。Node.js 的首个公开版本是在 2009 年 5 月发布的 [Node Project 2009]。但直到 2009 年 11 月 Dahl [2009] 在 jsconf.eu 上做了一次演讲后,它才引起了人们的重视。此后不久,Dahl 被 Joyent 雇用。Joyent 负责管理和支持 Node.js 的进一步开发,直到 2015 年将其交接给 Node.js 基金会为止 [Node Foundation 2018]。

Node.js 最早被设想为一种用于构建服务端应用的技术。但它已经成为了一个平台,使 JavaScript 能作为通用编程语言,应用在包括小型嵌入式设备在内的各种平台上。Node.js 的 I/O 模块与高性能的 V8 引擎相结合,在能力上足以与 Python 和 Ruby 等其他动态应用语言相媲美,在性能上也往往更胜一筹,成为了编写命令行 JavaScript 应用时的事实标准。Node.js 使掌握了 JavaScript 的 Web 程序员能将其技能转移到其他类型的应用和非浏览器环境中。最初许多客户端 Web 应用的开发者们之所以使用 JavaScript 编程,是因为他们别无选择。而许多 Node.js 开发者选择使用它,反而是因为他们更喜欢用 JavaScript 编程。