CommonJS 规范
CommonJS 提出的规范十分简单, 但是现实意义却十分强大。Node通过模块规范,组织了自身的原生模块,弥补JavaScript弱结构性的问题,形成了稳定的结构,并向外提供服务。
JavaScript 的发展历程,主要在浏览器前端发光发热。
- Web1.0时代,只对DOM、BOM等基本的支持
- Web2.0时代,HTML5将Web带进Web应用的时代,在浏览器中出现了强大的API供JavaScript调用
这些过程主要发生在前端,后端的JavaScript规范却远远落后。对于JavaScript而言,它的规范仍然是薄弱的,还有以下缺点:
没有模块系统
- ES5 标准库较少
- CMAScript 仅定义了部分核心库,对于文件系统,I/O流等常见的需求却没有标准的API。 没有标准接口
- 几乎没有定义过如Web服务器或数据库之类的标准统一接口 缺乏包管理工具
- 导致JavaScript应用中基本没有自动加载和安装以来的能力
CommonJS 规范涵盖了模块、二进制、Buffer、字符集编码、I/O流、进程环境、文件系统、套接字、单元测试、Web服务器网关接口、包管理等
CommonJS 的模块规范
CommonJS 对模块的定义:
- 模块引用
- 模块定义
- 模块标识
模块引用
var math = require('math')
require() 方法接受一个模块标识,来引入一个模块到当给钱上下文中。
模块定义
- 在Node中,一个文件就是一个模块。
- 在模块中,module 对象代表模块自身, exports 是module的属性。
- 将方法挂载到 exports 对象上作为属性即可定义导出的方式:
exports.add = function() {
var sun = 0, i=0;
args = arguments, l = args.length;
while(i<l) {
sum += args[i++];
}
return sum;
}
模块标识
- 就是传递给require()方法的参数
- 必须符合小驼峰命名法的字符串
- 或者以.或..开头的相对路径或者绝对路径
- 可以没有文件后缀名
CommonJS 构建的模块导出和导入机制使得用户完全不必考虑变量污染
Node 的模块实现
在Node中引入模块, 需要经过3个步骤:
- 路径分析
- 文件定位
- 编译执行
Node模块分为两类:
-
核心模块
-
核心模块部分在Node源代码的编译过程中,编译进了二进制执行文件
-
部分核心模块被就直接加载进内存中,所以在核心模块引入时,文件定位和编译执行步骤被省略
-
在路径分析中优先判断,所以它的加载速度是最快的
-
-
文件模块
- 在运行时动态加载,需要完成的路径分析、文件定位、编译执行过程,速度比核心模块慢
路径分析
1.模块标识符分析
- 模块标识符在Node中主要分为:
- 核心模块
- 核心模块的优先级仅次于缓存加载
- 自定义模块名称不能与核心模块重名,否则是不会被加载的
- .或..开始的相对经文件模块
- require() 方法会将路径转为真实路径
- 以真实路径为索引,将编译执行的结果存放到缓存
- 以 / 开始的绝对路径文件模块
- 以真实路径为索引,将编译执行的结果存放到缓存
- 非路径形式的文件模块,如自定义的connect 模块
- 一种特殊的文件模块
- 可能是一种特殊的文件模块,可能是一个文件或者包的形式
- 是所有方式中最慢的
- 核心模块
模块路径是Node在定位文件模块的具体文件时指定的查找策略,具体表现为一个路径组成的数组
模块路径的生成规则:
- 当前文件目录下的node_modules目录
- 父目录下的mode_modules目录
- 父目录的父目录下的node_modules目录
- 沿着路径向上逐级递归,直到根目录下的node_modules目录
2.文件定位
- 文件扩展名分析
- CommonJS模块规范允许在标识符中不包含文件扩展名
- Node会按.js、.json、.node的次序补足扩展名,依此尝试
- 尝试的过程,会调用fs模块同步堵塞的判断文件是否存在
【建议】如果是.json或.node文件,在传递给require()的标识符中带上扩展名,会加快一些速度。
- 目录分析和包
- require() 通过分析文件扩展名后,可能没有找到对应的文件,但却得到一个目录,Node会将目录当做一个包来处理。
- 首先:Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件),通过 JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。 如果文件名缺少扩展名,将会进入扩展名步骤
- 如果main属性指定的文件名犯错误,或严格没有package.json文件,Node会将index当做默认文件名,然后依次查找index.js、index.json、index.node
- 如果分析的过程中没有定位成功任何文件,则自定义模块进入到下一个模块模型进行查找,如果模块路径数组都被遍历完毕,仍然没有找到目标文件,则会抛出查找失败的异常
模块编译
定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,载入方法不同:
- JS 文件, 通过FS模块同步读取文件后编译执行
- node 文件,是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件。
- json 文件,通过fs模块同步读取文件后,用JSON.parse()解析返回结果
- 其余扩展名文件,都被当做.js文件载入
自定义扩展名加载,require.extensions['.ext'], v0.10.6版本后不推荐使用,而是期望通过其他语言或文件编译成JavaScript文件后在加载
Node 对获取的JavaScript文件内容进行收尾包装。 头部添加: (function(exports,require, module,__filename, __dirname) { 尾部添加: })
头尾包装后,每个模块文件之间都进行了作用域隔离。
优先从缓存加载
Node 缓存的是编译和执行之后的对象
不论是核心模块还是文件模块,require() 方法对同一个模块的二次加载都一律采用缓存优先的方式。