这个项目最初的判断是"已经实现了一些功能"。它确实能跑,四个自带 CSS 的效果组件都活着。但只要把 tsc -b 跑一遍,退出码就是 2,13 个错误。
真正的发现是:这 13 个错误没有一个是类型问题,它们全都是结构问题在类型系统里的投影。
错误一:构建产物里还留着 @tailwind
src/index.css 开头三行是 Tailwind 的 v3 指令:
@tailwind base;
@tailwind components;
@tailwind utilities;而 package.json 里没有 tailwindcss,没有 postcss,根目录也没有任何配置文件。
字符串本身不会报错,所以编译器和浏览器都很安静。唯一的证据在产物里:
$ head -c 120 dist/assets/index-DDREM-vb.css
@tailwind base;@tailwind components;@tailwind utilities;body{color:#fff;...三行指令原样保留,说明 Tailwind 的编译从未发生。于是 App.tsx 和 ui/*.tsx 里 91 处工具类全是死代码:flex、min-h-screen、text-8xl、backdrop-blur-xl、md: 响应式,一个都不生效。
页面看起来"部分实现了",是因为那四个 ReactBits 组件各自带了独立 CSS 文件。视觉效果全由例外支撑,规则本身是坏的。
错误二:两个未导入的标识符
App.tsx 第 359 行用了 <Dock />,import 列表里没有 Dock;第 332 行用了 <X />,X 从未定义。
这不是"少个图标"。Dock 未定义意味着渲染时抛 ReferenceError,整棵 React 树挂掉,页面白屏。而我当时看到的"能跑",其实是浏览器缓存里旧版本的错觉。
// 之前
import { Tabs } from './components/ui/Tabs'
// ... 中间 400 行
<Dock items={items} /> // ReferenceError: Dock is not defined
// 之后:导入收敛到一处,图标统一交给 lucide-react
import { ArrowRight, Info, Star, X } from 'lucide-react'
import Dock from '@/components/effects/Dock'错误三:noUnusedLocals 把墓碑照出来了
剩下 10 个错误几乎都是"声明但未使用":Tooltip、Dialog 全家、Trash。
这类错误通常被当成噪音,noUnusedLocals: false 一关就没了。但在这里它精确地指出:有一整套组件写完了没接入。Dialog 写好了,页面用的是原生 <dialog> 加 document.getElementById;Tooltip 写好了,App.tsx 传的参数和它的 API 对不上。
关掉这条规则,等于把唯一能发现"写完没接线"的探照灯关掉。
"假组件"比 bug 更贵
比未使用更隐蔽的,是实现了外观但没实现行为的组件。
| 组件 | 表面 | 实际 |
|---|---|---|
Tabs | 三个 tab 按钮 | 没有 context,data-state 恒为 inactive,三段内容同时渲染 |
Tooltip | 悬停提示 | 没有 hover 逻辑,内容常显,且 API 要求 content prop |
cn() | 类名合并 | 手写拼接,去重逻辑写错了,不处理 Tailwind 冲突 |
这类代码的问题是它消耗的是信任。测试不会失败,类型不会报错,页面甚至看起来是对的——直到有人点了一下。
结构比修复顺序更重要
修完这些错误后我做的第一件事不是庆祝,而是把 407 行的 App.tsx 拆掉:
src/
├─ App.tsx # 30 行,只决定 section 顺序
├─ sections/ # 4 个分区各一个文件
├─ components/effects/ # 效果组件与自己的 CSS 同层
├─ components/layout/ # 导航
├─ hooks/ # useActiveSection
├─ data/ # 分区清单,导航与高亮共用
└─ lib/ # cn()拆分之后,同样的 13 个错误里有一半根本没有发生的空间——因为"声明了没接线"需要一个地方同时容纳声明和接线,而拆分把这个空间消灭了。
收获
- 依赖缺失必须是构建期失败。 一个引用了不存在插件的 CSS 文件,应该让构建挂掉,而不是静默输出死指令。
- 不要关掉
noUnusedLocals。 它是最便宜的死代码检测器。 - 判断"实现了"要看向产物,不是看屏幕。 缓存会让旧构建看起来像新功能。
结构修完只代表"能跑"。要让它被搜索到、被分享出摘要,还得让每个路由在构建期就产出 HTML —— 见 预渲染的三道闸门。