`

一起读nodejs(三)----模块(Modules)

 
阅读更多

本文是对nodejs0.8.9版本的api开发手册解读.nodejs网址

模块(Modules)

stability:5 -locked

node有一个简单的模块加载机制.在node里面文件和模块是 一对一 对应的.例如,foo.js加载在同一文件夹下的circle.js模块.

foo.js的内容:

var circle = require('./circle.js');
console.log( 'The area of a circle of radius 4 is '
           + circle.area(4));

circle.js的内容:

var PI = Math.PI;

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

exports.circumference = function (r) {
  return 2 * PI * r;
};

circle.js模块以导出了area()方法和circumference()方法.为了导出一个对象,需要加上指定的exports对象.

 

属于模块的变量是私有的.在这个例子中变量PI就是私有于circle.js.

模块机制是在require('module')模块中实现的.


环形加载(Cycles)


当存在环形的require()调用时,当一个模块被返回被执行的时候,可能还没有加载完成,考虑下面这种情景:
a.js:
console.log('a starting');
exports.done = false;
var b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
b.js:
console.log('b starting');
exports.done = false;
var a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
main.js:
console.log('main starting');
var a = require('./a.js');
var b = require('./b.js');
console.log('in main, a.done=%j, b.done=%j', a.done, b.done);
当main.js加载a.js的时候,a.js依次需要加载b.js.在这时,b.js试着去加载a.js.为了防止一个无穷的循环(loop),一个未完成的a.js的副本的exports导出对象被返回到b.js模块.然后b.js完成加载,然后他的exports导出对象被提供给a.js模块.

当main.js加载完这两个模块的时候,他们都已经加载完.这个程序的输出应该这样子:
$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done=true, b.done=true
如果你的程序里面有环形的模块依赖,确保制定相应的计划(意思是,确保在所有依赖的模块都加载完时在调用,避免在模块未加载完时,调用导致程序不可预期的结果).

(循环加载,require()方法有对应的机制,上面例子中b.js返回时,b.js加载的a.js没有加载完,但是当b.js加载完时,a.js就加载完了,这时候b.js里面保存的a.js也会被更新成完成的对象,其实我觉得保存的可能就是类似于a.js的引用.)
核心模块(Core modules)

node中有一些模块被编译成二进制. 这些模块在文档的其他地方有更详细的描述.

核心模块被定义在node的源码中的lib文件夹下.

如果核心模块的标识符被传入require()方法时,总是被优先加载.例如,require('http')将总是返回内置的http模块,即使当前有一个同名的模块文件夹.

文件模块加载机制(File modules)

如果准确的文件名字没有找到,node将会试图依次加载添加了.js,.json,.node等后缀名的文件.
.js文件被当作javascript文本文件解读,.json文件被转换成json文本文件. .node文件被当做已经编译好的插件模块,使用dloopen加载.(在dlopen()函数以指定模式打开指定的动态连接库文件,并返回一个句柄给调用进程。使用dlclose()来卸载打开的库。)

当一个模块的前缀是"/"时,表示一个模块文件的绝对路径.例如:require('/home/marco/foo.js')将会使用'/home/marco/foo.js'作为路径加载.

当一个模块的前缀是"./"时,表示一个模块文件的相对路径调用require().例如:为了让foo.js中的require('./circle.js')能被找到,circle.js必须和foo.js在同一目录下.

如果没有"./"或者"/"当文件前缀,这个模块不是一个核心模块,就是一个需要从node_modules文件夹中加载的模块.

如果给定的加载路径不存在,requier()方法将会抛出一个Error错误对象,并且Error的code属性被设置成:'MODULE_NOT_FOUND'.
从node_modules文件夹加载模块(loading from node_mudoles folders)

如果传入到require()方法中的模块标识符不是本地模块,并且也不是"/","../",或者"./"开头,node则会在当前模块的父目录路径上追加/node_modules,以这个路径试图加载模块.

如果这里也没有找到,node会继续移动到更上一层的父目录,等等...直到到达根目录.

例如,文件'/home/ry/project/foo.js'中调用require('bar.js'),node会查找以下列出的路径,依次是:
  • /home/ry/projects/node_modules/bar.js
  • /home/ry/node_modules/bar.js
  • /home/node_modules/bar.js
  • /node_modules/bar.js
这将允许程序本地化他们的依赖,以便不产生冲突.

文件夹就是模块(Floder as modules)

组织程序和类库放进自己包含的目录下是很方便的,然后提供一个单独的入口指定这个类库,有三种方法把一个文件夹作为一个参数传进require()方法.

第一种方法是在根目录创建一个package.json文件,指定一个主模块.一个paceage.json的例子看起来像这样子:

{ "name" : "some-library",
  "main" : "./lib/some-library.js" }
如果当前程序路径是在./some-library目录下,则require('./some-library')将会试图加载路径./some-library/lib/some-library.js.

这是node自动拓展自package.json文件的结果.

如果这个目录下没有出现package.json文件,node将会试图在这个目录加载一个index.js或者index.node文件出来.例如,如果在上面的例子中没有package.json文件,则require('./sone-library')将会试图加载:
./some-library/index.js
./some-library/index.node
缓存(Caching)
当模块被第一次加载之后就被缓存了起来,这意味着在其他地方调用require('foo')将会获得一样一样的对象被返回,如果他们扫描的是同一文件的情况下.

多次调用require('foo')不会引起模块代码被执行多次.这是一个重要的特性.有了它,部分加载完成的对象可以被返回,进而允许加载过渡期的依赖对象,甚至当他们引起环形加载.
(上面这段话,其实就是对本文刚开始介绍的环形加载的一种说明,就是说,在node中require()方法加载对象时,可能还没加载完就返回了,但是返回的是一个索引而已,在一个node应用中,使用require()加载一个模块,都会先在缓存中查找,有就返回,没有在加载.)
如果你想要一个模块的代码被执行多次,你可以导出一个方法,然后调用这个方法.
缓存模块提醒(Module Caching caveats)
模块是以他们被解析出的文件名称为基础缓存起来的,因为模块可能由于调用require()方法的当前路径不一样,而导致解析出不一样的文件名称,(从node_modules文件夹加载),如果解析出不同的文件,将不能保证require('foo')总是会返回一致的对象.
module对象(The module Object)
在每一个模块中,自由变量module是一个代表当前模块的引用.因此module.exports和exports对象是一样的.module实际上不是一个全局变量,但是内置于每一个模块中.
module.exports
exports对象是被模块机制创建的,有时候,许多情况需要让模块成为一些类的一个实例,而使exports不可访问,为了把渴望导出的对象分配给moduel.exports对象,在例子中假设我们需要编写一个a.js模块.
var EventEmitter = require('events').EventEmitter;

module.exports = new EventEmitter();

// Do some work, and after some time emit
// the 'ready' event from the module itself.
setTimeout(function() {
  module.exports.emit('ready');
}, 1000);
在另一个文件里我们需要做:
var a = require('./a');
a.on('ready', function() {
  console.log('module a is ready');
});
注意,这种分配对象到module.exports对象的操作一定要立刻执行,如果放在任何一个回调函数里面,将不会生效.

x.js
setTimeout(function() {
  module.exports = { a: "hello" };
}, 0);
y.js
var x = require('./x');
console.log(x.a);

module.require(id)
  • id String
  • return:从解析出的模块中导出的对象.
module.require()方法提供了一种方式去加载一个在之前已经加载过的模块.
注意,为了实现这种方式,你必须获得一个这个模块的索引,自从require()方法返回了exports对象之后,这个模块只是在这个一段指定的代码里有效,为了使用你需要要明确的导出他.
module.id
  • String
模块的标识符,是一个能代表模块的完整路径
module.filename
  • String
一个能代表模块的完整路径
module.loaded
  • boolean
代表这个模块是加载完成还是正在加载中.
module.parent
  • module object
代表加载当前模块的模块.
module.children
  • Array
代表当前模块加载的子模块.
总结(All together...)
为了在调用require()时获得准确的文件名被加载,使用require.resolve()方法.
上面的代码放在一起,下面是require.resolve()方法的高级算法的伪代码:
require(X) from module at path Y
1. If X is a core module,
   a. return the core module
   b. STOP
2. If X begins with './' or '/' or '../'
   a. LOAD_AS_FILE(Y + X)
   b. LOAD_AS_DIRECTORY(Y + X)
3. LOAD_NODE_MODULES(X, dirname(Y))
4. THROW "not found"

LOAD_AS_FILE(X)
1. If X is a file, load X as JavaScript text.  STOP
2. If X.js is a file, load X.js as JavaScript text.  STOP
3. If X.node is a file, load X.node as binary addon.  STOP

LOAD_AS_DIRECTORY(X)
1. If X/package.json is a file,
   a. Parse X/package.json, and look for "main" field.
   b. let M = X + (json main field)
   c. LOAD_AS_FILE(M)
2. If X/index.js is a file, load X/index.js as JavaScript text.  STOP
3. If X/index.node is a file, load X/index.node as binary addon.  STOP

LOAD_NODE_MODULES(X, START)
1. let DIRS=NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
   a. LOAD_AS_FILE(DIR/X)
   b. LOAD_AS_DIRECTORY(DIR/X)

NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let ROOT = index of first instance of "node_modules" in PARTS, or 0
3. let I = count of PARTS - 1
4. let DIRS = []
5. while I > ROOT,
   a. if PARTS[I] = "node_modules" CONTINUE
   c. DIR = path join(PARTS[0 .. I] + "node_modules")
   b. DIRS = DIRS + DIR
   c. let I = I - 1
6. return DIRS
从全局文件夹加载模块(loading from global folders)
如果NODE_PATH环境变量被设置成一个以逗号分割的绝对路径列表,node如果没有在其他地方找到模块将会搜索这些路径,(注意:在windows中NODE_PATH是以分好为分隔符.)
另外,node将会搜索一下路径:
1: $HOME/.node_modules
2: $HOME/.node_libraries
3: $PREFIX/lib/node
$HOME是用户的home目录,$PREFIX是node配置的node_prefix.

这些基本上都是一些历史遗留问题.建议你把你的依赖放进node_modules文件夹中,这样的话,加载更快,更可靠.

访问主模块(Accessing the main module)

当一个文件是直接从node执行的,那么require.main变量会设置成module对象,这意味着你可以在测试时判断一个模块是否是直接运行的.
require.main === module

例如foo.js,如果通过node foo.js运行,将会返回true,如果通过require('./foo'),将会返回false.
因为module提供了一个filename对象,(正常情况下和_filename等价),当前程序的入口点可以通过require.main.filename获得.
附录:包管理建议(Package Manager Tips)
理论上,require()方法被设计成一般情况下,可以满足加载若干合理的文件结构目录.包管理程序例如:dbkg,rpm,and npm,在没有修改的情况下,是可以从node Modules中加载,建立本地packages.
下面我们给出了一些建议的目录结构:

比方说,我们想在目录:/user/lib/node/<some-package>/<some-version>下保存一个包的指定版本,

packages可以依赖另一个,为了安装包foo,你可能不得不安装bar包的一个指定版本.而且bar包可能还有自己的一些依赖,在某种情况下,这些依赖可能有冲突或者形成环形加载.
自从node查找它加载的每一个模块的真是路径,然后就像上面描述的那样,在node_modules中查找他们的依赖,用下面的这种结构,这种情况很简单就可以解决:
/usr/lib/node/foo/1.2.3/ - Contents of the foo package, version 1.2.3.
/usr/lib/node/bar/4.3.2/ - Contents of the bar package that foo depends on.
/usr/lib/node/foo/1.2.3/node_modules/bar - Symbolic link to /usr/lib/node/bar/4.3.2/.
/usr/lib/node/bar/4.3.2/node_modules/* - Symbolic links to the packages that bar depends on.

因此,即使发生环形加载,或者依赖冲突,每个模块都能够获得一个他们能够使用的版本.
挡在foo包中执行require('bar'),将会通过解析出来的路径连接字符/user/lib/node/foo/1.2.3/node_modules/bar获得指定版本的bar.然后,当bar包中执行require('quux')时,将会通过解析出来的路径连接字符/user/lib/node/bar/4.3.2/node_modules/quux获得指定版本的quux.
此外,为了让模块的查找过程更理想,不要直接把包放在/usr/lib/node下,我们可以把他们放在/usr/lib/node_modules/<name>/<version>下.这样node就可以不用麻烦的在/usr/node_modules/或者/node_modules下查找找不到的包.
为了确保模块在repl中是可见的,将/usr/lib/node_modules添加到$NODE_PATH环境变量中可能会起到效果.因为模块查到用到的其他模块全部是相对的,并且require()的加载也是基于真实路径的,所以,包可以放在任何地方.


分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics