Node.js API —— Modules(模块)

你离开我真会死。 提交于 2020-03-30 03:18:34
// 说明
    Node API 版本为 v0.10.31。
    中文参考:http://nodeapi.ucdok.com/#/api/http://blog.sina.com.cn/oleoneoy
    本段为博主注解。

目录

模块
    ○ Cycles
    ○ Core Modules
    ○ File Modules
    ○ Loading from node_modules Folders
    ○ Folders as Modules
    ○ Caching
        ■ Module Caching Caveats
    ○ The module Object
        ■ module.exports
            ■ exports.alias
        ■ module.require(id)
        ■ module.id
        ■ module.filename
        ■ module.loaded
        ■ module.parent
        ■ module.children
    ○ All Together...
    ○ Loading from the global folders
    ○ Accessing the main module
    ○ Addenda: Package Manager Tips

 模块

  稳定性:5 - 锁定
    Node 有一个简单的模块加载系统。在 Node 里面,文件和模块是一一对应的。举个例子,foo.js 加载同一目录下的模块 circle.js
    foo.js 的内容如下:
1 var circle = require('./circle.js');
2 console.log('The area of a circle of radius 4 is '
3   + circle.area(4));

   circle.js 的内容如下:

1 var PI = Math.PI;
2 
3 exports.area = function(r) {
4   return PI * r * r;
5 };
6 
7 exports.circumference = function(r) {
8   return 2 * PI * r;
9 };

    模块 circle.js 导出了函数 area()circumference()。想把函数和对象添加到你的模块的根节点,你可以把它们添加到特殊的 exports 对象中。
    模块的局部变量是私有的,好像模块被一个函数包裹起来一样。在这个例子里面,变量 PIcircle.js 私有的。
    如果你想你的模块的导出的根节点是一个函数(例如是一个构造函数)或者如果你想在一次指派就能导出一个完整的对象而不是每次都新建一个属性,把它指派到 module.exports 而不是 exports 中。
    在下面,bar.js 使用了一个导出一个构造函数的 square 模块:

1 var square = require('./square.js');
2 var mySquare = square(2);
3 console.log('The area of my square is ' + mySquare.area());

    square 模块定义在 square.js 中:

1 // assigning to exports will not modify module, must use module.exports
2 module.exports = function(width) {
3   return {
4     area: function() {
5       return width * width;
6     }
7   };
8 }

    模块系统的实现在 require("module") 模块中。

Cycles(循环引用)

    当有循环的 require() 引用时,一个模块可能返回时还没被执行。
    考虑这样的情形:
    a.js :
1 console.log('a starting');
2 exports.done = false;
3 var b = require('./b.js');
4 console.log('in a, b.done = %j', b.done);
5 exports.done = true;
6 console.log('a done');

    b.js :

1 console.log('b starting');
2 exports.done = false;
3 var a = require('./a.js');
4 console.log('in b, a.done = %j', a.done);
5 exports.done = true;
6 console.log('b done');

   main.js :

1 console.log('main starting');
2 var a = require('./a.js');
3 var b = require('./b.js');
4 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。为了防止无限的循环,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

    如果你在你的程序中有模块的循环依赖,确保有依据地组织。

Core Modules(核心模块)

    Node 有若干个编译成二进制的模块。这些模块在本文档其他地方更加详细地描述。
    核心模块定义在 Node 的源文件 lib/ 目录下。
    如果核心模块的模块标识符被传入 require() ,它们总是优先加载。举个例子, require('http') 将总是返回内建的 HTTP 模块,即使有一个同名的文件。

File Modules(文件模块)

    如果精确的文件名没有找到文件,node 会试图以 .js.json.node 的顺序给引用的文件名添加后缀并加载它。
    .js 文件被作为 JavaScript 文本文件解析,.json 文件被当成 JSON 文本文件解析,.node 文件当成编译好的二进制 addon 模块解析being使用 dlopen 加载。
    模块以 '/' 为前缀表示为文件的绝对路径。例如,require('/home/marco/foo.js') 将加载文件 /home/marco/foo.js
    模块以 './' 为前缀表示为相对于调用 require() 的文件的相对路径。也就是说,为了 require('./circle') 能找到它,circle.js 必须和 foo.js 在同一目录下。
    没有前缀 '/' 和 './' 来表示是一个文件,该模块会被认为或者是一个“核心模块”或者是从 node_modules 目录加载的模块。
    如果给出的路径不存在,require() 将抛出一个 code 属性设置为 'MODULE_NOT_FOUND' 的错误。

Loading from node_modules Folders(从 node_modules 文件夹加载)

    如果传递给 require() 的模块标识符不是一个核心模块,并且也不是以 '/''../' 或 './' 开头,那么 node 将从当前模块的父目录开始,拼接上 /node_modules,并尝试从那个位置加载模块。
    如果在找不到,那么将移到父目录,依次类推,知道到达根目录。
    例如,如果文件 '/home/ry/projects/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
    这能允许程序局部化它们的依赖,不至于产生冲突。

Folders as Modules(文件夹作为模块)

    将程序和库组织成自包含的目录,然后为库提供一个简单的入口点是很方便的。有三个途径可令文件夹可以作为参数传进 require() 中。
    第一种是在文件夹的根目录创建一个指定 main 模块的 package.json 文件。一个 package.json 的例子如下:
1 { "name": "some-library",
2   "main": "./lib/some-library.js" }

    如果 package.json 文件在 ./some-library 文件夹中,那么 require('./some-library') 将会尝试加载 ./some-library/lib/some-library.js
    这是 Node 能感知 package.json 文件的程度。
    如果在文件夹中没有 package.json 文件,那么 node 将尝试加载 index.jsindex.node 文件。例如,如果在上述例子中没有 package.json 文件,那么 require('./some-library') 将尝试加载:

  • ./some-library/index.js
  • ./some-library/index.node

Caching(缓存)

    模块在第一次加载之后会被缓存起来。这意味着(除了其它例外)如果解析到同一个文件,每次调用 require('foo') 都会返回同一个对象。
    多次调用 require('foo') 不会引起模块代码被执行多次。这是个很重要的特性。有了它,“部分完成”的对象才能被返回,因此允许依赖能被及时加载,即使它们会引起循环引用。
    如果你想一个模块会执行多次代码,那么导出一个函数,并调用那个函数。

Module Caching Caveats(模块缓存警告)

    模块是根据它们解析过的文件名来缓存的。因为模块可以根据调用模块的位置(从 node_modules 文件夹加载)解析成不同的文件名,所以无法保证 require('foo') 总是返回同一个对象。如果解析成不同的文件,则返回的是不同的对象。

The module Object(module 对象)

    ● {对象类型}
    在每个模块中,变量 module 是指向代表当前模块的对象的引用。为了更方便,module.exports 也可以通过模块的全局变量 exports 获得。module 不是真正的全局变量而是每个模块都有的局部变量。

module.exports

    ● {对象类型}
    module.exports 对象被模块系统创建。有时候这是无法接受的;很多人想把他们的模块作为某些类的实例。为了这个目的需要将希望得到的对象指派给 module.exports。注意将希望得到的对象指派给 exports 将只是简单地改变局部变量 exports 的引用,这很可能不是你想要的实现的。
    例如假设我们创建一个名叫 a.js 的模块
1 var EventEmitter = require('events').EventEmitter;
2 
3 module.exports = new EventEmitter();
4 
5 // Do some work, and after some time emit
6 // the 'ready' event from the module itself.
7 setTimeout(function() {
8   module.exports.emit('ready');
9 }, 1000);

    然后在另外一个文件我们可以

1 var a = require('./a');
2 a.on('ready', function() {
3   console.log('module a is ready');
4 });

   注意给 module.exports 赋值立即完成,不能在回调函数中完成。下面的方式不会正常工作:
    x.js:

1 setTimeout(function() {
2   module.exports = {a: "hello" };
3 }, 0);

   y.js

1 var x = require('./x');
2 console.log(x.a);

 exports alias(exports 别名)

    exports 变量在模块内都是可用的,并被初始化成指向 module.exports 的引用。如同任何其他的变量,如果你赋一个新值给它,它将不再指向之前的值。
    为了阐述这种行为,假设 require() 是这样实现的:
 1 function require(...) {
 2   // ...
 3   function(module, exports) {
 4     // Your module code here
 5     exports = some_func;             // re-assigns exports, exports is no longer
 6                                                  // a shortcut, and nothing is exported.
 7     module.exports = some_func;  // makes your module exports 0
 8   } (module, module.exports);
 9   return module;
10 }

    作为参考,如果 exportsmodule.exports 之间的关系对你来说想变魔术一样,忽略 exports 只使用 module.exports

module.require(id)

    ● id 字符串
    ● 返回值: 对象 解析出来的模块的 module.exports
    module.require 方法提供一个加载模块的途径使得就好像 require() 是在原始的模块被调用的一样。
    注意为了这样做,你必须得到一个 module 对象的引用。由于 require() 返回 module.exports,且 module 通常只能在特定的模块代码里被访问,因此为了使用它必须明确地导出。

module.id

     ● 字符串
   模块的模块标识。典型地这是绝对路径文件名。

module.filename

    ● 字符串
   模块的绝对路径名。

module.loaded

    ● 字符串
   模块是已经完成加载,还是还在加载中。

module.parent

    ● 模块对象
   引用本模块的模块。

module.children

    ● 数组
   本模块引用的模块。

 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, direname(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.json is a file, parse X.json to a JavaScript Object.  STOP
4. 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
   b. DIR = path join(PARTS[0 .. I] + "node_modules")
   c. let I = I - 1
6. return DIRS

 Loading from the global folders(从全局文件夹加载)

    如果 NODE_PATH 环境变量设置成以冒号分隔的绝对路径列表,那么如果在其他地方都找不到的话, node 将会从那些路径下查找模块。(注意:在 Windows 下,NODE_PATH 被分号分隔而不是冒号。)
    另外,node 会查找一下的位置:
  • 1: $HOME/.node_modules
  • 2: $HOME/.node_libraries
  • 3: $PREFIX/lib/node
    $HOME 是用户的主目录,$PREFIX 是 node 配置的 node_prefix
    这些主要是因为历史的原因。强烈推荐你将你的依赖放置在本地 node_modules 文件夹。它们会被加载得更快,更可靠。

Accessing the main module(访问主模块)

   当一个文件直接从 Node 启动,require.main 会设置成它的 module。那意味着你可以如下方式判断一个文件是否被直接运行
1 require.main === module

    对应文件 foo.js,如果通过 node foo.js 运行该值将是 true,但如果通过 require('./foo') 运行给支将是 false
    因为 module 提供了一个 filename 属性(通常和 __filename相等),当前应用程序的入口点可以通过检查 require.main.filename 得到。

Addenda: Package Manager Tips(附录:包管理技巧)

    Node 的 require() 函数的语义被设计得足够通用化来支持各种常规的目录结构。例如 dpkgrpmnpm 这些包管理程序将很有可能发现不需修改就可以从 Node 模块创建本地的包。
    下面我们给出一个可行的推荐目录结构:
    假设我们想在 /usr/lib/node/<some-package>/<some-version> 有一个文件夹放置一个包的特定版本的内容。
    包可能依赖其它包。为了安装包 foo,你可能需要安装包 bar 的一个特定的版本。包 bar 可能自己也有依赖,在某些情况下,这些依赖设置可能冲突或形成循环。
    由于 Node 查找任何模块加载时的真实路径( 也就是说,解析成符号链接),然后查找它们在 node_modules 文件夹中如上所述的依赖,这种情况对使用如下结构解析非常简单:
  • /usr/lib/node/foo/1.2.3/ —— foo 包1.2.3版本的内容。
  • /usr/lib/node/bar/4.3.2/ —— foo 包依赖的 bar 包内容。
  • /usr/lib/node/foo/1.2.3/node_modules/bar —— 指向 /usr/lib/node/bar/4.3.2 的符号链接。
  • /usr/lib/node/bar/4.3.2/node_modules/* —— bar 包依赖的包的符号链接。
    因此,即使遇到循环依赖,或依赖冲突,每个模块都能得到它所依赖且可用的版本。
    当 foo 包中的代码 require('bar'),它将得到符号链接指向 /usr/lib/node/foo/1.2.3/node_modules/bar 的版本。然后,当 bar 包中的代码调用 requier('quux'),它将得到符号连接指向 /usr/lib/node/bar/4.3.2/node_modules/quux 的版本。
    此外,为了使模块搜索过程更加理想,我们可以将包放到 /usr/lib/node_modules/<name>/<version> 而不是直接放到 /usr/lib/node。那么 node 将不会烦恼于查找 /usr/node_modules/node_modules 中不存在的依赖。
    为了使模块能被 node REPL 使用,把 /usr/lib/node_modules 文件夹也添加到 $NODE_PATH 环境变量也很有用。因为模块查找使用的 node_modules 文件夹都是相对,且基于调用 require() 的文件的真实路径,包本身可能在任何地方。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!