Node.js有一个简单的模块加载系统。 在Node.js中,文件和模块是一一对应的(每个文件被视为单独的模块)。
例如,考虑下面这个名为 foo.js 的文件:
const circle = require('./circle.js'); console.log(`The area of a circle of radius 4 is ${circle.area(4)}`);
在第一行, foo.js 加载与 foo.js 同一目录的模块 circle.js 。
circle.js 的内容如下:
const PI = Math.PI; exports.area = (r) => PI * r * r; exports.circumference = (r) => 2* PI * r;
模块 circle.js 导出了函数 area() 和 circumference() 。 要将函数和对象添加到模块的根目录,可以将它们赋值到特殊 exports 对象上。
模块内部的变量一定是私有的,因为模块被Node.js包裹在一个函数中(参见下面的模块包装器)。 在这个例子中,变量 PI 对于 circle.js 来说是私有变量。
如果你希望模块导出的是一个函数(如构造函数),或者是要导出完整的对象,而不是一次创建一个属性,则需要将其分配给 module.exports 而不是 exports 。
在下面的 bar.js 中,使用了 square 模块,它导出一个构造函数:
const square = require('./square.js'); var mySquare = square(2); console.log(`The area of my square is ${mySquare.area()}`);
在 square.js 模块中定义一个 square 方法:
module.exports = (width) => { return { area: () => width * width; }; }
此外,模块系统在 require(“module”) 模块中实现。
『main』模块
当某个 module 直接从Node.js运行时,它会将 require.main 设置该 module 。 你可以通过这个来测试这个 module 是被直接运行的还是被 require 的。
require.main === module
就拿文件 foo.js 来说,如果运行 node foo.js 这个属性就是 true 。运行 require('./foo.js') 就是 false 。
因为 module 提供了一个 filename (通常相当于 __filename ),因此可以通过检查 require.main.filename 来获取当前应用程序的入口点。
包管理器的一些提示
Node.js的 require() 函数支持一些合理的目录结构。它让软件包管理器程序(如 dpkg , rpm 和 npm )可以从Node.js模块中直接去构建本地的包而不需要修改。
下面我们给出一个可以正常工作的建议目录结构:
假设我们希望在 /usr/lib/node/<some-package>/<some-version> 中的文件夹来指定版本的包。
此外,包还可以相互依赖。 比如你想安装 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/ - 包含 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 包中的代码调用 require('quux') 时,它同样也可以软链到指定版本的 /usr/lib/node/bar/4.3.2/node_modules/quux 。
模块加载的全过程(重点,下面写的伪代码流程一定要记住)
要获取在调用 require() 将被加载的确切文件名,请使用 require.resolve() 函数。
以下是模块加载的全过程以及 require.resolve 的解析过程:
// 加载X模块 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 'https://www.jb51.net/' or '../' a. LOAD_AS_FILE(Y + X) b. LOAD_AS_DIRECTORY(Y + X) 3. LOAD_NODE_MODULES(X, dirname(Y)) 4. THROW "not found" // 加载X文件 // 加载过程:X -> X.js -> X.json -> X.node 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, load [X.json] as JavaScript text. STOP 4. If [X.node] is a file, load [X.node] as JavaScript text. STOP // 加载入口文件 // 加载过程:X -> X/index.js -> X/index.json -> X/index.node LOAD_INDEX(X) 1. If [X/index.js] is a file, load [X/index.js] as JavaScript text. STOP 2. If [X/index.json] is a file, load [X/index.json] as JavaScript text. STOP 3. If [X/index.node] if a file, load [X/index.node] as JavaScript text. 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) d. LOAD_INDEX(M) 2. LOAD_INDEX(X) // 加载node模块 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路径 NODE_MODULES_PATHS(START) 1. let PARTS = path split(START); 2. let I = count of PARTS - 1 3. let DIRS = [] 4. while I > 0 a. If PARTS[I] = "node_modules" CONTINUE b. DIR = path join(PARTS[0 ... I] + "node_modules") c. DIRS = DIRS + DIR d. let I = I -1 5. return DIRS
模块缓存
所有的模块都会在第一次加载之后被缓存起来。 这意味着你每次调用 require('foo') 将得到完全相同的对象。