Node 模块引入
Node 模块引入主要会经历如下3个步骤:
- 路径分析
- 文件定位
- 编译执行
在 Node 中,模块分为核心模块和文件模块。
核心模块是 Node 提供的模块。其中部分模块在 Node 源码编译时直接生成二进制执行文件,在 Node 进程启动时被加载到内存中,因此当这部分模块引入时,会只直接跳过文件定位和编译执行,且在路径分析中优先判断,它的加载速度最快。
文件模块是用户编写的模块。文件模块在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。
缓存优先
跟浏览器会缓存静态脚本文件以提高性能一样,Node 对引入的模块也会进行缓存,以减少二次引入的开销。不同之处在于,浏览器是缓存文件,而 Node 是缓存编译和执行之后的对象。
不论是核心模块还是文件模块,require()
方法对相同的模块的二次加载一律采用缓存优先的方式。
路径分析和文件定位
require()
方法接受一个标识符作为参数,标识符有几种形式,根据不同的标识符,模块的查找和定位有不同的差异。
模块标识符分析
模块标识符主要分为以下几类:
- 核心模块,如 http、fs、path 等
- . 或 … 开头的相对路径文件模块
- 以 / 开头的绝对路径文件模块
- 非路径形式的文件模块,如自定义的 connect 模块
核心模块
核心模块的优先级仅次于缓存加载。
如果要加载一个与核心模块标识符相同的自定义模块,那是不会成功的。如果要加载必须选择一个不同的标识符或换成路径方式。
路径形式的文件模块
以 .、… 和 / 开头的标识符,这里都被当作文件模块处理。在分析文件模块时,require()
方法会将路径转为真实路径,并以真实路径作为索引,将编译后的结果存放到缓存中。
由于文件模块给 Node 指明了确切的文件位置,所以在查找过程中可以节约大量时间,其加载时间慢于核心模块。
自定义模块
自定义模块是一种特殊的文件模块,可以是一个文件或者包的形式。这类模块的查找是最费时的,是所有方式中最慢的。
在介绍自定义模块的查找方式之前,需要先介绍一下模块路径这个概念。
模块路径是 Node 在定位文件模块的具体文件时制定的查找策略,具体表现为一个路径组成的数组。关于这个路径的生成规则,我们可以手动尝试一番。
(1) 创建 module_path.js 文件,其内容如下:
console.log(module.paths);
(2) 将该文件放到任意一个目录中,然后执行如下命令:
node module_path.js
在 Linux 下,你可能得到如下的数组输出:
['/home/user/demo/node_modules',
'/home/user/node_modules',
'/home/node_modules',
'/node_modules']
在 Windows 下,可能是这样:
['c:\\demo\\node_modules',
'c:\\node_modules']
可以看出,模块路径的生成规则如下所示:
- 当前文件目录下的 node_modules 目录;
- 父目录下的 node_modules 目录;
- 父目录下的父目录下的 node_modules 目录;
- 沿着路径向上逐级递归,直到根目录下的 node_modules 目录。
在自定义模块的加载过程中,Node 会逐个尝试模块路径中的路径,直到找到目标文件为止。可以看出,当前文件的路径越深,模块查找耗时会越多,这是自定义模块的加载速度最慢的原因。
文件定位
在文件定位过程中,有些细节需要注意,主要包括文件扩展名的分析、目录和包的处理。
文件扩展名分析
require()
在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况。CommonJS 模块规范也允许在标识符中不包含文件扩展名,这种情况下,Node 会按 .js、.json、.node 的次序补足扩展名,依次尝试。
在尝试的过程中,需要调用 fs 模块同步阻塞式的判断文件是否存在。因为 Node 是单线程的,所以这里是一个会引起性能问题的地方。所以,如果是 .json 和 .node 文件,在标识符中带上扩展名,会加快一点速度。
目录分析和包
在分析标识符的过程中,require()
通过分析文件扩展名后,可能没有查找到对应的文件,但却得到一个目录,这在引入自定义模块和逐个模块路径查找时经常会出现,此时 Node 会将目录当做一个包来处理。
在这个过程中,Node 对 CommonJS 包规范进行了一定程度的支持。首先,Node 在当前目录下查找 package.json(CommonJS 包规范定义的包描述文件),通过JSON.parse()
解析出包描述对象,从中取出 main 属性指定的文件名进行定位。如果文件名缺少扩展名,将会进入扩展名分析的步骤。
而如果 main 属性指定的文件名错误,或者压根没有 package.json 文件,Node 会将 index 当作默认文件名,然后依次查找 index.js、index.json、index.node。
如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败的异常。