ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Node系列 · Node基础:ES 模块化

Node系列 · Node基础:ES 模块化 Node系列 · Node基础ES 模块化CommonJS 是 Node 默认的模块系统但 ESM 才是 ECMAScript 规范本身。理解 ESM 的异步加载 静态分析特性就能解释为什么它能 tree-shaking、为什么必须写文件后缀、为什么与 CJS 互操作时要写default解构。一、ESM 与 CommonJS 的关键差异维度CommonJSESM规范归属Node 自定义实现ECMAScript 标准加载方式同步、运行时异步、静态分析关键字require/module.exportsimport/export文件后缀自动补全必须显式写Tree-shaking困难运行时才知道导出什么天然支持顶层await不支持支持Node 14.8适用老项目、Node CLI、配置文件现代前端、库发布、tree-shaking 场景::: infoESM 在 Node 14 已经很稳定。新项目默认 ESM维护老 CJS 项目不必迁移除非需要 tree-shaking 或与.mjs包互操作。:::二、启用 ESM 的两种方式2.1 用.mjs后缀文件后缀.mjs强制按 ESM 解析与package.json配置无关project/ ├── package.json └── app.mjsimport { readFile } from node:fs/promises; const data await readFile(./config.json, utf-8);2.2 在package.json加type: module整个项目除.cjs文件按 ESM 解析{ name: my-app, version: 1.0.0, type: module }project/ ├── package.json └── src/ ├── index.js ← 现在按 ESM 解析 └── util.cjs ← 显式按 CJS 解析即便在 typemodule 项目下::: tip混用场景项目主入口是 ESM但某个老依赖只能以 CJS 形式发布——把那个文件改成.cjs后缀即可。:::三、import语法3.1 命名导入 / 默认导入// 命名导出可以有多个 export const PI 3.14; export function add(a, b) { return a b; } // 默认导出一个模块只能有一个 export default class User { constructor(name) { this.name name; } }// 命名导入必须用花括号 import { PI, add } from ./export-demo.js; // 默认导入花括号外可以任意命名 import User from ./export-demo.js; // 混合导入 import User, { PI, add } from ./export-demo.js; // 重命名导入 import { add as sum } from ./export-demo.js; // 整体导入为一个命名空间对象 import * as utils from ./export-demo.js; console.log(utils.PI); // 3.143.2 路径规则ESM 下import的路径有 3 个强约束写法是否合法说明import x from ./foo.js✅必须带.js后缀import x from ./foo❌必须显式后缀CJS 会自动补全ESM 不会import x from foo⚠️走 npm 包解析同 CJS 的node_modules查找import x from node:fs✅Node 内置模块用node:前缀更规范::: warningESM 不补全后缀。老 CJS 项目里到处是require(./foo)迁到 ESM 后必须改成import x from ./foo.js。否则运行时报ERR_MODULE_NOT_FOUND。:::四、export语法4.1 命名导出 vs 默认导出// 命名导出导入时必须用同名 export const name Alice; export function greet() {} // 默认导出导入时任意命名 export default function () { return default function; }4.2 重导出聚合模块barrel 文件一个文件聚合多个子模块的导出export { Button } from ./Button.js; export { Input } from ./Input.js; export { Select } from ./Select.js;使用方只要import { Button } from ./components/index.js即可。4.3 重新导出并重命名export { foo as bar } from ./source.js; // 导出 source 的 foo但消费方叫 bar五、ESM 互操作实际项目里经常要 CJS 和 ESM 混用两种场景的互操作语法不一样。5.1 在 ESM 中importCJS 模块CJS 模块的module.exports整体被 ESM 当成默认导出// 一个普通 CJS 模块 module.exports { hello: () world, PI: 3.14, };// 在 ESM 里引用 CJS import cjs from ./cjs-module.js; console.log(cjs.hello()); // world console.log(cjs.PI); // 3.14如果 CJS 用module.exports.something ...拆成多个具名导出ESM 也能通过import { something }解构exports.foo 1; exports.bar 2;import { foo, bar } from ./cjs-named.js;::: warningNode 不做 CJS 的静态分析import { something }引用一个 CJS 模块时实际是运行后从module.exports解构。如果 CJS 用了动态赋值比如if (cond) exports.x ...ESM 拿不到。:::5.2 在 CJS 中requireESM 模块不允许——CJS 是同步加载ESM 是异步加载。Node 提供了两种方式绕过动态import()表达式import()不是声明是表达式返回 Promiseasync function load() { const { add } await import(./esm-module.mjs); console.log(add(1, 2)); // 3 } load();createRequire构造一个 CJS 风格的 require只用于加载 CJS 模块不能 require 一个 ESM。5.3 互操作矩阵调用方 \ 被调用方CJS 模块ESM 模块CJS 模块require()✅❌ 用动态await import()ESM 模块import default from ...✅import { ... } from ...✅六、顶层awaitESM 模块顶层允许直接await——这是 CJS 完全没有的能力const response await fetch(https://api.example.com/data); const data await response.json(); console.log(data);限制与注意点必须用在 ESM 模块.mjs或package.jsontypemodule模块的加载完成变成异步——所有依赖它的模块都必须等待不要在顶层await不会立即 resolve 的 Promise否则所有 import 它的模块都会被卡住// ❌ 危险长时间阻塞 await new Promise((resolve) setTimeout(resolve, 60_000)); console.log(所有人都得等我 60 秒);::: tip顶层await的最佳场景读配置文件作为模块初始化的依据一次性预热缓存 / 拉取启动数据单实例服务启动前的健康检查不适合长任务用户请求、消息队列消费不确定的资源获取:::七、ESM 的加载流程ESM 的异步、静态分析体现在加载流程文件系统Node ESM Loader入口 .mjs文件系统Node ESM Loader入口 .mjs所有依赖加载完成后才执行任意模块import ./a.js静态分析入口文件找出所有 import 语句并行读取 ./a.js / ./b.js / ./c.js文件内容构建依赖图拓扑排序执行入口文件CJS 是同步串行require(./a.js)一进来就读文件、执行完才返回。ESM 是并行预加载所有依赖文件并行读最后按依赖图顺序执行。八、ESM 与 CJS 的选择建议场景推荐理由新建 Node 项目ESM规范方向、生态趋势、tree-shaking写一个发到 npm 的库ESM同时支持 CJS via dual package下游用户两种生态都有维护老 CJS 项目继续 CJS迁移成本高收益有限CLI 工具CJS / ESM 都行单文件执行无依赖必须用同步requireCJSESM 不支持同步加载必须用__dirname/__filenameCJS或 ESM 下用import.meta.url转换见下一节九、ESM 下的__dirname等价物ESM 没有__dirname/__filename但能用import.meta拿到当前模块的 URLimport { fileURLToPath } from node:url; import { dirname } from node:path; const __filename fileURLToPath(import.meta.url); const __dirname dirname(__filename);import.meta携带了当前模块的元信息属性含义import.meta.url当前模块的file://URLimport.meta.dirname当前模块目录的路径Node 21.2import.meta.filename当前模块文件的路径Node 21.2import.meta.resolve(specifier)解析一个 specifier 为 URLNode 20.6Node 21.2 直接提供了import.meta.dirname和import.meta.filename不需要再fileURLToPath。十、常见错误错误信息原因解决ERR_MODULE_NOT_FOUND路径缺后缀或拼错写完整./foo.js检查文件名The requested module ./foo does not provide an export named XCJS 模块没module.exports.X改成import foo from ./foo默认导入await is only valid in async functions顶层 await 用在 CJS改.mjs或加type: moduleCannot use import statement outside a moduleCJS 文件里写了import改.mjs后缀或用requirerequire() of ES Module ... not supportedCJS 里同步 require ESM改用await import()十一、小结ESM 是 ECMAScript 标准CJS 是 Node 自定义实现。新项目默认 ESM启用 ESM 两种方式.mjs后缀 /package.json typemoduleESM 必须写文件后缀./foo.jsCJS 不会自动补全——这是迁移最常见的报错互操作ESMimportCJS ✅CJSrequireESM ❌用动态import()顶层await是 ESM 独有但只用于启动期一次性任务Node 21.2 提供import.meta.dirname/import.meta.filename简化 ESM 下的路径处理
返回列表