ES Modules 反模式是破坏静态分析与模块边界的用法:禁用 export default {…}、避免 import *、禁止跨目录直连私有逻辑、不在顶层执行副作用。

ES Modules 的反模式不是语法错误,而是破坏模块系统设计初衷的用法——它让静态分析失效、tree-shaking 失效、依赖关系模糊、边界难以维护。规避关键在于回归 ESM 的核心特性:静态性、显式性、文件粒度隔离。
禁用 export default { ... } 对象字面量导出
这种写法看似简洁,实则混淆命名导出与默认导出语义,导致导入方式错配且无法被工具正确识别:
- ❌ 错误:在
utils.js中写export default { formatTime, debounce },再用import { formatTime } from './utils.js'—— 实际解构的是默认导出对象,但 import 语句期望的是命名导出,结果为undefined - ✅ 正确:统一用具名导出:
export function formatTime() { }、export const debounce = ...;或若必须默认导出,就用import utils from './utils.js'再解构utils.formatTime - ? 工具保障:启用 ESLint 规则
import/no-default-export或import/prefer-default-export(按团队约定二选一),配合 TypeScript 的类型检查可提前暴露解构失败
杜绝 import * 通配符导入组件模块
通配符导入掩盖真实依赖、阻碍代码剔除、引发命名冲突,尤其在组件库中极易失控:
Node.js 可靠编程技能。避免事件循环阻塞、异步陷阱、ESM 兼容问题和内存泄漏。触发词:Node.js 开发、async/await、流处理、性能优化、安全编码。
- ❌ 错误:
import * as Button from './Button.js'—— 无法知道用了哪些 API,也无法 tree-shake 未使用的Button.renderSVG等内部方法 - ✅ 正确:
import { Button, BUTTON_SIZES } from './Button.js';若组件是单一函数/类,可用默认导入:import Button from './Button.js' - ? 工程约束:在 ESLint 中启用
import/no-namespace插件,并配置"allowNamespaces": false;CI 流水线中加入import/no-unused-modules检查未使用导出
避免跨目录直连私有逻辑
模块边界应由文件+显式导出定义,而非靠目录层级“自觉”隔离。子模块直接引用兄弟目录下的工具函数,等于绕过入口收敛,破坏封装:
- ❌ 错误:
Card.js直接import { throttle } from '../../utils/throttle.js' - ✅ 正确:所有共享能力收口到
shared/或lib/层,如import { throttle } from '../shared/utils.js';私有辅助函数放在同目录下__internal__/或不导出的utils.js中 - ? 目录即契约:约定
src/components/Button/下只允许index.js(主入口)、Button.js(实现)、types.js(类型)导出,其余文件默认不可被外部 import
不在模块顶层执行副作用或赋值全局变量
ESM 是声明式、惰性求值的,但把 window.myLib = {...} 写在模块顶层,会因执行时序不确定而失败:
- ❌ 错误:
my-lib.js中直接写window.myLib = { init() {} };—— 非模块脚本可能在它执行前就尝试调用window.myLib.init() - ✅ 正确:将挂载逻辑封装为函数,由使用者显式调用;或改用模块化方式加载依赖方,如
<script type="module"> import './my-lib.js'; </script> - ? 安全兜底:若必须支持旧环境,用
document.addEventListener('DOMContentLoaded', ...)包裹挂载,或加一层存在性判断:if (typeof window !== 'undefined') window.myLib = ...










