# 模块机制

# CommonJS

exportsmodule.exports

# 模块实现

# 模块引入

模块分为两类:Node提供的核心模块和用户编写的文件模块。

Node中引入模块,需要经历三个步骤:

  1. 路径分析

  2. 文件定位

  3. 编译执行

Node启动时,部分核心模块就被直接加载进内存,所以这部分核心模块引入时,无须文件定位和编译执行步骤,加载速度最快。

文件模块在运行时动态加载,速度较慢。

# 缓存加载

Node会对引入过的模块进行缓存,以减少二次引入时的开销。

浏览器仅缓存文件,Node缓存的是编译和执行之后的对象。

编译成功后会将文件路径作为索引缓存在Module._cache(核心模块为NativeModule._cache)对象上。

# 路径分析和文件定位

模块标识符在Node中主要分为:

  1. 核心模块,如httpfs等。

  2. 相对路径文件模块。

  3. 绝对路径文件。

  4. 非路径形式的文件模块,如自定义的connect模块。

查找耗时:缓存 < 核心 < 路径 < 自定义

自定义模块查找策略是:从当前目录沿路径向上逐级递归,直到根目录的 node_modules 目录。

允许标识符中不含文件扩展名,Node会按.js.json.node的次序依次尝试。在尝试的过程中,需要调用fs模块同步阻塞式地判断文件是否存在,所以最好补齐扩展名。可能最后查找到的是一个目录,Node会在当前目录下查找package.json,通过JSON.parse解析出包描述对象,取出main属性指定的文件名进行定位,如果没有package文件,会将index作为默认文件名依次查找。

# 编译

对于不同的扩展名,载入方法不同:

  1. .js文件。通过fs模块同步读取文件后编译执行。

  2. .node文件。这是用C/C++编写的,通过dlopen方法加载。

  3. .json文件。通过fs同步读取后用JSON.parse解析返回结果。

  4. 其余扩展文件都被当做.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 (){}
})
1
2
3

# CMD

支持动态引入

define(function (require, exports, module) {
    
})
1
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;
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上次更新时间: 9/12/2021, 10:30:35 PM