两个核心模块 requiremodule

require 模块对应全局的 require 方法。module 模块对应每一个模块全局空间中的 module 属性。

Node.js 加载一个模块主要经历以下几个步骤:

Resolving -> Loading -> Wrapping -> Evaluating -> Caching

Resolving

Node.js 中的模块和文件系统中的文件是一一对应的(这一点很重要)。加载模块的过程其实就是执行文件系统中的脚本并将结果载入内存的过程。

每一个模块都有一个 id 属性,该属性的值就是这个模块对应文件的绝对路径(在 REPL 里为 ““)。

Resolving 阶段的工作就是把我们 require 的字符串解析成一个文件系统中的绝对路径。根据我们 require 包的类型,这里又分为三种情况:

上面主要讲了 .js 文件的加载流程,其实 require() 不仅仅可以加载 .js 文件。require.extensions 属性中包含了 require() 支持的所有扩展名:

.json 对应普通的 JSON 文件,.node 对应编译好的 C++ 扩展。

如果你只是想查找摸个模块对应文件的绝对路径而不加载它,Node.js 为我们提供了 require.resolve() 方法。该方法会返回与某个模块对应的文件的绝对路径,如果该模块不存在,则抛出 Cannot find module 异常。我们可以利用该方法判断项目中是否安装了某个依赖。

Loading

找到模块对应的文件后,下一步就是加载这个模块的内容。对应不同的文件类型,Node.js 也有不同的处理方式:

可以看出:

Wrapping

Wrapping 阶段主要实现两个功能:

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 加载模块的大致流程,如果想了解更多细节可以查看相关源码

参考链接: