CommonJS规范

吐槽君 分类:javascript

概述

Node.js是基于CommonJS模块规范的实现,每个文件就是一个模块,有自己的作用域,模块内代码的写法必须遵守CommonJS规范。文件里定义的变量、函数、类,都是私有的,其他文件不可见。

如果想在多个文件分享变量,必须定义为global对象的属性。

global.x = 5;
 

这样变量x就可以被所有文件读取。但是,并不推荐这样写。

CommonJS模块的特点:

  • 所有代码都运行在模块作用域,不会污染全局;

  • 模块可多次加载,但只在第一次加载时运行一次,运行结果被缓存,以后加载直接读取缓存结果。再次运行,必须清除缓存;

  • 模块加载顺序,按照在代码中的出现顺序。

module对象

Node内部提供一个Module构建函数,所有模块都是它的实例。

function Module(id, parent) {
	this.id = id;
    this.exports = {};
    this.parent = parent;
    // ...
}
 

CommonJS规范规定,每个模块内部都有一个module对象,代表当前模块。它的exports属性是对外的接口。加载一个模块,其实是加载该模块的module.exports属性。

var x = 5;
var add = function (val) {
	return val + x;
};
module.exports.x = x;
module.exports.add = add;
 

上面代码通过module.exports输出变量x和函数add。

require方法用于加载模块。

var example = require('./example.js');

console.log(example.x);
console.log(example.add);
 

module对象的属性:

module.id 模块的识别符,通常是带有绝对路径的模块文件名;

module.filename 模块的文件名,带有绝对路径;

module.loaded 返回一个布尔值,表示模块是否完成加载;

module.parent 返回一个对象,表示调用该模块的模块;

module.children 返回一个数组,表示该模块要用到的其他模块;

module.exports 表示模块对外输出的值。

在命令行下调用某个模块,比如:node something.js,那么module.parent就是null。如果在脚本中调用,比如:require('./something.js'),那么,module.parent就是调用它的模块。利用这点,可以判断当前模块是否为入口脚本。

if(!module.parent) {
	app.listen(8088, function() {
    	console.log('app listening on port 8088');
    })
} else {
	module.exports = app;
}
 

module.exports属性

module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上是读取module.exports变量。

var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();

setTimeout(function() {
	module.exports.emit('ready');
}, 1000);
 

上面模块会在加载1秒后,发出ready事件。其他文件监听该事件,如下:

var a = require('./a');
a.on('ready', function() {
	console.log('module a is ready');
});
 

exports变量

Node为每个模块提供了一个exports变量,指向module.exports。等于在每个模块头部加了一行命令:

var exports = module.exports;
 

在对外输出模块接口时,可以向exports对象添加方法:

exports.area = function (r) {
	return Math.PI * r * r;
};
 

注意,不能直接给exports赋值,这样等于切断了exports与module.exports的联系。例如:

exports = function (x) {console.log(x)};
 

上面的写法,exports将不再指向module.exports。

如果,一个模块的对外接口,是一个单一的值,不能使用exports输出,只能使用module.exports。

module.exports = 'Hello world';
 

如果处理不好他们的区别,可以只使用module.exports。

AMD规范与CommonJS规范的兼容

CommonJS规范加载模块是同步的,只有加载完成,才能执行后面的操作。AMD规范是非同步加载模块,允许指定回调函数。Node.js主要用于服务器编程,模块文件一般都存在于本地硬盘,所以加载比较快,不用考虑非同步加载方式,比较适用CommonJS规范。浏览器环境,需要从服务器端加载模块,就必须采用非同步模式,所以浏览器端一般采用AMD规范。

AMD规范使用define方法定义模块:

define(['package/lib'], function(lib){
    function foo(){
        lib.log('hello world!');
    }
    
    return {
        foo: foo
    };
});
 

AMD规范允许输出的模块兼容CommonJS规范:

define(function(require, exports, module){
    var someModule = require("someModule");
    var anotherModule = require("anotherModule");
    
    someModule.doTehAwesome();
    anotherModule.doMoarAwesome();
    
    exports.asplode = function (){
        someModule.doTehAwesome();
        anotherModule.doMoarAwesome();
    };
});
 

require命令

Node内置的require命令用于加载模块文件。

require命令的基本功能,读入并执行一个js文件,然后返回该模块的exports对象。如果没有发现指定模块会报错。

// example.js
var invisible = function() {
    console.log("invisible");
}

exports.message = "hi";

exports.say = function() {
    console.log(message);
}
 

输出exports对象:

var example = require('./example.js');
example

// 输出
// {
//     message: "hi";
//     say: [Function]
// }
 

如果模块输出的是一个函数,就不能定义在exports对象中,要定义在module.exports里面。

module.exports = function() {
    console.log("hello world")
}

require('./example2/js')()
 

require命令调用自身,等于执行module.exports。

加载规则

require命令用于加载文件,后缀默认为.js

根据参数的不同格式,require命令去不同路径寻找模块文件。

(1)如果参数以"/"开头,表示加载的是一个绝对路径。

(2)如果参数以"./"开头,表示加载的是一个相对路径。

(3)如果参数不以以上两种开头,表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者位于node_modules目录的已安装模块(全局或局部安装)。

(4)如果参数不以"./"或"/"开头,而且是一个路径,如require('example/path/file'),则先找到example的位置,然后再以它为参数,找到后续路径。

(5)如果指定的模块文件没有发现,Node会尝试为文件名加.js.json.node后,再搜索。.js文件会以文本格式的js脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。

(6)如果想得到require命令加载的确切文件名,使用require.resolve()方法。

举例:

执行require('bar.js')命令,Node会依次搜索一下文件:

/usr/local/lib/node/bar.js

/home/user/projects/node_modules/bar.js

/home/user/node_modules/bar.js

/home/node_modules/bar.js

/node_modules/bar.js

目录的加载规则

通常,我们会把相关文件放在同一个目录里,便于组织管理。最好为该目录设置一个入口文件,require方法可以通过入口文件,加载整个目录。

在目录中放一个package.json文件,并将入口文件写入main字段。

{
    "name": "some-library",
    "main": "./lib/some-library.js"
}
 

require发现参数是一个目录后,自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package,json文件没有main字段或者没有package.json文件,则加载该目录下的index.jsindex.node文件。

模块的缓存

第一次加载模块时,Node会缓存该模块。以后再加载该模块,直接从缓存取出该模块的module.exports属性。

如果要多次执行模块,可以让该模块输出一个函数,每次require这个模块的时候,重新执行输出的函数。

所有缓存的模块保存在require.cache中,删除模块缓存:

// 删除指定模块的缓存
delete require.cache[moduleName];

// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
    delete require.cache[key];
})
 

注意,缓存是根据绝对路径识别模块的,如果同样的模块名,保存在不同路径,require命令还是会重新加载该模块。

环境变量NODE_PATH

Node执行一个脚本时,会先查看环境变量,它是一组以冒号分隔的绝对路径。在其他位置找不到指定模块时,Node会去这些路径查找。

可以将NODE_PATH添加到.bashrc

export NODE_PATH="/usr/local/lib/node"
 

如果遇到复杂的相对路径,如:"../../../../lib/myModule",可以将该文件加入node_modules目录,还可以修改NODE_PATH环境变量,package.json文件可以用下面的写法:

{
    "name": "node_path",
    "version": "1.0.0",
    "description": "",
    "main": "index,js",
    "scripts": {
        "start": "NODE_PATH=lib node index.js"
    },
    "author": ""
}
 

NODE_PATH环境变量是历史遗留下来的路径解决方案,通常不应该使用,应该使用node_modules目录。

模块的循环加载

模块的循环加载,即A加载B,B又加载A,则B将加载A的不完整版本。

// a.js
exports.x = 'a1';
console.log('a.js', require('./b.js').x);
exports.x = 'a2';

// b.js
exports.x = 'b1';
console.log('b.js', require('./a.js').x);
exports.x = 'b2';

// main.js
console.log('main.js', require('./a.js').x);
console.log('main.js', require('./b.js').x);
 

上面代码是三个js文件,a.js加载了b.js,b.js又加载了a.js,这时,Node返回a.js的不完整版本,所以,main.js中两次console.log输出的结果是不一样的。

$ node main.js
b.js a1
a.js b2
main.js a2
main.js b2
 

第一次执行main.jsrequire('./a.js').x时,先执行a.js,然后加载b.js,执行b.js时,a.jsexports.x等于a1,b.js执行完成,所以exports.x等于b2,第二次执行console.log('main.js', require('./b.js').x)时,直接从缓存中读取exports属性,所以直接输出main.js b2,a.js和b.js内部的console.log语句都不会执行。

require.amin

require方法有一个main属性,用来判断模块是直接执行,还是被调用执行(通过require加载该脚本执行)。直接执行的时候(node module.js),require.main属性指向模块本身require.main === module结果为true

模块的加载机制

CommonJS模块的加载机制是,输出一个值,模块内部的变化就影响不到这个值。

// lib.js
var counter = 3;
function incCounter() {
    counter++;
}
module.exports = {
    counter: counter,
    incCounter: incCounter
};
 
// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;

console.log(counter); // 3
incCounter();
console.log(counter); // 3
 

上面代码说明,counter输出以后,模块内部的变化影响不到counter。

require的内部处理流程

require命令不是一个全局命令,而是指向当前模块的module.require命令,module.require调用Node的内部命令Module._load

Module._load = function(request, parent, isMain) {
  // 1. 检查 Module._cache,是否缓存之中有指定模块
  // 2. 如果缓存之中没有,就创建一个新的Module实例
  // 3. 将它保存到缓存
  // 4. 使用 module.load() 加载指定的模块文件,读取文件内容之后,使用 module.compile() 执行文件代码
  // 5. 如果加载/解析过程报错,就从缓存删除该模块
  // 6. 返回该模块的 module.exports
};
 

第4步,采用module.compile()执行指定模块的脚本,逻辑如下:

Module.prototype._compile = function(content, filename) {
  // 1. 生成一个require函数,指向module.require
  // 2. 加载其他辅助方法到require
  // 3. 将文件内容放到一个函数之中,该函数可调用 require
  // 4. 执行该函数
};
 

require函数及其辅助方法主要如下:

require(): 加载外部模块

require.resolve():将模块名解析到一个绝对路径

require.main:指向主模块

require.cache:指向所有缓存的模块

require.extensions:根据文件的后缀名,调用不同的执行函数

require函数准备完毕,所有要加载的脚本内容,被放到一个新的函数中,这样可以避免污染全局环境。该函数的参数包括requiremoduleexports,以及其他一些参数。

(function (exports, require, module, __filename, __dirname) {});

Module._compile方法是同步执行的,所以Module._load要等它执行完成,才会向用户返回module.exports的值。

参考链接

本文参考阮一峰老师的文章(CommonJS规范),做了一些改动和总结。仅供学习和方便查阅。

回复

我来回复
  • 暂无回复内容