Node.js 中使用 require 加载模块时发生了什么?

描述了 Node.js 中 require 函数在加载模块时执行的一系列步骤,包括缓存检查、模块解析、定位与加载、编译和执行模块以及更新缓存。

Node.js 中等 模块系统 Require 模块

当在 Node.js 中使用 require 加载模块时,它会执行一个序列化的解析、加载和缓存过程:

  1. 检查缓存
    首先判断模块是否已加载过:
    if (require.cache[modulePath]) {
      return require.cache[modulePath].exports; // 直接返回缓存的 exports 对象
    }
    

    如果已在缓存中,直接返回结果(提高性能,避免重复加载)。

  2. 解析模块标识符
    基于传入的字符串路径或模块名进行分类处理:
    • 核心模块(如 require('fs'):直接返回内置模块。
    • 相对/绝对路径(如 require('./module')
      按顺序尝试:
      • 查找无后缀的文件(直接作为 JS 解析)。
      • 查找 .js, .json, .node 后缀文件(.json 直接解析为 JSON 对象)。
    • 非路径模块(如 require('lodash')
      从当前目录向上递归查找 node_modules 文件夹:
      • node_modules 内检查是否为核心模块。
      • 搜索对应名称的子文件夹(加载入口默认为 index.jspackage.json 指定 main)。
  3. 定位并加载模块内容
    针对文件/文件夹进行加载:
    const moduleCode = fs.readFileSync(modulePath); // 读取文件
    
    • 文件夹模块
      package.json 时默认加载 index.js;有 package.json 则加载 main 指定文件。
    • JSON文件:解析为 JSON 对象(仅配置加载)。
  4. 编译并执行模块
    将加载的文件内容封装在立即调用的函数表达式中执行:
    const compiledWrapper = (function(exports, require, module, __filename, __dirname) {
      ... // 执行模块代码,设置 exports
    });
    compiledWrapper(module.exports, require, module);
    

    此过程创建模块隔离作用域(避免了全局污染),导出 exports 接口。

  5. 更新缓存并返回结果
    模块执行后将导出对象(module.exports)加入缓存:
    require.cache[modulePath] = {
      exports: module.exports,
      loaded: true
    };
    return module.exports; // 提供出口给调用方
    

关键特性:

  • 同步加载:每次 require 时会阻塞后续代码执行直到模块完全加载(需避免大型模块阻塞)。
  • 单例缓存:模块首次加载后会被缓存(所有后续 require 共享实例),缓存可手动清理(如动态热重载优化)。
  • 错误处理:若路径不合法/文件不存在抛出 MODULE_NOT_FOUND 异常。