node入门(二)核心API(一)
2015-11-24 16:15:51

138
模块概述
Node.js采用模块化结构,按照CommonJS规范定义和使用模块。模块与文件是一一对应关系,即加载一个模块,实际上就是加载对应的一个模块文件。
require命令用于指定加载模块,加载时可以省略脚本文件的后缀名。
var zfpx = require('./zfpx.js'); //或者 var zfpx = require('./zfpx');//省略后缀名的时候会尝试自动添加.js查找
require方法的参数是模块文件的名字。它分成两种情况,第一种情况是参数中含有文件路径(比如上例),这时路径是相对于当前脚本所在的目录 第二种情况是参数中不含有文件路径,这时Node到模块的安装目录,去寻找已安装的模块(比如下例)。
var zpfx = require('zpfx');
有时候,一个模块本身就是一个目录,目录中包含多个文件。这时候,Node在package.json文件中,寻找main属性所指明的模块入口文件。
{ "name" : "zpfx", "main" : "./lib/zpfx.js" }
上面代码中,模块的启动文件为lib子目录下的zpfx.js。当使用require('bar')命令加载该模块时,实际上加载的是./node_modules/zfpx/lib/zfpx.js文件。下面写法会起到同样效果。
var bar = require('bar/lib/zfpx.js')
如果模块目录中没有package.json文件,node.js会尝试在模块目录中寻找index.js或index.node文件进行加载。
模块一旦被加载以后,就会被系统缓存。如果第二次还加载该模块,则会返回缓存中的版本,这意味着模块实际上只会执行一次。如果希望模块执行多次,则可以让模块返回一个函数,然后多次调用该函数。
核心模块
如果只是在服务器运行JavaScript代码,用处并不大,因为服务器脚本语言已经有很多种了。Node.js的用处在于,它本身还提供了一系列功能模块,与操作系统互动。这些核心的功能模块,不用安装就可以使用,下面是它们的清单。
http:提供HTTP服务器功能。 url:解析URL。 fs:与文件系统交互。 querystring:解析URL的查询字符串。 child_process:新建子进程。 util:提供一系列实用小工具。 path:处理文件路径。 crypto:提供加密和解密功能,基本上是对OpenSSL的包装。
上面这些核心模块,源码都在Node的lib子目录中。为了提高运行速度,它们安装时都会被编译成二进制文件。
核心模块总是最优先加载的。如果你自己写了一个HTTP模块,require('http')加载的还是核心模块。
自定义模块
Node模块采用CommonJS规范。只要符合这个规范,就可以自定义模块。
下面是一个最简单的模块,假定新建一个zfpx.js文件,写入以下内容。
module.exports = function(sound) { console.log(sound); };
上面代码就是一个模块,它通过module.exports变量,对外输出一个方法。
这个模块的使用方法如下。
var zfpx = require('./zfpx'); zfpx("hello world");
上面代码通过require命令加载模块文件zfpx.js(后缀名省略),
将模块的对外接口输出到变量zfpx,然后调用zfpx。这时,在命令行下运行zfpx.js,屏幕上就会输出hello world。
$ node zfpx.js hello world
module变量是整个模块文件的顶层变量,它的exports属性就是模块向外输出的接口。如果直接输出一个函数(就像上面的zfpx.js),那么调用模块就是调用一个函数。但是,模块也可以输出一个对象。下面对zfpx.js进行改写。
var zfpx = new Object(); function say(word) { console.log(word); } out.say = say; module.exports = zfpx;
上面的代码表示模块输出zfpx对象,该对象有一个say属性,指向一个函数。下面是这个模块的使用方法。
// zfpx.js var zfpx = require('./zfpx'); dog.say("hello world");
上面代码表示,由于具体的方法定义在模块的say属性上,所以必须显式调用say函数。
概述
Node程序由许多个模块组成,每个模块就是一个文件。Node模块采用了CommonJS规范。
根据CommonJS规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在一个文件定义的变量(还包括函数和类),都是私有的,对其他文件是不可见的。 // 文件名 calculator.js var x = 5; var add = function(value) { return value + x; };
上面代码中,变量x和函数add,是当前文件私有的,其他文件不可见。
如果想在多个文件分享变量,必须定义为global对象的属性。
global.warning = true;
面代码的waining变量,可以被所有文件读取。当然,这样写法是不推荐的。
CommonJS规定,每个文件的对外接口是module.exports对象。这个对象的所有属性和方法,都可以被其他文件导入。
var x = 5; var add = function(value) { return value + x; }; module.exports.x = x; module.exports.add = add;
上面代码通过module.exports对象,定义对外接口,输出变量x和函数add。
module.exports对象是可以被其他文件导入的,它其实就是文件内部与外部模块通信的桥梁。
require方法用于在其他文件加载这个接口,require具体用法在下面
var calculator = require('./calculator.js'); console.log(calculator.x); // 5 console.log(calculator.add(1)); // 6
module对象
每个模块内部,都有一个module对象,代表当前模块。它有以下属性。
- module.id 模块的识别符,通常是带有绝对路径的模块文件名。
- module.filename 模块的文件名,值为此模块的绝对路径。
- module.loaded 返回一个布尔值,表示本模块是否已经完成加载。
- module.parent 返回一个对象,表示调用该模块的模块。
- module.children 返回一个数组,表示该模块要用到的其他模块,也就是require的其它模块。
修改上面的 calculator.js,最后一行输出module变量。
console.log(module);
执行这个文件,命令行会输出如下信息。
Module { id: '.',// 此模块的ID,.代表入口启动模块 exports: { x: 5, add: [Function] }, //导出对象 parent: null, //表示调用该模块的模块。 filename: 'd:\\vip_data\\mygit\\zhufeng_node\\calculator.js',//模块的文件名,值为此模块的绝对路径。 loaded: false, //表示本模块是否已经完成加载。 children: [],//返回一个数组,表示该模块要用到的其他模块, paths: //require加载第三方模块的查找路径 [ 'd:\\vip_data\\mygit\\zhufeng_node\\node_modules', 'd:\\vip_data\\mygit\\node_modules', 'd:\\vip_data\\node_modules', 'd:\\node_modules' ] }
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.say = function (r) { console.log('hello world'); };
注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。
exports = function(x) {console.log(x)};
上面这样的写法是无效的,因为exports不再指向module.exports了。
下面的写法也是无效的。
exports.hello = function() { return 'hello'; }; module.exports = 'Hello world';
上面代码中,hello函数是无法对外输出的,因为module.exports被重新赋值了。
这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。
module.exports = function (x){ console.log(x);};
如果你觉得,exports与module.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports。
require命令
基本用法
Node.js使用CommonJS模块规范,内置的require命令用于加载模块文件。 require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的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,因此会输出 hello world。
加载规则
require命令用于加载文件,后缀名默认为.js。
var zfpx = require('zfpx'); // 等同于 var zfpx = require('zfpx.js');
根据参数的不同格式,require命令去不同路径寻找模块文件。
(1)如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require('/home/zfpx.js')将加载/home/zfpx.js。
(2)如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require('./circle')将加载当前脚本同一目录的circle.js。
(3)如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。
举例来说,在windows系统中,脚本/home/user/projects/zfpx.js执行了require('bar.js')命令,Node会依次搜索以下文件。
/d/user/projects/node_modules/bar.js /d/user/node_modules/bar.js /d/node_modules/bar.js
就是先从当前目录下面的node_modules开始,然后一级一级往上找,找到根目录为止。
这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。
(4)如果参数字符串不以“./“或”/“开头,而且是一个路径,比如require('example-module/path/to/file'),则将先找到example-module的位置,然后再以它为参数,找到后续路径。
(5)如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。
(6)如果想得到require命令加载的确切文件名,使用require.resolve()方法。
目录的加载规则
通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让require方法可以通过这个入口文件,加载整个目录。
在目录中放置一个package.json文件,并且将入口文件写入main字段。下面是一个例子。
// package.json { "name" : "some-library", "main" : "./lib/some-library.js" }
require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件。
查找模块
查找文件
模块的缓存
第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的exports属性。 require('./example.js'); require('./example.js').message = "hello"; require('./example.js').message // "hello"
上面代码中,连续三次使用require命令,加载同一个模块。第二次加载的时候,为输出的对象添加了一个message属性。但是第三次加载的时候,这个message属性依然存在,这就证明require命令并没有重新加载模块文件,而是输出了缓存。 如果想要多次执行某个模块,可以让该模块输出一个函数,然后每次require这个模块的时候,重新执行一下输出的函数。 注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块。
模块的循环加载
如果发生模块的循环加载,即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);
上面代码是三个JavaScript文件。其中,a.js加载了b.js,而b.js又加载a.js。这时,Node返回a.js的不完整版本,所以执行结果如下。
$ node main.js b.js a1 a.js b2 main.js a2 main.js b2
修改main.js,再次加载a.js和b.js。
// main.js console.log('main.js ', require('./a.js').x); console.log('main.js ', require('./b.js').x); console.log('main.js ', require('./a.js').x); console.log('main.js ', require('./b.js').x);
执行上面代码,结果如下。
$ node main.js b.js a1 a.js b2 main.js a2 main.js b2 main.js a2 main.js b2
上面代码中,第二次加载a.js和b.js时,会直接从缓存读取exports属性,所以a.js和b.js内部的console.log语句都不会执行了。
require.main
require方法有一个main属性,可以用来判断模块是直接执行,还是被调用执行。
直接执行的时候(node module.js),require.main属性指向模块本身。
require.main === module // true
调用执行的时候(通过require加载该脚本执行),上面的表达式返回false。