两个核心模块 require
和 module
。
require
模块对应全局的 require 方法。module
模块对应每一个模块全局空间中的 module 属性。
Node.js 加载一个模块主要经历以下几个步骤:
Resolving -> Loading -> Wrapping -> Evaluating -> Caching
Resolving
Node.js 中的模块和文件系统中的文件是一一对应的(这一点很重要)。加载模块的过程其实就是执行文件系统中的脚本并将结果载入内存的过程。
每一个模块都有一个 id
属性,该属性的值就是这个模块对应文件的绝对路径(在 REPL 里为 “
Resolving 阶段的工作就是把我们 require 的字符串解析成一个文件系统中的绝对路径。根据我们 require 包的类型,这里又分为三种情况:
- 核心模块。也即 Node.js 内置的模块,例如 “fs”、”path”、”http” 等,这类模块无需安装即可直接使用。
- 相对路径/绝对路径。Node.js 直接将相对路径转换成对应的绝对路径。
第三方依赖。如果不是前两种情况,那么 Node.js 会依次查找
module.paths
列表中的目录是否存在。 我们来看看module.paths
中都有哪些目录: 可以看到主要是从当前目录逐级向上查找node_modules
目录。这也就是为什么我们的依赖会被安装在node_modules
目录下的原因。 为了向前兼容,Node.js 还会检查一些已经被废弃的目录,不推荐使用它们。在找到这个列表中某个存在的目录之后,Node.js 会在该目录下继续查找,假设我们执行的是
require("moduleA")
,那么又可以分为以下三种情况:- 存在一个
moduleA.js
文件,那么该文件就是最终我们要加载的文件。 - 存在一个
moduleA
子目录,且该目录下存在一个名为index.js
的文件。则该index.js
文件为我们最终加载的文件。 - 存在一个
moduleA
子目录,且该目录下存在一个package.json
文件,该 JSON 对象上存在一个main
属性,那么这个main
属性的值就是最终加载的文件的路径。
- 存在一个
上面主要讲了 .js
文件的加载流程,其实 require()
不仅仅可以加载 .js
文件。require.extensions
属性中包含了 require()
支持的所有扩展名:
.json
对应普通的 JSON 文件,.node
对应编译好的 C++ 扩展。
如果你只是想查找摸个模块对应文件的绝对路径而不加载它,Node.js 为我们提供了 require.resolve()
方法。该方法会返回与某个模块对应的文件的绝对路径,如果该模块不存在,则抛出 Cannot find module
异常。我们可以利用该方法判断项目中是否安装了某个依赖。
Loading
找到模块对应的文件后,下一步就是加载这个模块的内容。对应不同的文件类型,Node.js 也有不同的处理方式:
可以看出:
- 针对
.js
文件,Node.js 会读取该文件的文本内容并执行module._compile
。 - 针对
.json
文件,Node.js 则会读取文本内容然后执行JSON.parse()
将其解析成 JSON 对象。 - 针对
.node
文件。Node.js 会执行process.dlopen()
来处理。
Wrapping
Wrapping 阶段主要实现两个功能:
- 为每个模块提供各自隔离的作用域
- 为每个模块注入与外界通信的桥梁(
require
/module
/exports
)以及模块的自身信息(__filename
和__dirname
)
Node.js 的具体做法是把我们模块的代码包裹在一个函数内,需要注入的信息则作为函数参数传递进来:
Module.wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
Module.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
Evaluating
这一步其实就是执行 warp 出来的函数,传入适当的参数。执行完成后,module.exports
上就是这个模块要对外部暴露的内容了。
这里只介绍 1 个点,那就是我们如何区分我们的模块是被其它模块 require()
还是被用户在命令行中以 Node.js 脚本的方式执行的呢?
举个例子,假设我们想编写一个 hello.js 模块,要求它既支持 node hello.js world
这样在 CLI 中调用,又支持作为依赖被其它模块 require
。这时就要用到 require.main
属性了。
如果一个 .js
文件是直接被 Node.js 执行的,那么 require.main
的值就会被设置成(参见这里和这里)它自己的 module
。换句话说,我们在模块中判断表达式 require.main === module
的值来进行区别处理。
我们的 hello.js
就可以写成如下的样子:
const hello = (word) => {
console.log(`hello ${word}`);
};
if (require.main === module) {
hello(process.argv[2]);
} else {
module.exports = hello;
}
Caching
最后一步就是将加载完的模块缓存起来,当下次再一次 require 该模块时 Node.js 会直接返回缓存中的模块。
Node.js 会将模块缓存在 require.cache
对象上。
总结
以上就是 Node.js 加载模块的大致流程,如果想了解更多细节可以查看相关源码。
参考链接: