# 模块机制:

在 Node.js 模块系统中,每个文件都被视为独立的模块。这个文件可能是 JavaScript 代码、JSON 或者编译过的 C/C++ 扩展

( 比如 http 是 Node.js 的一个核心模块,其内部是用 C++ 实现的,外部用 JavaScript 封装。)

通过使用模块机制, 我们可以把一个复杂程序各个功能拆分, 分别封装到不同的文件. 在需要的时候引入相关的模块. 这样做可以让代码的可读性, 复用性, 和易维护性够变得更高.

Node.js 的模块机制实现参照了 CommonJS 标准.

# 创建 & 加载模块:

Node.js 使用 exports 对象 和 require 方法来管理模块依赖.

  • exports 对象 指定一个模块所导出的内容

  • require 方法 引入外界模块到当前文件.

// myModule.js
var name = '';

exports.setName = function(setName) {
    name = setName;
}

exports.getName = function() {
    return name;
}
// getModule.js
var myModule = require('./myModule');

myModule.setName("Garrik");

console.log(myModule.getName());

在以上示例中,myModule.js 通过在 exports 对象指定 setName 和 getName 这两个方法, 两个匿名函数被导出了.

在 getmodule.js 中通过 require('./myModule')加载这个模块,然后就可以直接访问 myModule.js 中定义在 exports 对象里的函数了。

# 深入研究:

require 源码:

// Loads a module at the given file path. Returns that module's
// `exports` property.
Module.prototype.require = function(path) {
  assert(path,'missing path');
  assert(typeof path ==='string','path must be a string');
  return Module._load(path, this);
};

require() 函数是 Module对象 原型上的一个方法.

接收一个路径 (path) 作为参数, assert 模块进行简单的 path 变量的判断,需要传人的 path 是一个 string 类型。返回引入模块的 module.exports 对象

模块大概可以分成: 

  • 核心模块 (本身自带)
  • 文件模块 (自己编写的本地模块)
  • 第三方模块 (通过包管理器安装的)

模块文件的后缀 ( 例如: .js, .json, .node) 可以省略.

当它执行的时候, 经历下面五个步骤:

  • Resolving: 找到文件的绝对路径;

  • Loading: 判断文件内容类型;

  • Wrapping: 打包,给这个文件赋予一个私有作用范围

  • Evaluating: VM 对加载的代码进行处理的地方;

  • Caching: 缓存, 当再次需要用这个文件的时候,不需要重复一遍上面步骤。

# Resolving - 解析路径:

在每个模块中都有一个 module对象 (Module 的实例) 作为当前模块的引用.

module 对象可能看起来像下面这样:

Module {
  // 模块的标识符。 通常是完全解析后的文件名。
  id: '.',
  // 
  exports: { name: 'Garrik' },
  // 最先引用该模块的模块
  parent: null,
  // 模块的完全解析后的文件名
  filename: '/Users/xiangliu/Desktop/testNode/myModules/myName.js',
  // 模块是否已经加载完成,或正在加载中
  loaded: false,
  // 被该模块引用的模块对象
  children: [],
  // 模块的搜索路径
  paths:
   [ '/Users/xiangliu/Desktop/testNode/myModules/node_modules',
     '/Users/xiangliu/Desktop/testNode/node_modules',
     '/Users/xiangliu/Desktop/node_modules',
     '/Users/xiangliu/node_modules',
     '/Users/node_modules',
     '/node_modules' ] }

每一个模块都有一个唯一的 id 属性来标示它。id 通常是文件的完整绝对路径.

Node 模块和文件系统中的文件通常是一一对应的,引入一个模块需要把文件内容加载到内存中。

# 路径参数:

在用require()引入模块时, 路径参数可能有下面三种形式:

  • 相对路径: ./开头 或 ../开头
  • 绝对路径: /开头
  • 模块名 (例如: http, fs, url)

# 无路径, 直接模块名:

如果我引入了一个 haha 模块,并没有指定它的路径的话:

var haha = require('haha');

Node 首先会去 /lib 目录下查找, 看 haha 是否是一个 Node 核心模块.

Node 会按照 module.paths 所指定的文件目录顺序依次寻找 haha 的所在。若有两个同名文件,则遵循就近原则。优先引入目录顺序靠前的模块.

模块不一定非要是文件,也可以是个文件夹。我们可以在 node_modules 中创建一个 haha 文件夹,并且放一个 index.js 文件在其中。那么执行 require('haha') 将会默认使用 index.js 文件.

也可以手动控制指定到其他文件,修改 package.jsonmain 属性就可以。

# 有路径:

如果指定了模块的绝对路径, 或者相对路径.

例如:

var haha = require('./lib/haha');

因为用 require 来加载文件时可以省略扩展名, 所以在加载的时候, Node 会猜测文件的类型.

加载顺序为:

  • 按 js 文件来执行(先找对应路径当中是否有 haha.js 文件, 有就加载)
  • 按 json 文件来解析(若上面的 js 文件找不到时,则找对应路径当中的 haha.json 文件来加载)
  • 按照预编译好的 c++模块 来执行(还没有, 寻找对应路径当中的 haha.node 文件来加载)
  • 若参数字符串为一个目录(文件夹)的路径, 则自动先查找该文件夹下的 package.json 文件,然后再加载该文件当中 main字段 所指定的入口文件。(若 package.json 文件当中没有 main字段,或者根本没有 package.json 文件,则再默认查找该文件夹下的 index.js 文件, 并作为模块来载入。)
  • 要是还没有就玩蛋去吧!

# Loading 判断文件内容类型:

# Wrapping 打包:

很明显, 我们不能访问被引入模块内除module.exports 之外的东西.

这是因为在执行模块代码之前,Node.js 会使用一个如下的函数包装器将其包装, 可以用 module模块 的 wrapper 属性来查看。

(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
});

Node 并不直接执行你所写的代码,而是把你的代码打包成函数后,执行这个函数。

通过这样做,Node.js 实现了以下几点:

  • 它保持了顶层的变量(用 varconstlet 定义)作用在模块范围内,而不是全局对象。
  • 它有助于提供一些看似全局的但实际上是模块特定的变量,例如:
    • 用于从模块中导出内容的 moduleexports 对象。
    • 包含模块绝对文件名和目录路径的快捷变量 __filename__dirname

这个打包函数有 5 个参数:exportsrequiremodule__filename__dirname。函数使变量看起来全局生效,但实际上只在模块内生效。所有的这些参数都在 Node 执行函数时赋值:

  • exports 定义成 module.exports 的引用, 初始为 {};
  • requiremodule 对象都指向将要被包进去的代码实例 ==(这两个没太懂)==;
  • __filename__dirname 指这个打包模块的绝对路径和目录路径。

打包函数的返回值是 module.exports

# exportsmodule.exports:

在模块中定义外部可访问接口的时候, 有两个方法:

exports.name = 'Garrik';
module.exports = {name: 'Garrik'};

模块的 module.exports 是用于指定一个模块所导出的内容,即可以通过 require() 访问的内容。

模块中的 exports 变量实际上是 module.exports 的一个引用.

exportsmodule.exports 指向同一块内存.

使用 exports 的时候只能往这个对象里添加新的属性和方法, 而不能对其直接赋值. 如果想直接导出一个对象或者函数, 应该使用 module.exports.

// 这是可以的:
exports.name = 'Garrik';
exports.gender = 'Male';

// 这是不可以的:
exports = {name: 'Garrik', gender: 'Male'};

// 应该用 module.exports:
module.exports = {name: 'Garrik', gender: 'Male'}

这是因为如果把 exports 指向一个函数,那么相当于改变了 exports 的指向,exports 就不再是 module.exports 引用了。

# Evaluating - VM 对加载的代码进行处理:

require 是一个函数, 也更是一个对象. 也有自己的属性.

# Caching - 所有的模块都会被缓存:

模块在第一次加载后会被缓存。 多次调用同一模块不会导致模块的代码被执行多次。

借助它, 可以返回“部分完成”的对象,从而允许加载依赖的依赖, 即使它们会导致循环依赖。

如果想要多次执行一个模块,可以导出一个函数,然后调用该函数。

被引入的模块将被缓存在 require.cache 这个对象中。从此对象中删除键值对将会导致下一次 require 重新加载被删除的模块。

# 循环:

当循环调用 require() 时,一个模块可能在未完成执行时被返回。

例如以下情况:

// lib/module1.js
exports.a = 1;
require('./module2');
exports.b = 2;
exports.c = 3;

// lib/module2.js
const Module1 = require('./module1');
console.log(Module1);

运行 module1.js 可以看到:

{ a: 1 }

在 module1 为加载完成的时候, 引入了 module2, 而 module2 又想要请求 module1. 结果是 module2 只引入了 未完成的 module1. 只有 a 属性打印出来了,因为 b 和 c 是在请求了module2 并打印了 module1 之后才导出的。


require 调用时的步骤:

nodejs-require

# 参考:

# 相关:

上次更新: 7/4/2020, 4:14:54 AM