Last updated on September 11

模块系统

Node.js 有两个模块系统:CJSESM

Node.js 在以下情况时会使用 ESM

  1. 拓展名为 .mjs 的文件(无论 package.jsontype 配置为何值)
  2. 父级 package.jsontypemodule,拓展名为 .js 的文件
  3. 使用 import() 调用

Node.js 以下情况将会使用 CJS

  1. 拓展名为 .cjs 的文件(无论 package.jsontype 配置为何值)
  2. 父级 package.jsontypecommonjs 或不包含 type 字段时(默认为 commonjs),拓展名为 .js 的文件
  3. 使用 require() 调用
  4. 拓展名不为 .mjs.cjs.json.node.js 时(当父级 package.jsontypemodule 时,不作为程序的命令行入口时,这些文件在被要求时才会被识别为 CJS 模块)
    1. 由于 require() 的同步特性,不能用它来加载 ESM 模块文件,会产生 ERR_REQUIRE_ESM 错误。

ESM vs CJS module loader

CommonJS module loader

  1. 完全同步(full synchronous)
    1. 服务器端的模块体系,需要加载的模块都在本地,所以采用同步加载也不会出问题
    2. 只有加载完成,才能执行后面的操作
  2. monkey patchable(运行时动态替换属性)。
    1. CSJ 输出的是:值的拷贝(模块内部的变化也不会影响这个)
  3. 运行时加载
    1. 所有代码都运行在模块作用域,不会污染全局作用域。详情可见:module 包装器 module wrapper
    2. 运行时加载,加载的是整个模块,将所有的接口全部加载进来
    3. this 指向当前模块
  4. 负责处理 require() 调用
  5. 支持文件夹作为模块(folders as modules)
  6. 当解析一个指定符时,如果没有找到精确的匹配,它将尝试添加扩展名(.js.json,最后是 .node),然后尝试解析文件夹为模块
  7. .json 作为 JSON 文本文件
  8. .node 文件将被解释为用 process.dlopen() 加载的已编译附加模块
  9. 缺乏拓展名的文件默认为 JS 文本文件
  10. 不能用来加载 ESM 模块(尽管可以从 CJS 模块中夹在 ESM 模块)。当加载一个不是 ESM 模块的 JS 文件时,将使用 CJS 来加载

ECMAScript module loader

  1. 异步(asynchronous)

    1. ESM 用于浏览器端时,可能涉及到一些异步请求,所以需要采用异步加载
  2. 不支持 monkey patchable。但可以通过 Loader Hooks 进行定制

    1. ESM 输出的是:值的引用(被输出模块的内部的改变会影响引用的改变),所以不允许在外部直接修改值(对象修改或新增属性除外)
  3. 编译时输出接口

    1. 模块的依赖关系是可以根据 import 引用关系推导出来的
    2. 依赖关系与运行时状态无关,也就是说 import binding 是 immutable 的。编译时确定输出了
    3. 可以单独加载其中的接口
    4. this 指向 undefined
  4. 负责处理 import statementsimport() 表达式

  5. 不支持文件夹作为模块,目录索引必须完全指定(例如:./startup/index.js

  6. 不做拓展名搜索。当指定是一个相对或绝对的文件 URL 时,必须提供文件拓展名

  7. 可以加载 JSON 模块,但需要一个导入断言

    // index.mjs
    // An import assertion in a static import
    import info from './package.json' assert { type: 'json' }
    
    // An import assertion in a dynamic import
    const { default: info } = await import('./package.json', { assert: { type: 'json' } })
    
  8. 只接受以 .js.mjs,和 .cjs 为拓展名的 JavaScript 文本文件

  9. 可以用来加载 JavaScript CJS 模块。这些模块通过 cjs-module-lexer 来尝试识别命名的导出。如果可以通过静态分析确定,那么这些导出是可用的,导入的 CJS 模块将其 URL 转换为绝对路径,然后通过 CJS module loader 加载。

resolve 机制

All together

require(X),X 来自路径 Y 的模块

  1. 如果 X 是一个核心模块(内置)。返回该核心模块,停止
  2. 如果 X 以 / 开头。设置 Y 为文件系统根目录
  3. 如果 X 以 .//../ 开头。
    1. LOAD_AS_FILE(Y + X)
    2. LOAD_AS_DIRECTORY(Y + X)
    3. THROW "not found”
  4. 如果 X 以 # 开头
    1. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
  5. LOAD_PACKAGE_SELF(X, dirname(Y))
  6. LOAD_NODE_MODULES(X, dirname(Y))
  7. THROW "not found”

File Modules

如果没有找到准确的文件名,Node.js 将尝试加载所需的文件名,并添加扩展名:.js.json,最后是 .node。当加载具有不同扩展名的文件时(例如:.cjs),必须将其全名传递给 require(),包括其文件扩展名(例如:require('./file.cjs')