模块系统
NodeJS Modules
module.exports & exports
我们可以将module.exports
看做require()
的返回值,默认情况下是一个空对象,并且可以被设置为任意的值。而exports
则是对于module.exports
的引用,来减少开发者的代码量。其基本用法如下所示
exports.method = function() {…}
module.exports.method = function() {…}
在具体的使用中,譬如我们在calculator.js
文件中添加了add
函数,在需要将其导出时,可以进行如下导出
// calculator.js
module.exports.add = (a,b) => a+b
而在使用时
// app-use-calculator.js
const calculator = require('./calculator.js')
console.log(calculator.add(2,2)) // prints 4
模块搜索顺序
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.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.json is a file, parse X/index.json to a JavaScript object. STOP
4. 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 I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
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
5. return DIRS
笔者之前在使用
Node 模块的缓存机制是大小写敏感的,譬如如果你require('/foo')
与require('/FOO')
会返回两个不同的对象,尽管你的foo 与FOO 是完全相同的文件。- 模块是基于其被解析得到的文件名进行缓存的,鉴于不同的模块会依赖于其被调用的路径进行缓存鉴别,因此并不能保证你使用
require('foo')
会永远返回相同的对象,可能会根据不同的文件路径得到不同的对象。
创建新的NodeJS 模块
根据
// counter.js
let value = 0
module.exports = {
increment: () => value++,
get: () => value,
}
在
// app.js
const counter = require(‘./counter.js’)
counter.increment()
counter.increment()
console.log(counter.get()) // prints 2
console.log(counter.value) // prints undefined as value is private
Module Caching
Every call to require(‘foo’) will get exactly the same object returned, if it would resolve to the same file.
我们也可以通过如下简单的例子来验证这句话
// app-singleton.js
const counter1 = require(‘./counter.js’)
const counter2 = require(‘./counter.js’)
counter1.increment()
counter1.increment()
counter2.increment()
console.log(counter1.get()) // prints 3
console.log(counter2.get()) // also prints 3
可以看出尽管我们两次导入了该模块,但是还是指向了同一个对象。不过并不是每次我们导入同一个模块时,都会得到相同的对象。在
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)
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...
4...
b. LOAD_AS_DIRECTORY(Y + 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 JS text. STOP
3...
4...
3. LOAD_NODE_MODULES(X, dirname(Y))
4. THROW "not found"
简单来说,加载的逻辑或者说优先级为
- 优先判断是不是核心模块
- 如果不是核心模块则搜索
node_modules - 否则在相对路径中进行搜索
解析之后的文件名可以根据
// counter-debug.js
console.log(module.filename) // prints absolute path to counter.js
console.log(__filename) // prints same as above
// i get: "/Users/laz/repos/medium/modules/counter-debug.js"
let value = 0
module.exports = {
increment: () => value++,
get: () => value,
在上述的例子中我们可以看出,解析得到的文件名即使被加载模块的绝对路径。而根据文件与模块一一映射的原则,我们可以得出下面两个会破坏模块导入单例性的特例。
Case Sensitivity
在大小写敏感的文件系统中或者操作系统中,不同的解析之后的文件可能会指向相同的文件,但是其缓存键名会不一致,即不同的导入会生成不同的对象。
// app-no-singleton-1.js
const counter1 = require('./counter.js')
const counter2 = require('./COUNTER.js')
counter1.increment()
console.log(counter1.get()) // prints 1
console.log(counter2.get()) // prints 0, not same object as counter1
/*
We have two different resolved filenames:
- “Users/laz/repos/medium/modules/counter.js”
- “Users/laz/repos/medium/modules/COUNTER.js”
*/
在上面的例子中,我们分别用counter
、COUNTER
这仅仅是大小写不同的方式导入相同的某个文件,如果是在某个大小写敏感的系统中,譬如
解析为不同的文件名
当我们使用require(x)
并且node_modules
文件夹。而在
// npm2 installed dependencies in nested way
app.js
package.json
node_modules/
|---module-a/index.js
|---module-b/index.js
|---node_modules
|---module-a/index.js
这样的话,我们对于同一个模块就有两个副本,那当我们在应用中导入
// app.js
const moduleA = require(‘module-a’)
loads: “/node_modules/module-a/index.js”
而从
// /node_modules/module-b/index.js
const moduleA = require(‘module-a’)
loads “/node_modules/module-b/node_modules/module-a/index.js”
不过在
// npm3 flattens secondary dependencies by installing in same folder
app.js
package.json
node_modules/
|---module-a/index.js
|---module-b/index.js
不过此时就存在另一个场景,即我们应用本身依赖 module-a@v1.1 与