这是 DEV 夏季 Bug 粉碎活动:粉碎故事的投稿,由 Sentry 提供支持。
你知道那些库和框架中的版本号,对吧?如果不了解,这里快速复习一下。当我们看到类似 [email protected] 时:
- x 表示主版本,允许包含破坏性更改,
- y 表示向后兼容的功能版本,
- z 是补丁版本,通常包含小幅修复和 bug 修复。
如果想保持项目健康,至少应该定期更新补丁版本。事实上,如今我们的包管理器通常会在允许的情况下自动完成更新。
然而有一天,仅更新第三个数字就彻底破坏了我们的应用。而这并非框架的错,而是我们自己的问题。
我在日常工作中修复了大量 bug。但要找出值得在比赛中讲述的 bug,需要回溯到令人惊讶的过去……
所以,让我们回到 2017 年。
欢迎回到 2017 年
Web 应用接管世界才几年时间。ECMAScript 6 已经存在两年,但我们仍非常谨慎地使用其革命性特性(Promise、箭头函数、let 和 const),因为浏览器支持还不够好。
市场对开发者的需求如此旺盛,以至于公司基本上会雇佣任何能写出:
export class
进入全屏模式 退出全屏模式
技术聚会充斥着诸如「Angular 入门」之类的演讲。不过,新的 Angular 本身还不到一岁。至少创建一个新项目只需一条 CLI 命令。与此同时,创建一个 React 项目仍感觉像是在下载 npm 的一半内容。
我是一名相当有抱负的中级开发者,所在团队几乎从 Angular 诞生之初就开始使用它。
该项目是一个大型企业级应用,用于监控公平贸易认证产品。它必须在全球范围内运行,从西欧到一些非洲小国,那里的人们可能每隔几周才从公共图书馆通过极其缓慢的连接接入互联网。
面向未来的设计至关重要。
我们基本上是在构建这个应用的同时学习现代前端开发。Observable 如何工作?RxJS 到底是什么?何时应该使用服务?
更麻烦的是,Angular 本身仍在成熟过程中,有时某些功能无法正常工作,仅仅是因为……嗯……Angular 存在 bug。我们提交了一个又一个 issue。
值得称赞的是,Angular 团队反应极其迅速。有时他们甚至在我们填写完 issue 模板前就修复了 bug。
自然,我们定期进行升级,包括次要版本和补丁版本。
Angular 早期 i18n 的问题
如前所述,我们的应用必须支持多种语言。Angular 已经有了一个 i18n 系统的雏形,但与我们今天所拥有的完全不同。无需深入实现细节,可翻译元素只需用 i18n 属性标记:
<h1 i18n>Hello</h1>
进入全屏模式 退出全屏模式
Angular CLI 会扫描应用并从这些元素生成翻译文件。
问题是每种语言都需要单独构建。运行时切换语言实际上不可行。而不幸的是,运行时语言切换是我们应用的一个硬性需求(我已经不记得具体原因了 😄)。
由于 Angular 生态系统仍非常年轻,且没有成熟的库能解决这个问题,我们决定构建自己的解决方案。
我们的实现非常简单。每当语言发生变化时,我们会扫描页面中所有包含 i18n 属性的元素,并用正确的翻译替换其内容。
优雅、简单,也许只有十五行代码。
补丁更新
一切运行完美……直到 2017 年年中。
Angular 现在是 4 版本。(我记得他们跳过了版本 3,原因非常合理,尽管我已经记不清那些原因了。😄)
有一天,我们进行了一次完全例行的 Angular 升级。我已经记不清确切的版本号了,但大致是从 4.2.4 升级到 4.2.8。
一次微小的补丁更新。不应该发生任何问题。但……确实发生了。我们的翻译,我们的整个国际化系统,全都不见了!
应用正常启动,一切看起来都没问题。但当我们尝试切换语言时……什么都没有发生。我们被困在了英文界面。
数字侦探工作时间
现在进入我最喜欢的部分: 软件取证。
起初,我完全排除了如此微小的 Angular 更新可能导致问题的想法。我们经常更新补丁。肯定是发生了其他事情。
当时,CI 流水线和自动化测试仍处于起步阶段。也许我们的测试人员只是有一两周没有切换语言?
我查看了 Git 历史记录。没有任何可疑之处。最近的提交都没有触及国际化。
也许是后端出了问题?翻译文件消失了吗?
不是。一切都在原位。
最后,我几乎不情愿地再次审视 Angular 的升级。
我开始倒推。一次又一次地检查补丁。果然……在两个微小的补丁版本之间,翻译突然停止工作。
所以 Angular 一定有罪。或者……真的是这样吗?
解开谜团
翻阅 Angular 的变更日志,我发现了一条变更,大致内容是:
为什么要保留生成 HTML 中不必要的
i18n属性?编译器已经完成了它需要做的所有工作。让我们移除它。
一切都突然明了。我们整个运行时翻译系统依赖于一个 Angular 从未承诺保留的实现细节。
对于 Angular 来说,移除该属性是一次完全合理的清理。对我们来说……它破坏了整个应用。
官方参考资料(如果你好奇)。正如你所见,我们不是唯一受影响的人 🤣:
- Angular 变更日志(Angular 4.2.6):「compiler: remove i18n markup even if no translations」
- PR #17999: https://github.com/angular/angular/pull/17999
- 原始 issue #11042: https://github.com/angular/angular/issues/11042
- 后续讨论 #20055: https://github.com/angular/angular/issues/20055 (我甚至在那里看到了我同事的评论 🥰)
修复
幸运的是,修复这个问题并不特别困难。只是……有些烦人。我们没有依赖 Angular 的内部 i18n 属性,而是创建了自己的指令。
它的名字?
fi18n。
我知道。工程创造力的巅峰。😂
我们是糟糕的工程师吗?
这个故事是否意味着我们是缺乏经验、不知道自己在做什么的开发者?
恰恰相反!我们在「这成为潮流」之前多年就成功实现了 运行时语言切换 😉
我们唯一犯的错误是假设我们偶然观察到的东西实际上是 Angular 的公共契约的一部分。事实并非如此。我们构建的解决方案依赖于一个 Angular 有权更改的实现细节。
最终……它确实更改了。
幸运的是,我们的应用当时离生产环境还很远 😅。所以在青春期的乐观中,我们逃过了这一劫。
为什么选择这个 bug?
在从事专业编程十多年后,为什么我会选择这个 bug,而不是我处理过的许多更大的生产事件?因为这个教训变得更加相关。
在 2026 年,前端开发看起来完全不同。没有人再做名为「Angular 入门」的演讲了。React、Angular 和 Vue 都是成熟的生态系统。无论你面临什么问题,很有可能已经有人解决了它。
然后是 AI,它已经能够处理大量常规编程工作。
但今天这种事情还会发生吗?绝对会。只是……可能不再是前端了。
今天,我们有一个全新的生态系统正在以惊人的速度发展:AI,尤其是 AI 代理。
这里是当今许多最大工程问题所在。会议和技术聚会不断涌现,因为没有人拥有所有答案。我们仍在撰写文章解释代理循环的工作原理,而这些甚至不是入门主题。像 MCP 这样的协议演进如此之快,以至于保持最新需要真正的努力。
这让我想起了 2017 年的前端开发。
而就像我们当时所做的那样,在开发过程中做出危险的假设非常容易。
有人用正则表达式解析模型的自由格式响应,而不是使用结构化输出。
有人假设模型总是会将 JSON 包裹在完全相同的 Markdown 块中。
有人围绕提供商返回的一个未记录字段构建业务逻辑。
有人假设完全相同的提示总是会触发完全相同的工具。
今天一切正常。明天可能仍然正常。下个月,一个微小的更新改变了一个看似无关紧要的细节……
……然后一切都崩溃了。
就像我们小小的 i18n 属性一样。
真正的教训
回想起来,我认为这个故事并不是关于 Angular 的。它关乎更普遍的东西。
作为工程师,我们常常将 可观察行为 误认为是 有保证的契约。仅仅因为某事今天存在,并不意味着作者打算让你依赖它。
如果你的解决方案依赖于未记录的行为,你就不是建立在坚实的基础上。你只是到目前为止一直很幸运。
但是!
这并不意味着我们应该为错误而自责。没有人能把所有事情都做对。重要的是从中吸取教训。
而这个特定的项目?它有一个幸福的结局。我几年前就离开了它。但这个应用仍然存在并且运行良好。我的大部分代码现在可能已经消失了……但我在写这篇文章之前检查了一下。
登录屏幕仍然看起来和我近十年前设计的一模一样。这让我笑了。真酷啊 🤣❤️
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.