接手了一个拥有15万用户的前端应用,大部分代码是AI生成的
作者接手了一个内部AI应用的前端,代码质量堪忧,大部分由AI生成。面对15万用户,他决定逐步重构,改善代码质量,同时保持业务运行。
不久前,我在伦敦与一位老同事喝咖啡。我们曾一起参与过一个期权交易项目,我至今仍引以为豪——我们将一个过时、臃肿的Redux Saga重写成了真正优秀的代码库,尽管领域复杂。
他告诉我那边的项目依然进展顺利,而我随即向他倾诉当前团队遇到的困难。他打断了我对代码质量、AI垃圾等问题的抱怨,直接问道:
你现在有多少用户?
15万。
我们只有少数交易员。你不能说团队这样做不对。
我知道背景很重要,因为我们处于不同的领域,但这仍然让我思考界限在哪里。何时接受或拒绝变更?什么对业务真正重要?
老实说,我不确定。但那次对话时常萦绕在我心头,所以我想分享当前项目的状况以及我如何应对。
起点
大约六个月前,我加入了构建公司内部AI应用的团队。这个产品是公司业务运行的基石,自然备受关注,尤其是来自高层的关注。
代码质量堪忧。我并非刻薄,而且后面会承认自己也贡献了一些问题。这是小团队快速构建、严重依赖AI、且从未有安静的一周来清理的必然结果。
该项目始于2025年初,大量使用AI生成代码。我听说后端工程师经常端到端实现功能,这意味着也要接触前端。有一句关于AI的话让我觉得很有趣:
AI生成的代码看起来非常棒,直到它涉及到你的专业领域。
总之,以下是我发现的大致情况。
Web应用是一个TypeScript React应用,仍在使用react-app-rewired。如果你不了解,那感觉就像身处中世纪。构建耗时约十分钟。
检查工具是从随机模板设置的,从未集成到git工作流或CI/CD管道中。所以实际上没有检查,也没有编辑器检查。
只有少数几个单元测试。没有可用的端到端测试套件。
TypeScript,但很多地方用了any,手写的客户端类型与真实响应产生了偏差。
没有服务端状态管理器。每个获取数据的组件都重新实现了自己的加载和错误处理。
任何时候都有超过40个打开的拉取请求。花几天处理一个功能,回来时就会落后数百个提交,因此解决冲突成为常态。
一个手写的事件总线,任何组件都可以触发事件,任何其他组件都可以监听(重新实现Redux?!)。当然,事件名称在每个注册处硬编码,导致“my-event”变成“mY-event”。
localStorage被用来缓存不应该缓存的内容,包括不断增长的base64头像图像。有一次我们因达到localStorage大小限制而开始出现生产错误。
一个巨大的上下文包裹所有东西。工件、上传的文件、预设、集成,全都在同一个地方。
四种不同的文件上传方式,每种都略有不同(闻起来像是AI生成代码?哈哈)。
核心组件几千行,几乎全是回调、副作用、状态和标志,只在最后有一层薄薄的UI。
设计系统糟糕。到处是自定义Tailwind标记,大小、颜色、间距不一致。
有时我震惊于这些东西竟然能运行。
最明显的做法是什么都不做。它运行着,人们用着,没人愿意承担根本性改变带来回归的风险。
开玩笑,我不在乎。我无法每天在一个感觉像地狱的代码库中工作。这个领域很前沿,项目也令人兴奋,但那个仓库并不有趣,而我希望在工作中享受乐趣,并快速、自信地发布。
关于我的工作方式,有一点很重要:我认为在作业中使用AI生成代码是不负责任的。将团队中无人理解的代码交付给这么多人依赖的产品,最终会失败,而且通常由别人买单。我经常使用AI,但作为工具,有规则,来回修改,并在发布前阅读每一行代码。
我不会将我不理解的代码交付给这么多人依赖的产品。我需要能够将其掌握在脑中。这并非为了原则本身,而是我唯一能快速前进而不让事情变得更糟的方法。
所以,冒险开始了
我做的第一件事是迁移到Vite。抱歉Webpack,你的时代已经过去了。这花了我大约一周半的时间,将构建时间从大约十分钟缩短到不到两分钟,而且模块热重载不会因简单的className更改而触发整个页面重新加载。这类改进在演示中没人注意,但六个月后,它给整个团队节省了大量时间。
我确实感到压力,因为我并没有立即带来可量化的成果,但幸好我之前与这个管理团队合作过,处于被信任的甜蜜点,所以我拥有并且仍然拥有很多自由。
然后是认证。逻辑分散在几个不相关的组件中,这正让每个人都不敢触碰任何相关部分。我将它集中到一个地方,将整个应用置于一个单一入口之后,并删除了一堆处理访问、令牌和权限的过时代码。过程中出现了一些回归问题,随着出现而修复,之后一直稳定。
然后是类型。后端已经发布OpenAPI模式(谢天谢地),所以我们开始从这些模式生成类型,而不是在客户端手写并期望正确。诀窍是逐步采用,一次几个端点,而不是停止一切进行大规模重写。这部分值得单独写一篇文章,所以先保留。
然后是一堆小东西。检查工具已启用,尽管我们仍有积压代码需要清理才能将其集成到CI/CD中。更多测试,更重要的是,就新代码的外观达成一致,这样我们就不会继续增加混乱。更小的拉取请求,将逻辑从组件移到钩子中,不再使用any,不使用无根据的useEffect。要点是在我们慢慢修复其余部分的同时,阻止混乱继续增长。
当一些优秀的新员工加入,并且他们认同许多相同的原则时,我们开始扭转局面。当你不是孤军奋战,并且得到支持时,事情变得简单得多了。
移动端噩梦
我加入后不久,一个新需求出现了。我们需要一个移动应用,而且需要尽快完成(CEO对在公司AI应用在手机上的想法感到兴奋,以便在去见总统的路上使用)。
我们选择了Capacitor,它很棒,没有任何抱怨。但为了进入生产环境,我们不得不实施一些顶级的hack,每次想起都让我忍俊不禁。
事情从演示开始。我们需要快速展示些什么,所以在移动端重写了浏览器fetch以返回一些模拟数据,但至少显示了UI功能,并告诉自己之后会清理。
我们从未清理。代码堆积起来,变得太大了。所以今天移动应用发出的每一个HTTP请求都被我们自己的fetch版本拦截并转换为RPC调用,因为通过公司防火墙的唯一途径是一个端点,所有东西都必须通过它。
而流式传输,聊天依赖的部分,是最疯狂的部分。在移动端,SSE被Swift层转换为WebSocket以通过那个只支持套接字的端点,然后在无状态网关节点上转换回SSE,最后才到达最终实例。我们来回转换,以便它能够完成旅程。
想象一下,在这一切之上,添加重连逻辑和断线消息处理,事情会变得多么复杂。一点也不有趣。
我们正在制定具体计划,主要转向使用套接字,以避免这种仪式,并且也为了只有一个流,而不是同时有多个SSE连接(这是一个设计问题,但你能怎么办)。我们将使用react-socket作为编排器。它是开源的。如果你在处理流,可以看看它。
还有很长的路要走
我想诚实地说明这一点。我们开启了很多事情,完成得很少,还远未到一半。以下是在进行中的工作,而不是胜利宣言。
设计系统是其中之一。一个并行的CSS文件,加上迁移到Tailwind 4,继承现有样式表的优点并增加新内容。目标是逐个迁移每个组件,我们从低影响的组件开始,比如徽章、切换开关、模态框皮肤。
我们使用Storybook作为文档界面,每个组件都有完整说明和变体。还有很多组件待处理。
我们刚刚开始的另一件事是核心部分的重构。主要驱动者。AI聊天。
我们已经到了一个无法更改它而不引起回归的地步。并不是说你需要小心处理,而是触碰这个文件本身就是风险。在一处做小改动,就会在其他地方微妙地破坏某些东西,可能两周后你的代码上线后才发现。
最明显的症状是一个bug:开始新聊天时,如果之前的聊天还在运行,内容会泄露到新聊天中。你会打开一个全新的聊天,最后一条聊天的一部分会显示出来。真恶心。如果存储有一个顶层的chatId键,其他所有内容嵌套在它下面,这个bug本来可以很容易地避免。
那么计划是什么?我们不确定。首先将其分解成更小的部分而不改变行为,然后看看先处理什么以及顺序。诚实的总结是:前面还有很多工作。
最后的话
我意识到这篇文章读起来像Reddit上的吐槽。也许就是。但我写下来有两个原因。
第一个是伦敦的对话,我经常回想起来。15万用户是一个真实的答案。这并没有让代码不那么痛苦,但将问题从“这是否可以接受”转变为“修复它的成本是多少,团队是否准备好付出”。我们正在付出,缓慢地,在边缘上,同时保持发布。大多数时候,这感觉像是正确的速度。
第二个是给任何读到这篇文章的人,因为他们即将加入像我这样的团队,或者已经在一个团队中,想知道自己是否孤军奋战。你不是。现在有很多这样的代码库。出路并不光鲜:一次一个边界,一个提取的钩子,一个从模式生成的类型,一个移入Storybook的组件。这些都不会让路线图看起来令人印象深刻。但所有这些加起来。
如果你担心AI会取代你的工作,我认为这并非行业现状。AI正在产生更多代码,而不是更少。总得有人能够将其掌握在脑中。