CommonJS(http://www.commonjs.org)规范的出现,其目标是为了构建JavaScript在包括Web服务器,桌面,命令行工具,及浏览器方面的生态系统。CommonJS其实不是一门新的语言,甚至都不能说它是一个新的解释器——实际上它只是一个概念或者是一个规范。
它具体弥补了前端JavaScript 的哪些空白呢?其实这也涉及了很多前端JavaScript 所没有涉及的东西,如二进制、编码、IO、文件、系统、断言测试、套接字、事件队列、Worker、控制台等等。
CommonJS制定了解决这些问题的一些规范,而Node.js就是这些规范的一种实现。Node.js自身实现了require方法作为其引入模块的方法,同时NPM也基于CommonJS定义的包规范,实现了依赖管理和模块自动安装等功能。这里我们将深入一下Node.js的require机制和NPM基于包规范的应用。
先来看一个简单定义的模块计算圆的面积:
// circle.js
var PI = Math.PI;
exports.area = function(r){
return PI * r * r;
}
// app.js
var circle = require('./circle.js');
console.log(circle.area(2)); // 4PI
可以看到模块的调用十分地方便,只需要require需要调用的文件即可;Node.js讲模块的定义和调用都封装地及其简约,对用户十分友好;
来看看一个小栗子:
// rocker.js
module.exports = 'ROCK N ROLL!';
exports.name = function() {
console.log('Let them know that we still rock n roll');
};
//app.js
var rocker = require('./rocker.js');
rocker.name(); // TypeError: Object ROCK N ROLL! has no method 'name'
rocker模块完全忽略了exports.name,然后返回了一个字符串’ROCK IT!’。
通过上面的例子,你可能认识到你的模块不一定非得是模块实例(module instances)。你的模块可以是任何合法的JavaScript对象 - boolean,number,date,JSON, string,function,array和其他。
如果你没有明确的给module.exports设置任何值,那么exports中的属性会被赋给module.exports中,然后并返回它。
正确用法是:
// rocker.js
module.exports = function(name, age) {
this.name = name;
this.age = age;
this.about = function() {
console.log(this.name +' is '+ this.age +' years old');
};
};
//app.js
var Rocker = require('./rocker.js');
var rocker = new Rocker('yivi',18);
rocker.about(); //yivi is 18 years old
把属性赋予module.exports的结果与把属性赋予给exports是一样的。看下面这个例子:
module.exports.name = function() {
console.log('My name is Lemmy Kilmister');
};
//等同于
exports.name = function() {
console.log('My name is Lemmy Kilmister');
};
Node.js的模块分为两类:
但是Node.js对原生模块和文件模块都进行了缓存,于是在第二次require时,是不会有重复开销的。其中原生模块都被定义在lib这个目录下面,文件模块则不定性。
加载文件模块的工作,主要由原生模块module来实现和完成,该原生模块在启动时已经被加载,进程直接调用到runMain静态方法。
实际上在文件模块中,又分为3类模块。这三类文件模块以后缀来区分,Node.js会根据后缀名来决定加载方法:
**.js:**通过fs模块同步读取js文件并编译执行。
**.node:**通过C/C++进行编写的Addon。通过dlopen方法进行加载。
**.json:**读取文件,调用JSON.parse解析加载。
Node.js在编译js文件的过程中会对js文件进行头尾包装,将其包装为一个模块,使其不污染全局,就像这样:
(function (exports, require, module, __filename, __dirname) {
var circle = require('./circle.js');
console.log('The area of a circle of radius 4 is ' + circle.area(4));
});
runInThisContext
方法执行(类似eval,只是具有明确上下文,不污染全局),返回为一个具体的function对象。最后传入module对象的exports
,require
方法,module,文件名,目录名作为实参并执行。__filename
、__dirname
、module
、 exports
几个没有定义但是却存在的变量。其中__filename
和 __dirname
在查找文件路径的过程中分析得到后传入的。module变量是这个模块对象自身,exports
是在module的构造函数中初始化的一个空对象({},而不是 null)。以上所描述的模块载入机制均定义在lib/module.js中。
require方法接收以下几种参数的传递:
http、fs、path等,原生模块。
/mod或…/mod,相对路径的文件模块。
/pathtomodule/mod,绝对路径的文件模块。
mod,非原生模块的文件模块。
对于每一个被加载的文件模块,创建这个模块对象的时候,这个模块便会有一个paths属性,其值根据当前文件的路径计算得到。
path的生成规则是:从当前文件目录开始查找node_modules目录;然后依次进入父目录,查找父目录下的node_modules目录;依次迭代, 直到根目录下的node_modules目录。
如果require绝对路径中的文件,查找时不会由内而外地遍历每一个node_modules目录,速度最快。
前面提到JavaScript缺少包结构。CommonJS致力于改变这种现状,于是定义了包的结构规范(http://wiki.commonjs.org/wiki/Packages/1.0)。而NPM的出现则是为了在CommonJS规范的基础上,实现解决包的安装卸载,依赖管理,版本管理等问题。require的查找机制明了之后,我们来看一下包的细节。
一个package.json文件应该存在于包顶级目录下。
二进制文件应该包含在bin目录下。
JavaScript代码应该包含在lib目录下。
文档应该在doc目录下。
单元测试应该在test目录下。
由上文的require
的查找过程可以知道,Node.js在没有找到目标文件时,会将当前目录当作一个包来尝试加载,所以在package.json文件中最重要的一个字段就是main。而实际上,这一处是Node.js的扩展,标准定义中并不包含此字段, 对于require
,只需要main属性即可。但是在除此之外包需要接受安装、卸载、依赖管理,版本管理等流程,所以CommonJS为package.json文件定义了如下一些必须的字段:
name : 包名,需要在NPM上是唯一的。不能带有空格。
description : 包简介。通常会显示在一些列表中。
version : 版本号。一个语义化的版本号(http://semver.org/),通常为x.y.z。该版本号十分重要,常常用于一些版本控制的场合。
keywords : 关键字数组。用于NPM中的分类搜索。
maintainers : 包维护者的数组。数组元素是一个包含name、email、web三个属性的JSON对象。
contributors : 包贡献者的数组。第一个就是包的作者本人。在开源社区,如果提交的patch被merge进master分支的话,就应当加上这个贡献patch的人。格式包含name和email。
bugs : 一个可以提交bug的URL地址。可以是邮件地址 (mailto:mailxx@domain),也可以是网页地址(http://url)。
licenses : 包所使用的许可证。
repositories : 托管源代码的地址数组。
dependencies : 当前包需要的依赖。这个属性十分重要,NPM会通过这个属性,帮你自动加载依赖的包。
scripts: 指明了在进行操作时运行哪个文件,或者执行拿条命令。
通常有一些模块可以适用于前后端,但是浏览器通过script标签加载的JavaScript文件时裸漏的,而Node.js在载入到最终的执行过程中,进行了包装,使得每个文件的变量都封装在一个闭包内,不会污染全局变量。
所以为了解决前后端一致性的问题,类库开发者需要将类库代码包装在一个闭包内。
所以在设计前后端通用的JavaScript类库时,都有着以下类似的判断:
if (typeof exports !== "undefined") {
exports.EventProxy = EventProxy;
} else {
this.EventProxy = EventProxy;
}
即,如果exports对象存在,则将局部变量挂载在exports对象上,如果不存在,则挂载在全局对象上。