基础模块✅
CommonJS 与 ESM 模块系统区别?
答案
| 系统 | CommonJS(CJS) | ESM |
|---|---|---|
| 加载 | 同步 require | 异步,支持顶层 await |
| 导出语义 | 导出为对象快照(非 live binding) | live binding,静态可分析(利于 tree-shaking) |
| 解析与定位 | 传统解析,可省扩展名,支持目录 index | URL/显式扩展名;受 exports/imports 影响 |
| 互操作 | 可 require JSON、.node;用 import() 加载 ESM | CJS 需用动态 import;ESM 不可直接用 require |
| 文件/配置 | .cjs 或 package.json 设置 "type":"commonjs" | .mjs 或 package.json 设置 "type":"module" |
| 实现 | 本质上是一个必包函数,默认非严格模式 | V8 引擎内置支持,默认严格模式 |
示例说明
// package.json
{ "type": "module", "exports": "./dist/index.js" }
// CJS 中加载 ESM(需异步)
/* index.cjs */
(async () => {
const { sum } = await import('./esm.mjs')
console.log(sum(1, 2))
})()
/* esm.mjs */
export const sum = (a, b) => a + b
const cfg = await import('./cfg.json', { assert: { type: 'json' } })
console.log(cfg.default.name)
ESM 需写全扩展名,路径以 ./ 或 ../ 开头;库发布优先使用 package.json 的 exports 控制入口,避免隐式深引用与解析歧义。
延伸阅读
- Node.js Modules: CJS CommonJS 规范与解析
- Node.js ESM node.js API 文档,对 ESM 支持的全面说明,同时详细讲解了和 commonjs 相关的兼容差异
ESM 支持顶层异步,为什么 Node.js 中可以加载 ESM 模块?
答案
Node.js Common.js 模块只支持引入没有顶层异步的 ESM 模块, 如果存在异步会抛出 ERR_REQUIRE_ASYNC_MODULE 错误,如果希望在在 Common.js 中引用异步可采用 import 语句,运行时需要开启 --experimental-print-required-tla 配置。
import('./asyncAdd.mjs').then(({ add }) => {
console.log(add(1, 2)) // 3
})
延伸阅读
- require(esm) in Node.js Node.js 核心开发者详细讲解实现 CJS 同步加载 ESM 中的难点
- 实现同步 ESM 的讨论
Node 原生支持哪些模块,有什么区别
答案
| 类型 | 扩展名/入口 | 加载与执行 | 导出语义 | 解析与定位 |
|---|---|---|---|---|
| CommonJS(CJS) | .cjs 或 .js(默认 commonjs) | 同步 require | 值快照(非 live binding),require 缓存 | 传统解析,可省扩展名,支持目录 index |
| ES Module(ESM) | .mjs 或 .js(type:module) | 异步,支持顶层 await | live binding,可静态分析 | URL/显式扩展名;受 package.json exports/imports 影响 |
| JSON 模块 | .json | CJS 同步加载,ESM 需要使用 import attributes 申明为 with {type: 'json'} | 默认为 default 的 JS 对象 | ESM 导入必须申明文件后缀 |
| 原生扩展 | .node | 同步装载本机动态库 | 导出 N-API/C++ 绑定对象 | 由 Node 动态装载,跨 CJS/ESM 可用 |
| 内置核心模块 | node:fs、node:path 等 | 零网络/磁盘解析 | 稳定 API 对象 | 通过 node: 方案名直接解析 |
示例说明
// CJS
const cfg = require('./cfg.json')
const addon = require('./addon.node')
;(async () => {
const { sum } = await import('./esm.mjs') // 加载 ESM 用 import()
console.log(cfg.name, addon.version, sum(1, 2))
})()
// ESM
import fs from 'node:fs'
import cfg from './cfg.json' assert { type: 'json' }
export { fs, cfg }
常见误区
- CJS 不能直接 require 含顶层 await 的 ESM,请用 import()。
- ESM 路径需写全扩展名,受 exports 字段影响解析;避免深引用内部文件。
- JSON 在 ESM 中必须使用
assert { type: 'json' },并以 default 形式导出。
优先使用 node: 前缀引用核心模块(如 node:fs),可避免与用户空间同名包冲突,并利于静态分析。
延伸阅读
- Node.js Modules (CJS) CommonJS 规范与解析
- Node.js ESM 加载、解析、互操作细节
- N-API 原生扩展接口与稳定性承诺
- package 详细讲解 node 中的包相关配置
用过 Node.js 哪些内置模块或 API?
答案
Node.js API 文档 详细罗列了 Node 支持的内部能力,可按照如下逻辑分类
- 核心基础 全局和常用的模块
- Modules 模块系统, ESM 模块
- global api Node 宿主环境支持的全局对象,例如
__filename、__dirname、process等 - Events 事件模块
- Timer 定时器相关 API
- 文件系统
- File system(文件系统)
- Readline(行读取)常用在日志处理等场景
- Path(路径处理)
- Stream(流式数据), 注意和 webstreams 浏览器端实现的区别
- String decoder(字符串解码)
- 进程与多线程 处理并发、子进程、集群和多线程的 API,用于高性能和分布式场景。
- Environment Variables(环境变量)
- Process node 进程信息及监控
- Child processes 提供子进程创建能力
- Cluster(多进程集群)
- Worker threads(工作线程)
- 网络与通信 涉及 TCP/UDP、HTTP/HTTPS、DNS 等网络通信,以及 URL 解析和域名处理。
- Net TCP socket 编程相关能力
- UDP/datagram(UDP 套接字)
- HTTP, 配合 Query string、URL
- HTTP/2
- HTTPS
- TLS/SSL
- DNS(域名解析)
- 工具类 涉及 Buffer、编解码、加密、国际化等功能
- Buffer Node 提供的二进制数据容器,基于 uint8Array 实现,Buffer 脱离 V8 垃圾回收机制
- String decoder 配合 Buffer 使用实现常见 UTF-8、UTF-16 等编码解码
- Zlib 压缩能力
- OS 获取系统相关信息
- Sqlite 集成 SQLite 数据库
- Crypto (加密模块),还有 webcrypto web 端加解密 API
- Internationalization 国际化
- Permissions(权限控制)
- Util 内部集成的工具类
- Asynchronous context tracking 提供了一步上下文获取能力,在 Koa 中利用 AsyncLocalStorage 实现了异步上下文获取
- 工程化类 涉及调试、测试、性能等
- Console 控制台输出
- Errors(错误处理)
- Performance 性能监控与分析
- Assert 断言库
- Test Runner Node 内部集成的测试运行器
- 高级扩展 提供 Node.js 底层扩展、嵌入、虚拟机和 WASI 等高级功能。
- N-API 提供了一组 C/C++ API,用于构建原生插件和扩展。
- V8 获取 V8 内部信息,比如 getHeapSpaceStatistics 获取堆的快照
- vm 提供沙盒环境运行 JavaScript 代码的能力
- WASI web assembly 能力
面试官视角
此问题可作为热身题,基于面试者对常用 API 的分类和罗列可以评估面试者对 Node.js 整体熟悉程度,结合面试者提及的内容可以做进一步下探
node 是如何实现 require 的?
答案
核心概念
-
解析 Module._resolveFilename 基于模块 id 返回模块的绝对路径,核心规则包括,细节参考 模块解析
- 如果是核心模块解析后直接返回
- 如果是绝对路径,直接返回绝对路径
- 如果是相对路径,先走文件查找,在基于目录查找,会逐级向上查找,此外可以通过
NODE_PATH来增加解析路径。 - 如果以
#开头,基于imports配置 - 如果以
@xx开头按照作用域包加载 - 如果以
xx开头按照普通 node 包加载
-
缓存 Module._cache 以绝对文件路径缓存已加载模块,二次 require 命中缓存
-
文件类型 Node 支持多种文件类型,可以通过 `console.log(require('module')._extensions) 获取内部支持的文件后缀
.js:会更具 type 配置来确定按照 commonjs 还是 ESM 方式加载.json采用 JSON.parse 解析返回,注意 esm 需要通过with {type : 'json'}识别.node通过 process.dlopen 装载原生扩展cjs按照 commonjs 处理,内部调用 cjs loader, 需要显示包含文件名不会默认扩展.mjs内部调用 esm loader.ts、.cts、mts先通过- 内部采用 amaro 剔除 ts 类型,也可通过 node 传入
--experimental-transform-types配置使能 ts 转换,代码详见 typescript.js - 然后在基于文件类型走 Commonjs 或 ESM 处理
- 内部采用 amaro 剔除 ts 类型,也可通过 node 传入
对于循环依赖 cjs 会默认暂停后续解析,esm 由于 live binding 不受影响但是会基于场景抛出相关警告,在 TopLevelAwait 避免循环引用会抛出警告无法正常加载循环模块
延伸阅读
- Node.js Modules(CJS) require 行为与 API
- CJS 说明 CommonJS 加载细节
- ESM 说明 ESM 加载细节
- TypeScript 说明 TypeScript 加载细节
- Module 说明 Module 相关 API
- package 详细讲解 Node.js 原生支持的
package.json配置
exports 和 module.exports 有什么区别?
答案
| 项 | 说明 |
|---|---|
| 返回值 | require() 返回的永远是 module.exports |
| 关系 | exports 只是 module.exports 的初始别名(同一引用):var exports = module.exports = |
| 赋值差异 | exports.foo = 1 等价于 module.exports.foo = 1 但 exports = {...} 只是改了局部变量引用,失效 |
| 覆盖规则 | 一旦对 module.exports 重新赋值(如函数/对象),它将成为最终导出,之前挂在 exports 上的成员不会被导出 |
核心概念
- Node 在执行 CJS 时用包装函数注入
(exports, require, module, __filename,__dirname),并令 exports 与 module.exports 指向同一对象。 - require 取的是 module.exports 的引用,只有修改 module.exports 或向其上挂载属性才会生效。
示例说明
// 正确:挂载属性
exports.a = 1
module.exports.b = 2
// 正确:整体导出(常用于导出函数/类/单例)
module.exports = function Service () {}
// 反例:无效的整体替换(不会改变 module.exports)
exports = { x: 1 } // ❌ 仅改了局部变量 exports 的指向
// 冲突示例:以下最终只导出函数(对象属性丢失)
exports.a = 1
module.exports = function main () {}
// 典型策略, 共享引用修复此问题
module.exports = exports = function main () {}
exports.a = 1
node 如何处理循环依赖
答案
- CommonJS require 在执行前就把模块对象放入缓存;循环依赖时,下游拿到的是“部分初始化”的 module.exports(可能缺成员),执行完后再补齐。
- ESM:基于 live binding。依赖图先创建绑定,后求值;若在求值过程中读取尚未初始化的导出,会触发 TDZ 语义(ReferenceError)。导出函数/常量通常更安全。涉及 Top-level await 的循环会导致依赖图挂起并报错,应避免。
在使用 Top-level await 时,浏览器会丢弃无法正常加载的循环依赖,Node.js 会抛出警告
// CJS:得到“半成品”导出
// a.cjs
exports.ready = false
const b = require('./b.cjs')
console.log('b.ready in a:', b.ready) // 可能为 false(部分导出)
exports.ready = true
// b.cjs
exports.ready = false
const a = require('./a.cjs')
console.log('a.ready in b:', a.ready) // false
exports.ready = true
// ESM:不安全示例(TDZ)
/*a.mjs*/
import { fromB } from './b.mjs'
export const fromA = 1
console.log(fromB) // 可能触发 ReferenceError(fromB 依赖未初始化)
/*b.mjs*/
import { fromA } from './a.mjs'
export const fromB = fromA + 1
// ESM:安全模式(导出函数/延迟访问)
/*a.mjs*/
export function getA() { return 1 }
import { getB } from './b.mjs'
console.log(getB()) // 2
/*b.mjs*/
import { getA } from './a.mjs'
export function getB() { return getA() + 1 }
实践建议:CJS 避免在顶层立即使用来自对端的值,改为在函数内或异步回调中读取;ESM 避免在顶层读取尚未初始化的绑定,优先导出函数/常量。含顶层 await 的循环应打散(移到入口 await、或改用动态 import)。
讲一下 node:module 模块?
答案
node:module提供 Node 模块系统底层 API,解决 CJS/ESM 互操作与工具化需求- 关键 API
createRequire(在 ESM 中构造 require)、builtinModules/isBuiltin(内置模块检测)、findSourceMap/SourceMap(读取 sourcemap) - 使用
node:前缀如node:module、node:fs可避免与用户同名包冲突并利于静态分析 - 典型场景:ESM 中加载 CJS/JSON/.node,检测依赖是否为内置模块,调试与定位源码
示例说明:
// demo.mjs(Node 16+)
// 在 ESM 中使用 createRequire 加载 CJS/内置模块;检测内置模块
import { createRequire, builtinModules, isBuiltin } from 'node:module'
const require = createRequire(import.meta.url)
// 通过 require 访问内置模块(或第三方 CJS)
const fsByRequire = require('node:fs')
// 检测内置模块
console.log('isBuiltin("fs") =', isBuiltin('fs')) // true
console.log('builtinModules has fs =', builtinModules.includes('fs')) // true
findSourceMap/SourceMap 用于在带有 //# sourceMappingURL 的文件上读取并解析 Source Map,便于调试与错误定位。
用过 Events 模块么,讲解一下?
答案
- Events 提供 EventEmitter 发布-订阅机制;emit 是同步广播,监听回调按注册顺序执行
- 关键 API:
on/off(或 add/removeListener)、once、emit、prependListener、setMaxListeners、listenerCount - 特殊事件 error:若未注册 error 监听器,emit('error') 会抛出异常并使进程崩溃
- 监听数超过阈值(默认 10)会告警,可 setMaxListeners(0) 取消或调大阈值;避免意外内存泄漏
- 与 Web 的 EventTarget 不同:Node 的回调签名自由、无捕获/冒泡;Node 亦提供 node:events 的 once/ on 以 Promise/AsyncIterator 方式使用
未监听 error 时触发将抛出异常;生产环境务必统一注册 error 监听器或隔离风险域。
相关示例
- 基础使用
- 错误处理
- on/once Promise
- EventTarget
- 自定义事件对象
- 监听器告警
Terminal
Terminal
Terminal
Terminal
Terminal
Terminal
延伸阅读:
- Node.js: Events — EventEmitter API 与行为细节
- Node.js: EventTarget — 与浏览器模型差异与适用场景
- Node.js: once/on(Promise/AsyncIterator) — 以异步方式等待事件