# 模块机制:
在 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.json
的 main
属性就可以。
# 有路径:
如果指定了模块的绝对路径, 或者相对路径.
例如:
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 实现了以下几点:
- 它保持了顶层的变量(用
var
、const
或let
定义)作用在模块范围内,而不是全局对象。 - 它有助于提供一些看似全局的但实际上是模块特定的变量,例如:
- 用于从模块中导出内容的
module
和exports
对象。 - 包含模块绝对文件名和目录路径的快捷变量
__filename
和__dirname
。
- 用于从模块中导出内容的
这个打包函数有 5 个参数:exports
,require
,module
,__filename
,__dirname
。函数使变量看起来全局生效,但实际上只在模块内生效。所有的这些参数都在 Node 执行函数时赋值:
exports
定义成module.exports
的引用, 初始为{}
;require
和module
对象都指向将要被包进去的代码实例 ==(这两个没太懂)==;__filename
和__dirname
指这个打包模块的绝对路径和目录路径。
打包函数的返回值是 module.exports
# exports
和 module.exports
:
在模块中定义外部可访问接口的时候, 有两个方法:
exports.name = 'Garrik';
module.exports = {name: 'Garrik'};
模块的 module.exports
是用于指定一个模块所导出的内容,即可以通过 require()
访问的内容。
模块中的 exports
变量实际上是 module.exports
的一个引用.
exports
和 module.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
调用时的步骤:
# 参考:
- module.js 源码
- Node 官方文档
- 深入源码 - Chapter 2
- 在 Node.js 中引入模块:你所需要知道的一切都在这里
- 通过源码解析 Node.js 中一个文件被 require 后所发生的故事