JavaScript从诞生以来,就缺乏一项功能:模块。不像其他高级语言,Java有类,python有import机制,JS只能通过script标签来引入代码,显得杂乱无章。
在Node出现之前,服务器端JS基本没有市场。但是经历十多年的发展后,社区也为JS制定了相应的规范,其中CommonJS规范的提出算是最为重要的里程碑。
CommonJS规范
CommonJS规范为JS制定了一个美好的愿景:希望JS能够在任何地方运行——CommonJS规范的提出,主要是为了弥补当前JS没有标准的缺陷,以达到像Python Java具备开发大型应用的基础能力,而不是停留在脚本程序的阶段。他们希望那些用CommonJS API写出的应用可以具备跨宿主环境执行的能力。这样不仅可以利用JS开发客户端应用,还可以编写:
1.服务器端JS程序
2.命令行工具
3.桌面图形界面应用程序
…
上图是Node,W3C,CommonJS,ECMAScript之间的关系。
Node借鉴CommonJS的Module规范实现了一套非常易用的模块系统。NPM对Packages规范的完好支持使得Node应用在开发过程中事半功倍。
CommonJS的模块规范
CommonJS对模块的定义时分简单,主要分为模块引用,模块定义和模块标识三个部分。
1、模块引用
var math = require('math');
2、模块定义
在Node中,一个文件就是一个模块,将方法或者变量挂载在export对象上作为属性即可定义到出的方式。export是module的属性。
// math.js
exports.add = function () {
var sum=0, i=0,
args = arguments,
l = args.length;
while(i<l){
sum += args[i++];
}
return sum;
};
在另一个文件中,通过require方法引入模块后,就可以定义属性或方法了:
// program.js
var math = require('math');
exports.increment = function (val) {
return math.add(val, 1);
};
3、模块标识
模块标识其事就是传递给require方法的参数,它必须是符合小驼峰命名的字符串,或者以. 、.. 开头的相对路径或者绝对路径。可以没有文件名后缀.js。
CommonJS的这套模块导出和引入机制,使得用户完全不必考虑变量污染。
Node的模块实现
Node的模块分为两类,一类是Node提供的模块,称为核心模块;一类是用户编写的模块,称为文件模块。
核心模块部分在Node源码的编译过程中,编译进了二进制执行文件,在Node进程启动时,部分核心模块就被直接加载进内存中。
而文件模块则是在运行时动态加载。它需要完成完整的如下三个步骤:
在Node中引入模块,需要经历的三个步骤:
1、路径分析(Node在路径分析时,将会优先判断核心模块)
2、文件定位
3、编译执行
模块加载详述:
1、优先从缓存加载
Node和前端浏览器会缓存静态脚本文件一样, 也会对引入的模块进行缓存。并且,Node缓存的是变异和执行后的对象,而不是像浏览器那样仅缓存文件。
不论是核心还是文件模块,require方法对相同模块的二次加载都一律次啊用缓存优先的方式。核心模块的缓存检查会先于文件模块的缓存检查。
2、路径分析和文件定位
3、模块编译
在Node中,每个文件模块都是一个对象。它的定义如下:
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if (parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
编译和执行是引入文件模块的最后一个阶段。定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。编译成功的模块会想起文件路径作为索引缓存在Module._cache对象上,以提高二次引入的特性。
模块调用栈
各种模块之间的调用关系:
包与NPM
在模块之外,包和NPM则是将模块联系起来的一种机制。包的出现,在模块的基础上进一步组织了JS代码。
在介绍NPM之前,首先介绍Common JS的包规范。
CommonJS的包规范定义 由包结构和包描述文件两个部分组成,前者用于组织包中的各种文件,后者则用于描述包的相关信息。
1、包结构
包实际上是一个存档文件,即一个目录直接打包为.zip格式的文件,安装后解压还原为目录。
完全符合CommonJS规范的包目录应该包含如下文件:
1、package.json:包描述文件
2、bin:用于存放可执行二进制文件的目录
3、lib:用于存放JS代码的目录
4、doc:用于存放文档的目录
5、test:用于存放单元测试用例的代码
2、包描述文件与NPM
package.json即包描述文件,它是包的重要组成部分,NPM的所有行为都与包描述文件的字段息息相关。
CommonJS为package.json文件定义了一些必需字段如name, description, version等,还有一些可选字段如homepage,os等。
包规范的定义可以帮助Node解决依赖包安装的问题,而NPM正是基于该规范进行了实现。
3、NPM常用功能
CommonJS包规范是理论,而NPM是其中一种实践。对于Node而言,NPM帮助完成了第三方模块的发布、安装和依赖等。借助NPM,Node与第三方模块之间形成了很好的一个生态系统。
借助NPM,可以帮助用户快速安装和管理依赖包。
ADM和CDM
ADM和CDM规范都应用于前端场景。
ADM规范是CommonJS模块规范的一个延伸。它的模块定义如下:
define(id?, dependencies?, factory);
ADM模块需要用define来明确定义一个模块,而在Node实现中是隐式包装的。它们的目的是进行作用域隔离。
ADM需要在生命模块的时候制定所有的依赖,并通过形参传递依赖到模块中。
define(['dep1', 'dep2'], function (dep1, dep2) {
return function () {};
});
与ADM相比,CDM模块更接近于Node对CommonJS规范的定义:
define(factory);
在依赖部分,CDM支持动态引入:
define(function(require, exports, module) {
// The module code goes here
});
require, exports, module通过形参传递给模块,在需要依赖模块时,随时调用require引入即可。