NodeJs的CommonJS模块规范

前言

本人记忆力一般,为了让自己理解《深入浅出Node.js-朴灵》一书,会在博客里记录一些关键知识,以后忘了也可以在这里找到,快速回想起来

Node通过require、exports、module实现CommonJS模块规范的

路径分析

require('http') //如http、fs、path,速度仅次于缓存加载,它在node源代码编译过程中已经被编译成二进制代码,其加载速度最快

require('./a.txt') //以.或者..开始的相对路径模块

require('/a.txt') //以/开始的绝对路径模块
//以上两种都当做文件模块来处理,在分析路径模块时require()方法会将路径转化为真实路径,并以真实路径为索引,将编译执行后的结果放到缓存中,以使二次加载更快。
//因为路径模块给了确切文件位置,所以在查找过程中可以节约大量时间,其加载慢于核心模块。

require(*) //非路径形式的文件模块,如自定义的connect模块//自己没太理解这块,因为可能没实现过自己的自定义模块所以举不出例子
//特殊的文件模块,可能是一个文件或者包的形式。这类模块查找最费时,也是所有方式中最慢的。原因是和js原型链一样要一层层node_modules找

文件定位

从缓存加载的优化策略使得二次引入不需要分析路径分析、文件定位和编译执行的过程,大大提高了再次加载时的效率

  • 文件扩展名分析

require() 允许参数不带后缀,在这种情况下,node会按照.js、.json、.node次序补足扩展名依次尝试,在尝试过程中需要调用fs模块同步阻塞式判断文件是否存在。

所以在后两种引入方式时推荐加上后缀名

  • 目录分析和包

require() 通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录,这在引入自定义模块和逐个模块路径进行查找时经常会出现,此时node会将目录当做一个包来处理。

在这个过程中,node对commonjs包规范进行了一定程度的支持。首先,node在当前目录下查找package.json,通过json.parse解析出包描述对象从中取出Main属性指定的文件名进行定位。

如果文件缺少扩展名,将会进入扩展名分析的步骤。

而如果main属性指定的文件名错误或者压根没有package.json文件,node将会将index当做默认文件名,依次添加扩展名查找

如果在目录分析的过程中没有定位成功任务文件则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败异常。

编译执行

在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会新建一个模块对象,然后根据路径载入并编译。对于不同扩展名,其载入方式不一样如下:

  • .js 通过fs模块同步读取文件后编译执行

  • .node 这是通过c/c++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件

  • .json 通过fs模块同步读取后,用JSON.parse解析返回结果

// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
 var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
 try {
 module.exports = JSON.parse(stripBOM(content)); 
 } catch (err) {
 err.message = filename + ': ' + err.message;
 throw err;
 }
}; 
  • 其余拓展名文件 都被当做js文件载入

每一个编译成功的模块都会将其文件路径作为索引缓存在Modules._cache对象上,以提高二次加载速度

实践

我们有个area.js文件看看node的commonjs规范流程

  • 暴露文件给node并会给下面js包装
 var math = require('math');
 exports.area = function (radius) {
 return Math.PI * radius * radius;
 }; 

变为

(function (exports, require, module, __filename, __dirname) { 
 var math = require('math');
 exports.area = function (radius) {
 return Math.PI * radius * radius;
 };
}); 

这样每个模块文件之间都进行了作用域隔离。包装后的代码会通过vm原生模块的runInThisContext方法执行(类似eval,只是有明确的上下文,不污染全局),返回一个具体function对象。

最后将当前模块对象的exports属性、require方法、module(模块自身)以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function执行,执行后模块的exports属性返回给了调用方,

其他变量方法无法被调用。

  • 外部文件引用
require('./area');

node通过上述的(路径分析)来查这个'./area',通过fs找到area.js文件(文件定位)并读取编译执行(编译执行)

AMD与它的区别

AMD需要用define明确定义一个模块,而在node实现中是隐形包装的,目的是作用于隔离,仅在需要时被引入,避免掉过去那种全局变量或者命名空间的方式,防止被污染,另一个区别是内容需要通过返回的方式实现导出。

define(id?, dependencies?, factory);

define(function() {
var exports = {}; exports.sayHello = function() {
alert('Hello from module: ' + module.id); };
return exports; });

CMD与它的区别

AMD需要在声明模块的时候定义所有依赖,通过形参传递到模块内容中,CMD支持动态引入依赖,require、exports、module通过形参传递给模块,在需要依赖模块时,随时调用require引入即可

define(['dep1', 'dep2'], function (dep1, dep2) { return function () {};
});
define(function(require, exports, module) {
 });