CSC-NOTESCSC-NOTES
首页
  • 前端笔记

    • Vue
    • React
    • CSS
    • JavaScript
    • TypeScript
  • 服务端笔记

    • NodeJS
  • C/C++

    • C语言
  • 数据库笔记

    • Postgresql
  • 计算机网络

    • 计算机网络基础
    • 计算机网络进阶
  • 操作系统

    • 操作系统基础
    • 操作系统进阶
  • 前端工程化

    • Vue
    • React
  • 后端工程化

    • NodeJS
  • 数据结构
  • 算法
  • 前端面试
  • 后端面试
Github
首页
  • 前端笔记

    • Vue
    • React
    • CSS
    • JavaScript
    • TypeScript
  • 服务端笔记

    • NodeJS
  • C/C++

    • C语言
  • 数据库笔记

    • Postgresql
  • 计算机网络

    • 计算机网络基础
    • 计算机网络进阶
  • 操作系统

    • 操作系统基础
    • 操作系统进阶
  • 前端工程化

    • Vue
    • React
  • 后端工程化

    • NodeJS
  • 数据结构
  • 算法
  • 前端面试
  • 后端面试
Github
  • NodeJS
    • CommonJS规范
    • Node的模块实现

1. 🗃️ Node模块实现

在Node中引入模块需要三个步骤

  • 路径分析
  • 文件定位
  • 编译执行

在Node中模块分为两类:一类是Node提供的模块,称为核心模块;另一类是用户编写的模块,称为文件模块。

核心模块部分在Node源代码的编译过程中,编译进行了二进制执行文件。在Node进程启动的时,部分核心模块就被直接加载到内存中,这部分核心模块引入时,文件定位和编译执行这两步就可以直接省略掉,并且在路径分析中优先判断,所以它的加载速度是最快的。

文件模块则是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。

2. 🛅 优先从缓存加载

Node对引入过的模块都会进行缓存,以减少二次引入时的开销。不论是核心模块还是文件模块,require()方法对相同模块的二次加载一律采用缓存优先的方式。

3. ⬆️ 路径分析和文件定位

3.1. 🏷️ 模块标识符分析

require()方法接收一个标识符作为参数。在Node实现中,正是基于这样一个标识符进行模块查找的。模块标识符在Node中主要分为以下几类。

  • 核心模块,如http、fs、path等。
  • .或..开始的相对路径文件模块。
  • 以/开始的绝对路径文件模块。
  • 非路径形式的文件模块,如require('mymodule.js')。

3.1.1. 🗂️ 核心模块

核心模块优先级仅次于缓存加载,它在Node的源代码编译过程中已经编译为二进制代码,如果视图加载一个与核心模块标识符相同的自定义模块,那是不会成功的,如果自己编写了一个http模块,要想加载成功,必须选择一个不同的标识符或者换用路径的方式引入。

// http.js 自己编写http模块
console.log("http");
/**
 * 希望这里打印字符串 http 是不会成功的。
 * 因为自己命名的模块与核心模块名称冲突了,这会导致自定义的模块引入失败。
 */
const http = require("http");

正确引入方式

const http = require('./http.js');

3.1.2. 🛩️ 路径形式和文件模块

以.或..或/开始的标识符,都会被当做文件模块处理。在分析模块时,require()方法会将路径转化为真实路径,并以真实路径为索引,将编译后的结果放到缓存中,让二次加载更快。但是加载速度还是要慢于核心模块。

3.1.2. 🤳 自定义模块

自定义模块是一种非常特殊的文件模块,可能是一个文件或者包的形式。这类模块的查找是最费时的,也是所有方式中最慢的一种。

模块路径是Node在定义文件模块的具体文件时制定的查找策略,表现形式为一个路径数组。关于这个路径的生成规则,我们可以手动感受一下。

  • 创建一个a.js文件,内容为console.log(module.paths);。
  • 将其放到任意一个目录中然后执行node a.js。
// 打印这个路径数组
console.log(module.paths);
/**
 * [
  'E:\\workspace\\csc-notes\\node_modules',
  'E:\\workspace\\node_modules',
  'E:\\node_modules'
]
 */

可以看出,模块路径的生成规则如下

  • 当前文件目录下的node_modules目录。
  • 父级目录下的node_modules目录。
  • 父级目录的父级目录下的node_moudles目录。
  • 沿着路径逐级递归查找,直到根目录下的node_modules目录。 这种查找方式与JavaScript的原型链或作用域链的查找方式十分类似。在加载过程中,Node会逐个尝试模块路径中所有的路径,直到找到文件为止。可以看出,当文件的路径越深,模块查找耗时越多。

3.2. ↘️ 文件定位

从缓存加载的优化策略使得二次引入时不需要路径分析、文件定位和编译执行的过程,大大提高了再次加载模块时的效率。

但在文件定位过程中,还需要一些细节需要注意,包括文件扩展名的分析、目录和包的处理。

3.2.1. 🧩 文件扩展名分析

require()在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况。CommonJS模块规范也允许在标识符中不包含文件扩展名,在这种情况下,Node会按照 .js、.json、.node的次序补足扩展名,以此尝试。

在尝试的过程中,需要调用fs模块同步阻塞式判断文件是否存在。因为Node是单线程的,所以这里是一个会引起性能问题的地方。小诀窍是:如果是.node、.json文件,在传递给require()的标识符中带上扩展名,会加快一点速度。

3.2.2. 📓 目录分析和包

在分析标识符的过程中,可能没有查到对应的文件,但却得到了一个目录,这在引入自定义模块和逐个模块路径时经常出现,此时Node会将目录当做一个包来处理。在这个过程中,Node对CommonJS包规范进行了一定程度的支持。首先,Node在当前目录下查找package.json,通过JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。如果文件名缺少扩展名,将会进入扩展名分析步骤。而如果main属性指定的文件名错误,或者压根没有package.json文件,Node会将index当做默认文件名,然后依次查找index.js、index.json、index.node。如果在目录分析过程中没有定位到任何的文件,则Node会进入到下一个模块路径进行查找。如果模块路径数组全部遍历完毕,依然没有找到目标文件,Node就会抛出异常。

💬 欢迎评论!请确保您已登录 GitHub。

最近更新: 2025/5/19 12:57
Contributors: chenshicong
Prev
CommonJS规范