这次事件暴露的两个问题:编译器和编辑器的缓存是分开的,二者对于文件大小写和后缀进行了不同的默认隐式处理。 再加上系统对文件的处理不一样,事情就复杂了,例如windows文件不区分大小写这种。

一、背景与问题

昨天,我在一个 React 项目中遇到了非常诡异的白屏问题。
错误信息是:

⚠️ Warning: React.createElement: type is invalid – expected a string (for built-in components) or a class/function (for composite components) but got: undefined.
在这里插入图片描述

IDE 没有任何报错,类型系统也完全通过。这个错误提示没有任何作用,因为不是报错的组件有问题,而是它使用的一个组件有文件,例如组件B使用了错误的组件A,报错提示是B有问题。(但是要注意,这次组件A并没错误,只是暴露了编译器自主操作的弊端)

从网上找了一些解决办法毫无作用,由于和上次可运行时间不长,于是采用全注释和减少注释方式,定位到了是组件B的某个组件CachedImage ,因为注释掉它就好了。但我发现发现有导出,于是改了导出方式,从export default 加了个单独的 export { CachedImage }导出,但依旧没有变化。

而且vscode里面点这个变量还会跳到定义的页面,说明代码不存在问题 这时候让ai来检查处理,毫无用处,因为它们看得到上下文、但看不到缓存,哈哈哈哈。最后,我开始在主scope打印console.log(CachedImage) 可控制台一看,竟然是 undefined

最终的“灵光一现”,我唤起了操作记忆:
我有多个项目,为了瞎折腾,angular/react/vue都有,而且客户端和管理端我采用了不同的技术栈(无语,我存粹是不想让自己太舒服)。于是很多组件需要先实现一次再实现一次,这次的就是一个云存储OBS图片预览,因为给的url是不会缓存的,所以需要自己下载cache再显示,不然流量hold不住。

我显示在angular这边完成了组件开发 cached-image.ts就是我复制了html和ts内容的文件,拷贝到后台项目,让Ai重写为react写入到CacheImage.tsx 之后删除了旧文件,但 TypeScript 编译器/模块缓存系统仍然把旧文件的路径认为是同一个模块。改名后依旧存在命名冲突,导致新组件被识别为 undefined
当我手动更改文件名和组件名后,一切恢复正常。其实情理编译缓存也可以,但不希望下次build还等那么久…

这次事故让我彻底复盘了 TypeScript 的模块导入解析机制和大小写敏感问题,以及对后缀的隐藏处理。


二、TypeScript 模块解析机制简析

TypeScript 的模块解析可以理解为编译器在查找模块定义文件路径时的规则。
主要有两种模式:

1. Classic 模式(旧版)

这是早期的查找方式,不依赖 tsconfig.json 的配置,按相对路径和全局文件查找。

2. Node 模式(现代)

TypeScript 默认使用 Node 模块解析规则,与 Node.js 的 require() 查找逻辑相同,遵循以下顺序:

import { X } from './foo'

→ ./foo.ts
→ ./foo.tsx
→ ./foo.d.ts
→ ./foo/index.ts
→ ./foo/index.tsx
→ ./foo/index.d.ts

因此,如果你项目中存在多个文件名相似(甚至只大小写不同)的文件,TypeScript 在某些操作系统(尤其是 macOS 和 Windows,默认大小写不敏感的文件系统)上会缓存并混淆路径


三、为什么会出现“undefined”但不报错?

从这次例子来看,问题出在文件缓存 + 模块名冲突

  • 原有的文件是:cached-image.ts(小写+中划线)
  • 新文件是:CachedImage.tsx(首字母大写+驼峰)
  • 删除旧文件后,TypeScript 仍然认为两者是同一个模块(路径相同,只是大小写不同)
  • 编译器缓存的模块导出对象为空(undefined)
  • React 渲染时报错:type invalid

⚠️ IDE 能跳转但运行时报 undefined,是因为 IDE 的语言服务(tsserver)有自己的缓存索引,而运行时(webpack/Vite)使用的是物理文件系统缓存,两者不同步

这就解释了“能跳转但undefined”的现象。


四、路径解析与大小写问题的根源

1. 文件系统大小写敏感性差异

操作系统 文件系统 是否大小写敏感
Windows NTFS ❌(默认不敏感)
macOS APFS ❌(默认不敏感)
Linux ext4 ✅(敏感)

这意味着:

在 macOS/Windows 上,import CachedImage from './CachedImage'

import CachedImage from './cached-image'
都能“找到同一个文件”。

但在 Linux(或构建服务器)上,这两者是完全不同的文件


五、TypeScript 导入导出规则回顾

在排查此类问题时,也要留意导入导出方式不匹配的情况:

导出方式 对应导入方式 错误导入示例
export default A import A from './file' import { A } from './file'
export const A = ... import { A } from './file' import A from './file'
export * from './module' 取决于被导出的内容 -

编译器在类型层面能帮你检查很多语法错误,但如果路径被缓存成错误文件,就会导致:

编译通过 → 运行时报 undefined → React 渲染崩溃。


六、避坑建议(总结重点)

1. 文件命名统一规范

  • React 组件文件统一使用大驼峰命名法CachedImage.tsx
  • 工具文件、小函数使用小写中划线命名image-cache.ts
  • 避免两个命名仅大小写不同的文件存在于同一目录。

2. 删除/重命名文件后清理缓存

在以下场景后强制清理缓存非常必要:

  • 删除或重命名文件(尤其是仅大小写变化)
  • 改变文件扩展名(如 .ts.tsx
  • 迁移或复制文件到新目录

清理方式:

# Vite
rm -rf node_modules/.vite
rm -rf node_modules/.cache

# Next.js / Webpack
rm -rf .next
rm -rf node_modules/.cache

3. 启用 TypeScript 大小写检查

tsconfig.json 中添加:

{
  "compilerOptions": {
    "forceConsistentCasingInFileNames": true
  }
}

这样,当你在 Windows/macOS 上导入与实际文件大小写不一致的路径时,编译器会报错。


4. 使用 eslint-plugin-import 检查模块路径

安装 ESLint 插件自动检测:

npm i -D eslint-plugin-import

配置 .eslintrc

{
  "plugins": ["import"],
  "rules": {
    "import/no-unresolved": "error",
    "import/no-named-as-default": "warn",
    "import/no-named-as-default-member": "warn",
    "import/no-duplicates": "error"
  }
}

5. 避免同名不同后缀文件

例如:

cached-image.ts
cached-image.tsx
cached-image.d.ts

这样的结构会增加解析歧义,建议明确分层(例如 types/components/utils/)。


七、结语

这次“白屏事故”是一个活生生的例子,说明:

TypeScript + React 项目中的文件命名规范与模块缓存机制非常关键。

有时候不是语法错、不是导出错,而是编译器的缓存系统被大小写不敏感的文件系统坑了
真正理解 TypeScript 的模块解析逻辑,才能在关键时刻少掉几根头发。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐