nodejs 的模块加载系统是指文件和模块是一一对应的,每个文件均被视作一个模块
一.作用域
模块本地的变量将是私有的,除非挂载在exports方法上变成可引用变量,示例如下:
/----a.js----/
const { PI } = Math;
exports.area = (r) => PI * r ** 2;
exports.circumference = (r) => 2 * PI * r;
/----b.js----/
const circle = require('./a.js');
console.log(circle.area(2))//圆面积
console.log(circle.circumference(2))//圆周长
console.log(PI)//undefined
在这个例子中,变量PI是私有的
二.模块引用路径结构
一个可以工作的建议目录结构:
假设我们想让文件夹 /usr/lib/node//保持特定版本的包的内容。
包可以相互依赖。为了安装软件包foo,可能需要安装特定版本的软件包bar。该bar 包装本身可能有相关性,在某些情况下,这些甚至可能发生碰撞或形成循环依赖。
由于Node.js的中查找realpath它加载任何模块(也就是解决符号链接),然后寻找自己的依赖node_modules 所描述的文件夹位置,这种情况是非常简单的用下面的架构来解决:
/usr/lib/node/foo/1.2.3/- foo包的内容,版本1.2.3。
/usr/lib/node/bar/4.3.2/- 依赖于的bar包的内容foo。
/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程序包中的代码调用时require(‘quux’),它会得到符号链接到的版本 /usr/lib/node/bar/4.3.2/node_modules/quux。**
此外,为了使模块查找过程更加优化,而不是直接将包放入/usr/lib/node,我们可以将它们放入 /usr/lib/node_modules//。然后Node.js的不会刻意寻找缺少的依赖/usr/node_modules或/node_modules。
为了使模块对Node.js REPL可用,将该/usr/lib/node_modules文件夹添加到$NODE_PATH环境变量可能也是有用的。由于使用node_modules文件夹的模块查找都是相对的,并且根据调用的文件的实际路径 require(),软件包本身可以位于任何地方。
三.模块引用机制
1)核心模块
Node中有一些模块是编译成二进制的。这些模块在本文档的其他地方有更详细的描述。
核心模块定义在node源代码的lib/目录下。
require()总是会优先加载核心模块。例如,require(‘http’)总是返回编译好的HTTP模块,而不管是否有这个名字的文件。
2)文件模块
如果按文件名没有查找到,那么node会添加 .js和 .json后缀名,再尝试加载,如果还是没有找到,最后会加上.node的后缀名再次尝试加载。
.js 会被解析为Javascript纯文本文件,.json 会被解析为JSON格式的纯文本文件. .node 则会被解析为编译后的插件模块,由dlopen进行加载。
模块以’/’为前缀,则表示绝对路径。例如,require(‘/home/marco/foo.js’) ,加载的是/home/marco/foo.js这个文件。
模块以’./’为前缀,则路径是相对于调用require()的文件。 也就是说,circle.js必须和foo.js在同一目录下,require(‘./circle’)才能找到。
当没有以’/’或者’./’来指向一个文件时,这个模块要么是”核心模块”,要么就是从node_modules文件夹加载的。
如果指定的路径不存在,require()会抛出一个code属性为’MODULE_NOT_FOUND’的错误。
3)从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
这就要求程序员应尽量把依赖放在就近的位置,以防崩溃。
4) Folders as Modules
可以把程序和库放到一个单独的文件夹里,并提供单一入口来指向它。有三种方法,使一个文件夹可以作为require()的参数来加载。
首先是在文件夹的根目录创建一个叫做package.json的文件,它需要指定一个main模块。下面是一个package.json文件的示例。
{
"name" : "some-library",
"main" : "./lib/some-library.js"
}
示例中这个文件,如果是放在./some-library目录下面,那么require(‘./some-library’)就将会去加载./some-library/lib/some-library.js。
This is the extent of Node’s awareness of package.json files.
如果目录里没有package.json这个文件,那么node就会尝试去加载这个路径下的index.js或者index.node。例如,若上面例子中,没有package.json,那么require(‘./some-library’)就将尝试加载下面的文件:
./some-library/index.js
./some-library/index.node
四 缓存
模块在第一次加载后会被缓存。这意味着(类似其他缓存)每次调用require(‘foo’)的时候都会返回同一个对象,当然,必须是每次都解析到同一个文件。
多次调用 require(foo) 未必会导致模块中的代码执行多次. 这是一个重要的功能. 借助这个功能, 可以返回部分完成的对象; 这样, 传递依赖也能被加载, 即使它们可能导致循环依赖
如果你希望一个模块多次执行,那么就输出一个函数,然后调用这个函数。
注意:
模块基于他们解析的文件名被缓存。因为模块可以解析为基于调用模块(从加载的位置的不同的文件名node_modules的文件夹),它不是一个保证 这require(‘foo’)将始终返回完全相同的对象,如果它会解析到不同的文件。
此外,在不区分大小写的文件系统或操作系统,不同的解析文件名可以指向同一个文件,但缓存仍然将它们视为不同的模块,并将多次重新加载文件。例如, require(‘./foo’)和require(‘./FOO’)返回两个不同的对象,而不论是否./foo和./FOO是相同的文件。
五 周期
当有循环require()调用时,模块在返回时可能没有完成执行
a.js:
console.log('a starting');
exports.done = false;
const 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;
const 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');
const a = require('./a.js');
const 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。为了防止无限循环,将导出对象的未完成副本a.js返回给 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
六.名词解释
__dirname 当前模块的目录名称 /Users/mjr
__filename 当前模块的文件名。这是当前模块文件解析的绝对路径。/Users/mjr/example.js
module.exports 出口
require()
require.cache 当需要时,模块被缓存在这个对象中。通过从这个对象中删除一个键值,下一个require将重新加载模块。请注意,这不适用于本地插件,重装会导致错误。
module.id 模块的标识符。通常这是完全解决的文件名
module.loaded 无论模块是否完成加载,或正在加载过程中。
module.parent 需要这个的模块
module.paths 模块的搜索路径。