# 模块机制
# CommonJS
exports和module.exports
# 模块实现
# 模块引入
模块分为两类:Node提供的核心模块和用户编写的文件模块。
在Node中引入模块,需要经历三个步骤:
路径分析
文件定位
编译执行
在Node启动时,部分核心模块就被直接加载进内存,所以这部分核心模块引入时,无须文件定位和编译执行步骤,加载速度最快。
文件模块在运行时动态加载,速度较慢。
# 缓存加载
Node会对引入过的模块进行缓存,以减少二次引入时的开销。
浏览器仅缓存文件,Node缓存的是编译和执行之后的对象。
编译成功后会将文件路径作为索引缓存在Module._cache(核心模块为NativeModule._cache)对象上。
# 路径分析和文件定位
模块标识符在Node中主要分为:
核心模块,如
http、fs等。相对路径文件模块。
绝对路径文件。
非路径形式的文件模块,如自定义的
connect模块。
查找耗时:缓存 < 核心 < 路径 < 自定义
自定义模块查找策略是:从当前目录沿路径向上逐级递归,直到根目录的 node_modules 目录。
允许标识符中不含文件扩展名,Node会按.js、.json、.node的次序依次尝试。在尝试的过程中,需要调用fs模块同步阻塞式地判断文件是否存在,所以最好补齐扩展名。可能最后查找到的是一个目录,Node会在当前目录下查找package.json,通过JSON.parse解析出包描述对象,取出main属性指定的文件名进行定位,如果没有package文件,会将index作为默认文件名依次查找。
# 编译
对于不同的扩展名,载入方法不同:
.js文件。通过fs模块同步读取文件后编译执行。.node文件。这是用C/C++编写的,通过dlopen方法加载。.json文件。通过fs同步读取后用JSON.parse解析返回结果。其余扩展文件都被当做
.js文件载入。
require.extensions 定义了上面三种扩展名的加载方式。可以通过类似require.extensions[.ext]的方式实现对自定义扩展名的特殊加载。
JavaScript模块的编译:在编译的过程中,Node对于获取的文件都在头部添加了(function(exports, require, module, __filename, __dirname) {}),这样实现了作用域隔离。包装后的代码会通过vm原生模块的runInThisContext(类似eval,不污染全局)方法执行,返回一个function对象。引入时将exports返回给调用方。
C/C++模块的编译:无需编译,只用加载和执行。执行效率较高,门槛也相对较高。
JSON文件的编译:通过fs同步读取后用JSON.parse解析。
# AMD
define(id?, dependicies?, factory)
声明模块时指定所有的依赖,通过形参传递依赖到模块中。
define(['dep1', 'dep2'], function (dep1, dep2) {
return function (){}
})
2
3
# CMD
支持动态引入
define(function (require, exports, module) {
})
2
3
# 兼容多种模块规范
(function (name, definition) {
var hasDefine = typeof define === 'function',
hasExports = typeof module !== "undefined" && module.exports;
if (hasDefine) {
define(definition);
} else if (hasExports) {
module.exports = definition();
} else {
// 浏览器
this[name] = definition();
}
})('hello', function () {
var hello = function () {};
return hello;
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16