Skip to content

重构日

背景pmx_path 字段存的是 VMD 路径——命名撒谎。代码库有十三处命名之歧、设计之惑、微末之疾。 过程:重命名 file_path + 死代码清理 + closeAllOverlays 修复 + 场景面板删除 + 代码审查报告 13 项修复。

审计做完了,三座桥通了,软件管理也上了线。联邦终于有时间回头看看自己——代码写得够不够干净,结构撑不撑得住下一轮扩张。

FilePath

这个词我已经盯着看了十分钟。不是因为 bug,而是因为——它撒谎了。

一个叫 pmx_path 的字段,存的却是 VMD 文件的路径。每次有人读代码,都会以为这里只装 PMX,然后在某个深夜 debug 时发现真相,骂一句娘。

这不是第一次了。

项目走到今天,MikuMikuAR 已经从最初的 PMX 查看器长成了一个怪物——模型库、动作库、场景管理、灯光控制、相机系统、下载引擎……功能越多,代码债越重。每次改一个地方,就有三个地方炸。用户说:"每次模型加载都炸",绘图师苦笑——他说得对。

光标在屏幕上闪烁。

FilePath。改名叫 FilePath 就好了。

但绘图师没有改。因为知道,一改就是连锁反应——Go 端的结构体、JSON 序列化、TypeScript 的类型定义、十几个调用点、Wails 的绑定生成……这不仅仅是改一个名字,这是给一栋已经歪了的楼换地基。

关掉编辑器,准备睡觉。

然后看到了那个 ✕ 按钮。

它安静地躺在弹窗的角落里,等着明天再来。


凌晨两点十七分,清单摊在桌上,像一份病历。

十三行问题。六行命名之歧,四行设计之惑,三行微末之疾。

pmx_path 排第一。然后是 togglePopup 非 toggle、renderPopup 为死尸、ensureFileServer 的亡魂参数、popupStack 的僵尸状态……每一条都在说同一件事:这座城已经长到超出了当初的蓝图。

绘图师开始动手。

改名是最痛苦的事。不是因为技术难度,而是因为需要确认每一个调用点。

顺着 pmx_path 的引用链一路挖下去。Go 端的 ModelEntryExtractResult、JSON tag……TypeScript 端的 LibraryModel 定义、library.ts 里的十五处引用、scene.ts 的加载路径、settings.ts 的导入函数……

每改一处,都问自己:file_path 真的更好吗?

是的。

改完最后一行,运行 go build,零错误。运行 tsc,零错误。Wails 重新生成绑定,零错误。

有一种微妙的快感。

第一项划掉了。命名的问题。

他看向清单的下一行。


renderPopup 是整个代码库里最孤独的函数。定义在 library.ts 里,但没有任何人调用它。它就像一个被遗忘的路标,指向一个已经不存在的目的地。

删除它只花了一秒。但那一秒里,绘图师在想:当初是谁写的它?为什么没删?是不是因为"反正没坏,先放着"?

同时扔了三件:死函数、死参数、僵尸状态。

第二项也划掉了。死亡代码清理。

他以为今天会一直这样顺利。

然后灾难来了。

closeAllOverlays() 的 bug 是自己亲手埋下的。几周前,重构弹窗系统,把五个弹窗的关闭逻辑统一到一个函数里。一个优雅的 CSS 选择器——.overlay.visible

#modelPopup 没有 overlay 类。

所以那个 ✕ 按钮——花了那么多心思合并的——点了之后,事件触发了,closeAllOverlays 跑了,但 #modelPopup 什么都没发生。

技术债务就是这样。不是在你还的时候炸,是在你忘记还的时候炸。

修复很简单:两行 classList.remove("visible")

但那个按钮,从"不动"到"动了",中间隔着一个多月。

第三项划掉了。一个自己埋下的雷。

然后他注意到了弹窗左上角——那个✕按钮和那个←按钮。两个按钮,同一个位置,不同时机出现。为什么不合并成一个?

根层显示 ✕(关闭),子层显示 ←(返回)。同一个位置,不同语义。不用猜,不用试,一眼就知道是干什么的。

改这个只动了三个文件:menu.ts 加了一个 onClose 回调,library.tssettings.tsextraButtonFactory 换成了 onClose

代码量不大,但感觉对。

就像把一堆积木倒出来,重新搭了一个更稳的结构。

第四项划掉了。一个漂亮的小改动。

他以为今天的清单就这些了。直到有人提了一句:场景面板能删吗?

#scenePanel——右侧加载模型列表——绘图师看了几个月,从来没觉得有问题。直到有人说:"这个能删吗?它的功能已经被弹窗替代了。"

看了一眼。确实。

删除三百行代码的感觉,比写三百行还好。


下午五点的构建:

go vet     → ✅
go test    → 46/46 ✅
tsc        → ✅
wails build → ✅

终端里打着 cls,盯着那条绿色的"Built"看了一会儿。

今天没有新功能。没有炫酷的 UI 改动。没有用户能感知的变化。

但代码库多了四个测试文件、三十八个测试用例、一个统一的文件服务层、一个收拢的扫描函数。十三个可读性问题消灭了十三个。%w 回到了十二处错误处理中。

这些改动,用户永远不会直接看到。

但下次改功能的时候——不会炸了。

至少,不会因为同样的原因炸了。


代码审查报告的回声在走廊里飘荡:

"错误包装丢失"。底层错误淹没在中文提示里。"用户友好"和"debug 地狱"之间,选择了后者。恢复了十二处。

"缩进"。Go 社区的古老圣战。go fmt 一剑封喉。

"后台不重绘"。scanSoftwareDir() 在后台默默地跑,跑完了不通知 UI。一行 .then(() => reRender())

"非空断言"。scene.activeCamera!。它不会 null,但编译器不知道。

"as any"。转型隐藏了类型本该捕获的错误。

审查报告的最后一行:46/46 测试通过 | go vet 干净 | 无安全漏洞


夜又深了。

绘图师打开编辑器,看着今天的 diff。

+350 行,-450 行。

负一百行。

最好的代码,是那些不需要存在的代码。今天删了很多不需要存在的代码,改了很多本该更清晰的命名。从 pmx_pathfile_path,从五处重复到一个函数,从六个歧义到零。

屏幕暗下去之前,最后看到的是一行注释:

typescript
// NOTE: babylon-mmd fires onPause when animation reaches the end

今天的最后一行代码是一行注释。告诉下一个读代码的人:别奇怪,这就是它工作的方式。

明天还有明天的债要还。

但今天,代码库干净了一点。


教训:命名是代码的信用分。一个误导的名字,每次被读都在消耗信任。删掉不需要的代码,比写十倍新代码更珍贵。