JavaScript 缺乏一个最重要的模块机制,而 Node 通过模块规范,组织了自身的原生模块,弥补了 JS 若结构性的问题,形成了文档的结构。NPM 通过对包规范的支持,有效地组织了第三方模块,这时的项目开发中的依赖问题得到了很好的解决,并有效提供了分享和传播的平台。
前言

随着 Web 2.0 时代的到来,JavaScript 不再是以前的小脚本程序了,它在前端担任了更多的职责,也逐渐地被广泛运用在了更加复杂的应用开发的级别上。

但是 JavaScript 缺乏一个最重要的模块机制,虽然它能够通过 <script> 标签重新组织代码和结构,但是依旧没有解决变量污染的问题,标签只作用于代码层面。在其他高级语言中,Java 有类文件,Python 有 import 机制,Ruby 有 require,PHP 有 include 和 require。所以 JavaScript 开发人员不得不用命名空间等方式人为地约束代码,已达到安全和易用的目的。

变量污染

在文件形式上这些不同文件的 js 以不同的身份存在着的,但是一旦引入到同一个 index.html 文件之后,如果其中两个 js 文件中声明了同名变量 c ,由于 <script> 标签并不会为每个 js 文件单独划分私有作用域,它们被全部组织到一个作用域内,(可以把 index.html 看作一个作用域)。所以,在运行期间,浏览器将抛出 SyntaxError 错误,即变量 c 已被声明。

a.js 文件:

// a.js
let c = 10

b.js 文件:

// b.js
let c = 11

在 index.html 中引入 js 文件:

<script src="../../../static/js/chap06/a.js"></script>
<script src="../../../static/js/chap06/b.js"></script>

两个变量 c 声明冲突,所以抛出异常:

[Node]模块机制_作用域

经过数年的发展后,Mozilla 工程师 Kevin Dangoor 在 2009年8 月,发布了 CommonJS 规范,它的出现是 JavaScript 发展的重要的里程碑。

CommonJS 规范

JavaScript 没有模块机制,缺乏包管理系统,ECMAScript 仅定义了部分核心库,对于文件系统,I/O流等常见需求却没有标准的 API ,这导致 JavaScript 的标准库缺少。

Java 开发人员可以从 Maven 仓库、或其他平台下载第三方 jar 包,引入到自己的项目中进行开发。这得益于良好的模块机制和包管理系统,使得 Java 的生态规模逐渐壮大起来。

[Node]模块机制_javascript_02

CommonJS 规范的提出,主要是为了弥补当前 JavaScript 没有标准的缺陷,以达到像 Pythone、Java 和 Ruby 具备开发大型应用的基础能力,而不是停留在小脚本程序的阶段。Node 借鉴 CommonJS 的 Modules 规范实现了一套非常易用的模块系统, NPM 对 Packages 规范的完好支持使得 Node 应用在开发过程中事半功倍。

CommonJS 的模块规范

模块的意义在于将方法和变量等限定在私有作用域中,同时支持引入和导出功能以顺畅地连接上下游的依赖。

众所周知,长江有许许多多的支流,支流为长江源源不断地注入流量,支流与主流顺畅地连接,上游与下游之间有机地组织者。所以,从长江任一一条支流出发,都可以进入到主流中,并达到最终的目的地。

[Node]模块机制_作用域_03

exports

exports 用于导出当前模块的方法或变量,并且它是唯一的导出出口。一个 module 对象就代表模块自身,而一个 js 文件可以看作是一个模块。

exports 是 module 的属性,将方法或变量挂载到 exports 对象上作为属性即可定义导出的方式。

这里定义一个 math.js :

// 导出一个函数
exports.add = function() {
    let sum = 0, i = 0, args = arguments, l = args.length
    while (i < l) {
        sum += args[i++]  
    }
    return sum
}

在其他模块中(js 文件)中,通过 require() 方法引入 math 模块,然后就可以调用属性或方法了。

require()

上文提到,一个 js 文件可以看作是一个模块。模块引入其他模块使用 require() 方法,以此引入一个模块的 API 到当前上下文中。

require() 函数相当于在长江的一边开了一条支流,将支流中的流量引入到主流中使用。

let math = require('math')

在 Java 语言中,一个类文件用 import 关键字引入另一个类文件。

import java.util.Date;
NPM

Node 组织了自身的核心模块,也使得第三方文件模块可以有序地编写和使用。第三方模块中,模块与模块之间任然是散列在各地的,相互之间不能直接引用。但是,包和NPM则是将模块联系起来的一种机制。包是在模块的基础上进一步组织 JavaScript 代码。

[Node]模块机制_java_04

包实际上是一个存档文件,即一个目录直接打包为 .zip 或 tar.gz 格式的文件。完全符合 CommonJS 规范的包目录应该包含如下这些文件:

  1. package.json:包描述文件。
  2. bin:用于存放可执行的二进制文件的目录。
  3. lib:用于存放 JavaScript 代码的目录。
  4. doc:用于存放文档的目录。
  5. test:用于存放单元测试用例的代码。

包描述文件与 NPM

包描述文件,即 package.json 用于表达非代码相关的信息,位于包的根目录下,是包的重要组成部分。而 NPM 的所有行为斗鱼宝描述文件的字段息息相关。包的规范定义可以帮助 Node 解决依赖包安装的问题,而 NPM 正是基于该规范进行了实现。

NPM 的意义

CommonJS 包规范是理论,而 NPM 是它的一种实践。NPM 相当于 gem 之于 Ruby,Maven 之于 Java。对于 Node 而言,NPM 帮助完成了第三方模块的发布、安装和依赖等。借助 NPM,Node与第三方模块之间形成了很好的一个生态系统。

总结

CommonJS 提出的规范均十分简单,但是现实意义却十分强大。Node 通过模块规范,组织了自身的原生模块,弥补了 JavaScript 若结构性的问题,形成了文档的结构。NPM 通过对包规范的支持,有效地组织了第三方模块,这时的项目开发中的依赖问题得到了很好的解决,并有效提供了分享和传播的平台。