3.3 模块和包
3.3.1 什么是模块
模块(module)和包(package)是Nodejs的重要支柱。开发一个具有一定规模的程序不可能只用一个文件,通常需要把各个功能拆分、封装然后组合起来,模块正是为了实现这种方式诞生的。
模块是Nodejs最基本的组成部分,文件和模块是一一对应的。即一个Nodejs文件就是一个模块。包是实现了某个特定功能的模块的集合。包和模块的概念时常混用。
Nodejs提供的require函数可以调用其他模块,这符合CommonJS的标准。前面章节所提到的HTTP模块是Nodejs的核心模块,其内部是用C++实现的,外部用JS封装。所以,node模块对应的模块除了JS文件外还可能是编译过的JSON或者C/C++括展。
3.3.2 如何创建模块
Nodejs中,一个文件就是一个模块,我们要关注的问题仅仅在于如何在其他文件中获取这个模块。Nodejs提供了exports和requirejs两个对象,其中exports是模块公开的接口,require用于从外部获取一个模块的接口,即获取模块的exports对象。
建立一个文件夹,在下面放置如下两个文件:
module.js:
var name = "Kyle";
function setName(newName) {
name = newName;
}
function sayHello() {
console.log("my name is "+name);
}
exports.setName = setName;
exports.sayHello = sayHello;
getModule.js:
var module = require('./module');
module.setName("Tom");
module.sayHello();
require取模块时要注意模块的路径,模块名即为文件名,具体请向上翻阅,见3.1.2节。module.js通过exports对象把setName和sayHello作为模块的访问接口,然后就可以直接访问module.js中exports对象的成员函数了。这种简单但不失优雅的方式搭建了npm提供的上万个模块。
3.3.3 模块单次加载
模块的加载和创建对象有本质的区别,因为require不会重复的加载模块,也就是说,无论调用多少次require,只要是其参数,即加载的模块名相同,无论调用多少次require,获得的模块总是同一个。
下面的代码在getModule.js稍作改动:
var module1 = require('./module');
var module2 = require('./module');
module1.setName("Jack");
module2.setName("Jeff");
module1.sayHello();
输出 my name is Jeff。前者被后者覆盖。
3.3.4 导出一个构造函数——导出模块的另一种方式
有时候我们只想把一个构造函数封装到模块中,例如:
//Student.js
function Student(newName) {
this.name = "";
this.setName = function (newName) {
this.name = newName;
};
this.sayHello = function () {
console.log("Hello everyone, my name is" + this.name );
}
}
exports.Student = Student;
此时通过require("./Student.js").Student来获取Student构造函数就显得有些多余,因为实际上我们只导出了一个构造函数,再以对象.属性的方式获取反倒没有必要。Nodejs提供了另一种导出模块的方法:
//Student.js
function Student(newName) {
this.name = "";
this.setName = function (newName) {
this.name = newName;
};
this.sayHello = function () {
console.log("Hello everyone, my name is" + this.name );
}
}
module.exports = Student;
获取模块的方式也有所变化
//getModule.js
var Student = require("./Student");
var student = new Student();
student.setName("Jack");
student.sayHello();
此时require()的返回值,也就是Student模块导出的东西直接就是Student构造函数,所以直接获取之就可以。这种方式和上一种为exports设置子属性的方式的区别在于这种方法更适合那种只导出一个事物的模块——一般用这种导出构造函数(当然你可以用这种方式导出任意一种东西),在获取模块内容时就无需再以对象.属性的方式获取。实际上这两种方法的本质是一样的。我们可以用第二种导出构造函数的方式来实现模块多次加载的效果,因为每一次new出来的对象都是新的。
3.3.5 深入理解exports对象
exports本身仅仅是一个普通的空对象,即{},它专门用来声明接口。因为它没有任何特殊的地方,所以可以用其他任何东西来代替之。
但是,require函数的返回值,即其加载模块输出的东西实际上是module.exports,而不是exports。
exports是对module.exports的局部引用,也就是说exports只是与module.exports指向同一个对象的变量,二者共同占用一块内存空间。你修改了exports对象,module.exports的内容也会被修改。exports会在模块执行结束后释放,而最终导出的是module.exports,换句话说exports.myFun只是module.exports.myFun的缩写。
所以,像下面这种直接给exports赋值情况是不允许的
exports = Student;
因为这样你改变了exports指向的内存地址,它不再与module.exports指向同一个地方,所以导出时导出的module.export没有变化。而像这样
exports.Student = Student;
则是允许的,因为exports
只是新增了属性,而其指向的内存地址没有变化,与此同时,因为exports
和module.exports
指向的内存地址相同,所以module.exports
的内容也被修改了。
简而言之,在定义模块导出时,实际上导出的是module.exports
。设置module.exports.属性
和exports.属性
是等价的,因为它们不涉及内存地址的改变,exports.属性
只是Nodejs为导出提供的一个简写。而exports是不能被修改的,否则就会打破module.exports
的引用关系,所以如果你只想导出一个构造函数,需要设置module.exports
的值而不是exports
的值。
3.3.6 创建一个最简的单包
前面我们说过,Nodejs包和模块的概念往往不用分的那么清楚。最最基本的原则是一个模块是一个文件。然而,当你的“模块”实现的功能非常多,代码量非常庞大的时候,为了便于组织,往往需要分成很多“子模块”,否则很难承载庞大的结构。这样我们的“模块”就被分成了多个文件,似乎违反了Nodejs模块的原则。然而,Nodejs认为,文件夹(目录)也是一个文件,所以,当我们的模块由许多“子模块”文件组成时,我们就把这些子文件放在一个文件夹下,成为一个“目录模块”,即“包”——子模块的集合。
最简单的包,就是这样一个作为文件夹(目录)的模块。现在我们先试着创建一个最简单的包。
创建一个叫做somepackage的文件夹,在里面创建一个叫做index.js的文件:
exports.sayHello = function () {
console.log("Hello everyone, I love you")
};
这样,一个最简单的包就制作完成了。注意包的名字要小写,作为包的文件夹名字中如果出现大写字母的话,在引入模块时会被当做小写解析。现在我们试着调用这个包。在这个包的外部建立一个叫做getPackage.js的文件:
var somePackage = require('./somepackage');
somePackage.sayHello();
运行getPackage.js文件,输出:Hello everyone, I love you
3.3.7 导入一个包究竟导入了什么东西
上面的案例里,包中只有一个叫做index.js的文件,当我们引入该包时,该包的接口文件就是index.js,我们引入包的时候,就像是require函数里的参数是./somepackage/index
一样。那么问题来了:
- 首先,我们明明引入的是这个包,怎么和单独引入
./somepackage/index
的效果一样? - 当我们的包中不只有index.js一个文件,而是有多个文件的时候,又该怎么办呢?
请思考,包本身就是一个文件夹,本身不能承载任何代码,所以单独引入它是没有意义的。Nodejs引入包的规则是:当你引入一个包时,Nodejs会寻找包(文件夹)一级目录下的index.js文件作为接口文件,你导入获得的对象就是这个文件相应的模块。当然我们能够改变这个局面。
3.3.8 为包设置接口模块
每个包都需要一个接口文件,即定义这个模块的文件,否则这个包就是一个装着一大堆文件的文件夹,没有意义。如果我们不愿将就,不想让index.js作为接口文件,或者不想让我们的接口文件放在文件夹的一级目录下面,又该怎么办呢?
package.json可以帮助我们解决这个问题。通过定制package.json,我们可以创建更复杂,更完善,更符合规范的包用于发布。有关package.json的介绍我们后面会详细讲解。
在前面的somepackage文件夹下,创建一个叫做package.json的文件,内容如下所示:
{
"main":"./lib/interface.js"
}
在somepackage下创建一个文件夹名叫lib,将index.js改名为interface.js并移动至lib文件夹下。重新运行getPackage.js文件,与前面运行的结果相同。
Nodejs在调用某个包时,会首先解析包中的package.json文件,我们可以通过在package.json文件中添加main属性(元素)来指定包的接口模块是哪个,如果按照这个路径找不到相应的文件,则会报错。如果包中不存在package.json,那么Nodejs就会默认认为接口文件是文件夹一级目录下的index.js或index.node,如果也找不到index.js或index.node,则会报错。如果包中存在package.json但是这个文件中不存在main字段,在解析时也会报错。
寻找接口模块的流程如下图所示:
3.3.9 commonJS对包的规范
Nodejs的包是一个目录,其中包含一个名叫package.json的包说明文件。下面是CommonJS对包的规范:
- package.json必须在顶层目录下
- 二进制文件应该在bin目录下
- JS代码应该在lib目录下
- 文档应该在doc目录下
- 单元测试应该在test目录下
nodejs本身并没有这么严格的要求,只要顶层目录有package.json并符合一些规范即可。当然为了提高兼容性,你还是被建议严格遵守CommonJS制作包的规范