# CommonJS 规范

# CommonJS 的模块规范

  1. 模块引用
    在 CommonJS 规范中,存在 require()方法,这个方法接受一个模块标识,以此引入一个模块的 API 到当前的上下文中。
  2. 模块定义
    在模块中,上下文提供 require()方法来引入外部模块。对应引入的功能,上下文提供了 exports 对象用于导出当前模块的方法或者变量,并且他是唯一的导出接口。在模块中,还存在一个 module 对象,它代表模块自身,exports 是 module 的属性。
  3. 模块标识
    模块标识其实就是传递给 require()方法的参数,它必须是符合 小驼峰命名的字符串,或者是相对路径,绝对路径。 每个模块具有独立的空间,它们互不干扰,在引用的时候也显得干净利落,用户也完全不用考虑变量污染avatar

# Node 的模块实现

在 Node 中引入模块,需要经历一下 3 个步骤:

  1. 路径分析
  2. 文件定位
  3. 编译执行 而模块又分为两类,一类是核心模块,既 Node 提供的模块;一类是文件模块,既用户自己编写的模块。
  • 核心模块,在 Node 源代码的编译过程中,编译成了二进制模块,在 Node 进程启动时,部分核心模块直接加载进了内存中,所以部分核心模块引入时,文件定位和编译执行被忽略掉,路径分析也优先判断,所以它的加载速度是最快的
  • 文件模型是运行时动态加载,需要完整的三个步骤流程才行,所速度会比核心模块慢。

需要注意的是,Node 和前端浏览器会缓存静态脚本文件以提高性能一样,Node 也会对引入的模块进行缓存,不过浏览器仅仅缓存文件,而Node 缓存的是编译和执行之后的对象(require 对相同模块的二次加载一律采用缓存优先的方式,不过核心模块的缓存检查还是先于文件模块的缓存检查)

# 路径分析

  • 模块标识符分析 模块标识符在 Node 中主要分为一下积几类:
  1. 核心模块
  2. 以.或..开始的相对路径文件模块
  3. 以/开始的绝对路径模块文件
  4. 非路径形式的文件模块
  • 核心模块 加载优先级仅次于缓存加载,因为他在 Node 的源代码编译过程中已经编译成了二进制代码。

  • 路径形式的文件模块 分析模块时,require()方法会将路径转为真实路径,以真实路径作为索引,并将编译执行后的结果存放到缓存中,以便二次加载。

  • 自定义模块 主要采用一种叫模块路径的查找策略,模块路径具体表现为一个路径组成的数组。
    这个我们可以编写一个 js 文件查看,例如:

console.log(module.paths);

我们执行这个文件可以得到一下输出:
avatar

模块路径的生成规则:
❑ 当前文件目录下的 node_modules 目录。
❑ 父目录下的 node_modules 目录。
❑ 父目录的父目录下的 node_modules 目录。
❑ 沿路径向上逐级递归,直到根目录下的 node_modules 目录。

# 文件定位

缓存加载不需要路径分析,问价定位和编译执行过程

  • 文件扩展名分析 CommonJS 模块允许在标识符中不包含文件扩展名,它会按.js, .json, .node 的次序以此补全,依次尝试。
    在尝试过程中,需要调用 fs 模块同步阻塞式地判断文件是否存在。因为 node 是单线程的,所以这里会引起性能问题。
    小诀窍:1. 如果是.node 或 .json 文件,我们可以加上扩展名。 2. 同步配合缓存

  • 目录分析和包 require()在通过文件扩展民查找文件之后,没有找到响应文件,但获得了一个目录,此时 Node 会将当作一个包来处理。
    Node 在当期目录下查找 package.json,通过 JSON.parse()解析出包描述对象,去除 main 属性指定的文件名进行定位,并进行文件扩展名分析,如果 main 指定的文件错误,或者没有 package.json 文件,Node 默认将 index 当作默认文件名,进行文件扩展名分析。
    require()通过模块路径路径查找,没找到就进入下一个模块中。如果都没找到,就报错

# 模块编译

定位到具体的文件后,Node 会新建一个模块对象,然后根据路径载入并编译。

  • 不同的文件扩展名,载入方法也不同:
    ❑ .js 文件。通过 fs 模块同步读取文件后编译执行。
    ❑ .node 文件。这是用 C/C++编写的扩展文件,通过 dlopen()方法加载最后编译生成的文件。
    ❑ .json 文件。通过 fs 模块同步读取文件后,用 JSON.parse()解析返回结果。
    ❑ 其余扩展名文件。它们都被当做.js 文件载入。
    每一个成功编译的模块都会将起文件路径作为索引缓存在 Module._cache 对象上,以提高二次引入的性能。

  • javascript 模块的编译 javascript 模块会被 Node 头尾包装,在头部添加了(function (exports, require, module, **filename, **dirname){\n,在尾部添加了\n}
    这样每个模块之间都进行了作用域隔离,不染全局变量。包装后的代码会通过 vm 原生模块的 runInThisContext()方法执行(类似 eval,只具有明确上下文,不污染全局),返回一个具体的 function 对象。
    再执行之后,模块的 exports 属性返回给了调用方。exports 属性上的任何方法都可以被外部调用到,但是模块中的其余变量则不可被调用。

  • c/c++模块的编译 Node 调用 process_dlopen()方法进行加载和执行。window 和*nix 平台下分别有不同的实现,通过linuv 兼容层进行了封装。
    实际上.node 文件只需要加载和执行。在执行的过程中,模块的 exports 对象与.node 模块产生联系,然后返回给调用者。所以 c/c++模块执行效率会更高些。

  • JSON 模块的编译 Ndoe 通过 fs 模块同步读取 JSON 文件的内容之后,调用 JSON.parse()方法得到对象,然后将它赋值给模块对象的 exports,以供外部调用。
    根据这个,我们将 JSON 文件用在项目的配置文件时比较有用,我们可以不用 fs 模块去异步读取和解析,只需要 require()即可。