珠峰培训

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。