Node.js从零开始-模块加载

上周过了一遍REPL,那接下去应该看看哪部分呢?既然知道怎么玩输入输出了,那就要开始了解模块部分的内容了。

Node.js有一个简单的模块加载系统,文件和模块一一对应。

foo.js

const circle = require('./circle.js')
console.log(`The area of a circle of radius 4 is ${circle.area(4)}`)

circle.js

const { PI } = Math
exports.area = (r) => PI * r ** 2
exports.circumference = (r) => 2 * PI * r

官方这个例子,circle.js暴露了areacircumference的方法,然后在foo.js被引入。

通过export关键字指定附加属性,把模块内的函数和对象暴露到根模块中。

而本地变量PIcircle.js的私有变量。类似这样的变量就会通过module wraper包裹起来,不被其他模块访问到。

export暴露接口,然后通过require('module')方式引用其他模块的函数和对象。

主入口

在node里直接执行一个文件,如$ node foo.jsrequire.main将会被设为当前的这个入口文件模块。这一点可以通过require.main === module来测试。

但如果是通过require('./foo.js')这种方式来引入的话,这个require.main则不会指向foo.js

module提供了filename属性,如果在当前REPL的环境中,我们可以通过require.main.filename获取到当前应用的入口文件名。

例如在REPL中我们建立的myeval.js里

$ node myeval.js
> require.main
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/crossing/Sites/nodejsfromzero/myeval.js',
loaded: true,
children: [],
paths:
[ '/Users/crossing/Sites/nodejsfromzero/node_modules',
'/Users/crossing/Sites/node_modules',
'/Users/crossing/node_modules',
'/Users/node_modules',
'/node_modules' ]
}

这里就能看到我对应执行的入口文件了。

包管理

文档这部分涉及到一些之前版本的包依赖管理的问题,例如引入foo模块的时候,我们可能需要引入bar模块,因为它被foo所依赖,而且有可能bar自身还依赖了其他模块,还有可能在这个子模块的依赖中的某个模块还依赖了不同版本的同一个模块,造成了冲突,甚至还依赖了foo模块,形成一个循环依赖。

都说node_module深不见底,比黑洞还深,这就是其中的缘由了。

不过这点随着Node.js引入了realpath这个概念之后就被简单的解决了,它会查找实际的路径,解析连接。具体的此处暂不深究。

require.resolve()

我们在使用require()获取X确切的文件名的时候,会用到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 '/'
a. set Y to be the filesystem root
3. If X begins with './' or '/' or '../'
a. LOAD_AS_FILE(Y + X)
b. LOAD_AS_DIRECTORY(Y + X)
4. LOAD_NODE_MODULES(X, dirname(Y))
5. 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_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, parse X/index.json to a JavaScript object. STOP
3. If X/index.node is a file, load X/index.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)
d. LOAD_INDEX(M)
2. LOAD_INDEX(X)
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
b. DIR = path join(PARTS[0 .. I] + "node_modules")
c. DIRS = DIRS + DIR
d. let I = I - 1
5. return DIRS

看完上面这个伪代码之后,我们也就大概知道了上面的那个require.main.paths里的那串到底咋来的了。

核心模块总会被优先加载。这个其实从上面的部分伪代码中也能看出来。

循环依赖,即便产生了循环依赖,那其实依然还是有依赖的先后情况的,就根据这个先后顺序,当依赖了未完成加载的,那就先先暴露unfinished copy,然后继续往下走,直到这个模块完成加载,以此类推,最后到主模块的时候这些子依赖肯定是完成了最终的加载了的。这里有点绕。

文件模块

文件夹模块

node_modules文件夹加载模块

在'/home/ry/projects/foo.js' 调用 require('bar.js')

然后查找顺序会是这样的

/home/ry/projects/node_modules/bar.js
/home/ry/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js

从全局文件夹中加载模块,更推荐local的node_module而不是全局,因为涉及到一个版本依赖的问题。没法保证全局的版本是否符合当前的需求。

模块包装

在模块代码被执行之前,`Node.js`将会把这个模块用下面的形式包装起来
(function(exports, require, module, __filename, __dirname) {
// Module code actually lives in here
});
这么做是为了限定变量作用范围,通过`__filename`和`__dirname`就能取到文件的绝对文件名和目录路径。

模块的作用范围

  • dirname 和 filename,在目录/usr/mjr下执行$ node example.js
console.log(__dirname);
// Prints: /Users/mjr
console.log(path.dirname(__filename));
// Prints: /Users/mjr
console.log(__filename);
// Prints: /Users/mjr/example.js

其实看完上面那段伪代码,基本下面的也都是些补充说明了。

官方文档确实很清晰明了,也让我明白了之前没想到的一些地方,之前只管用,没去想后面的实现逻辑。

所以我的思维还是有待提升的,需要学习的还有很多很多呀。

不能光去用,还得去想想为什么这么实现。

思考,才能让我进步更快。

REF

官方文档Module

坚持原创技术分享,您的支持将鼓励我继续创作!