# CommonJS 规范
# CommonJS 的模块规范
- 模块引用
在 CommonJS 规范中,存在 require()方法,这个方法接受一个模块标识,以此引入一个模块的 API 到当前的上下文中。 - 模块定义
在模块中,上下文提供 require()方法来引入外部模块。对应引入的功能,上下文提供了 exports 对象用于导出当前模块的方法或者变量,并且他是唯一的导出接口。在模块中,还存在一个 module 对象,它代表模块自身,exports 是 module 的属性。 - 模块标识
模块标识其实就是传递给 require()方法的参数,它必须是符合 小驼峰命名的字符串,或者是相对路径,绝对路径。 每个模块具有独立的空间,它们互不干扰,在引用的时候也显得干净利落,用户也完全不用考虑变量污染。
# Node 的模块实现
在 Node 中引入模块,需要经历一下 3 个步骤:
- 路径分析
- 文件定位
- 编译执行 而模块又分为两类,一类是核心模块,既 Node 提供的模块;一类是文件模块,既用户自己编写的模块。
- 核心模块,在 Node 源代码的编译过程中,编译成了二进制模块,在 Node 进程启动时,部分核心模块直接加载进了内存中,所以部分核心模块引入时,文件定位和编译执行被忽略掉,路径分析也优先判断,所以它的加载速度是最快的
- 文件模型是运行时动态加载,需要完整的三个步骤流程才行,所速度会比核心模块慢。
需要注意的是,Node 和前端浏览器会缓存静态脚本文件以提高性能一样,Node 也会对引入的模块进行缓存,不过浏览器仅仅缓存文件,而Node 缓存的是编译和执行之后的对象(require 对相同模块的二次加载一律采用缓存优先的方式,不过核心模块的缓存检查还是先于文件模块的缓存检查)
# 路径分析
- 模块标识符分析 模块标识符在 Node 中主要分为一下积几类:
- 核心模块
- 以.或..开始的相对路径文件模块
- 以/开始的绝对路径模块文件
- 非路径形式的文件模块
核心模块 加载优先级仅次于缓存加载,因为他在 Node 的源代码编译过程中已经编译成了二进制代码。
路径形式的文件模块 分析模块时,require()方法会将路径转为真实路径,以真实路径作为索引,并将编译执行后的结果存放到缓存中,以便二次加载。
自定义模块 主要采用一种叫模块路径的查找策略,模块路径具体表现为一个路径组成的数组。
这个我们可以编写一个 js 文件查看,例如:
console.log(module.paths);
我们执行这个文件可以得到一下输出:
模块路径的生成规则:
❑ 当前文件目录下的 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()即可。