模块系统中同名变量或类的优先级由路径解析与加载顺序共同决定,而非书写先后;关键在于导入路径是否完全匹配、本地路径优先于node_modules、mock必须在首次import前执行,且应通过分层导出或显式路径避免冲突。
模块化系统中同名变量或类的优先级问题,本质是路径解析与加载顺序共同作用的结果,不是靠“谁写得晚”决定,而是由模块系统在运行时如何定位、缓存和返回目标模块所控制。解决这类问题的关键,在于理解并主动利用层级加载逻辑,而非被动等待覆盖发生。
明确模块解析路径与命名空间边界
当两个同名类(如 Logger)分别存在于 ./utils/Logger.js 和 node_modules/@company/core/Logger.js 时,导入语句 import Logger from './utils/Logger' 和 import Logger from '@company/core' 实际指向完全不同的模块实体——路径不同,命名空间就不同。系统不会自动合并或比较它们。
- 不要依赖文件名一致就认为会冲突;真正起作用的是 导入路径字符串是否完全匹配
- 本地相对路径(
./、../)始终优先于node_modules中的包,这是 Node.js 的默认解析规则 - 若使用别名(如
@/services),需确认打包工具(Vite/Webpack)是否已正确配置别名映射,否则可能因路径不匹配导致加载失败或误加载
控制加载时机:避免缓存污染
模块一旦被加载并缓存(require.cache 或 ESM 的 module map),后续所有相同路径的 import 都会复用该实例。这意味着:如果某处提前加载了旧版 Logger,之后再 mock 或替换就无效。
- 在测试中,
vi.mock()或jest.mock()必须放在 任何 import 该模块之前,否则模块已缓存,mock 不生效 - 开发中若需热切换实现,可借助动态
import()+ 清除缓存(如delete require.cache[require.resolve('./Logger')]),但仅限调试,不可用于生产 - ESM 环境下无法直接清除缓存,应改用顶层
import.meta.url构造唯一路径,或通过构建时条件编译隔离不同实现
分层注入:用导出结构替代文件名竞争
与其让多个 Logger.js 文件争夺导入路径,不如统一入口、分层导出。例如:
- 建立
src/adapters/logger/index.ts,内部按环境或用途导出不同实现:export { default as DevLogger } from './DevLogger'、export { default as ProdLogger } from './ProdLogger' - 业务代码按需导入:
import { DevLogger } from '@/adapters/logger',彻底规避同名文件冲突 - 配合构建插件(如 vite-plugin-environment),在打包阶段注入对应实现,无需运行时判断
验证加载结果:不靠猜,靠查
遇到疑似优先级异常时,最有效的方式是直接检查模块实际加载路径:
- Node.js CommonJS 下,打印
require.resolve('./Logger')查看真实文件路径 - ESM 中可在模块顶部加
console.log(import.meta.url)确认当前模块身份 - Vite 项目可启用
server.hmr.overlay = false并查看控制台 HMR 日志,观察模块是否被正确替换 - 对第三方包,用
npm ls <package-name></package-name>检查实际安装版本及嵌套层级,确认是否存在多版本共存










