一、CommonJS的模块规范
Node与浏览器以及 W3C组织、CommonJS组织、ECMAScript之间的关系
Node借鉴CommonJS的Modules规范实现了一套模块系统,所以先来看看CommonJS的模块规范。
CommonJS对模块的定义十分简单,主要分为模块引用、模块定义和模块标识3个部分。
1. 模块引用
模块引用的示例代码如下:
var math = require('math');
在CommonJS规范中,存在require()方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中。
2. 模块定义
在模块中,上下文提供require()方法来引入外部模块。对应引入的功能,上下文提供了exports对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口。在模块中,还存在一个module对象,它代表模块自身,而exports是module的属性。在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式:
// 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。模块的定义十分简单,接口也十分简洁。它的意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地连接上下游依赖。每个模块具有独立的空间,它们互不干扰,在引用时也显得干净利落。
二、Node的模块实现
Node在实现中并非完全按照规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性。尽管规范中exports、require和module听起来十分简单,但是Node在实现它们的过程中究竟经历了什么,这个过程需要知晓。
在Node中引入模块,需要经历如下3个步骤。
1. 路径分析
2. 文件定位
3. 编译执行
在Node中,模块分为两类:一类是Node提供的模块,称为核心模块;另一类是用户编写的模块,称为文件模块。
• 核心模块部分在Node源代码的编译过程中,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以它的加载速度是最快的。
• 文件模块则是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。
1.优先从缓存加载
与前端浏览器会缓存静态脚本文件以提高性能一样,Node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同的地方在于,浏览器仅仅缓存文件,而Node缓存的是编译和执行之后的对象。不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。不同之处在于核心模块的缓存检查先于文件模块的缓存检查。
2.路径分析和文件定位
因为标识符有几种形式,对于不同的标识符,模块的查找和定位有不同程度上的差异。
1). 模块标识符分析
Node基于一个模块标识符进行模块查找。模块标识符在Node中主要分为以下几类。
核心模块,如http、fs、path等。
.或..开始的相对路径文件模块。
以/开始的绝对路径文件模块。
非路径形式的文件模块,如自定义的connect模块。
• 核心模块
核心模块的优先级仅次于缓存加载,它在Node的源代码编译过程中已经编译为二进制代码,其加载过程最快。如果试图加载一个与核心模块标识符相同的自定义模块,那是不会成功的。如果自己编写了一个http用户模块,想要加载成功,必须选择一个不同的标识符或者换用路径的方式。
• 路径形式的文件模块
以.、..和/开始的标识符,这里都被当做文件模块来处理。在分析路径模块时,require()方法会将路径转为真实路径,并以真实路径作为索引,将编译执行后的结果存放到缓存中,以使二次加载时更快。由于文件模块给Node指明了确切的文件位置,所以在查找过程中可以节约大量时间,其加载速度慢于核心模块。
• 自定义模块