這是 DEV 的 Summer Bug Smash:Smash Stories 活動(由 Sentry 提供技術支援)的投稿。

你知道函式庫和框架中的版本號碼,對吧?如果不知道,這裡快速複習一下。當我們看到類似 [email protected] 這樣的版本號碼時:

  • x 是主要版本,允許有破壞性變更,
  • y 是向後相容的功能版本,
  • z 是修補版本,通常只包含小型修正和錯誤修復。

如果我們想讓專案保持健康,就應該定期更新至少修補版本。事實上,現在的套件管理工具如果允許的話,經常會自動幫我們更新。

然而有一天,僅僅升級第三個數字就徹底搞壞了我們的應用程式。而且這不是框架的錯,是我們的問題。

我在日常工作中修復了許多錯誤。但要找出值得在比賽中分享的那個錯誤,卻意外地讓我回溯到很久以前的記憶……

所以讓我帶你回到 2017 年


回到 2017 年

網頁應用程式才剛開始接管世界沒幾年。ECMAScript 6 已經問世兩年,但我們還是非常小心地使用它那些革命性的功能(Promises、箭頭函式、letconst),因為當時瀏覽器支援度還不夠好。

市場對開發人員的需求如此旺盛,任何會寫以下程式碼的人,基本上公司都會聘用:

export class

進入全螢幕模式 退出全螢幕模式

聚會中充滿了像是「Angular 入門」這樣的演講。嗯,新的 Angular 本身甚至還不到一歲。至少建立新專案只需要一個 CLI 指令。與此同時,建立 React 專案感覺就像要下載一半的 npm。


我當時在一個團隊擔任相當有野心的中階開發人員,這個團隊從 Angular 誕生之初就開始使用它。

這個專案是一個大型企業應用程式,用來監控公平貿易認證產品。它必須在世界各地運作,從西歐到小型非洲國家,在那些地方有人可能每隔幾週才從公共圖書館用極慢的連線上一次網。

未來相容性很重要。


我們基本上是在建構這個應用程式的同時學習現代前端開發。Observables 是怎麼運作的?RxJS 到底是什麼?我們什麼時候應該使用 services?

更糟的是,Angular 本身還在成熟階段,有時候東西就是不正常運作,因為……嗯……Angular 有 bug。我們一再提交問題。

值得讚揚的是,Angular 團隊的反應快得驚人。有時候他們在我們還沒填完問題模板之前就修復了錯誤。

自然地,我們定期進行更新,包括次要版本和修補版本。


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 內部的 i18n 屬性,而是建立了自己的指令。

它的名稱?

fi18n

我知道。工程創意的巔峰。😂


我們是糟糕的工程師嗎?

這個故事是否意味著我們是沒有經驗、不知道自己在做什麼的開發人員?

恰恰相反!我們早在「這變得很酷」之前就成功實作了執行階段語言切換 😉

我們唯一犯的錯誤是假設我們碰巧觀察到的東西實際上是 Angular 的公開合約。但它不是。我們在 Angular 有權變更的實作細節上建構了解決方案。

而最終……它確實變更了。

幸運的是,我們的應用程式還沒接近正式上線 😅。所以在我們年輕的樂觀下,我們逃過了一劫。


為什麼選擇這個錯誤?

在從事專業程式設計十多年之後,為什麼我會選擇這個錯誤,而不是我處理過的許多更重大的生產事故?因為這個教訓變得更加相關。

在 2026 年,前端開發看起來完全不同了。沒有人再發表「Angular 入門」這樣的演講。React、Angular 和 Vue 都是成熟的生態系統。無論你面臨什麼問題,很有可能已經有人解決了。

然後還有 AI,它已經可以處理大量例行的程式設計工作。

但這在今天還可能發生嗎?絕對可能。只是……可能不再是前端了。

今天,我們有一個全新的生態系統正在以驚人的速度成長:AI,特別是 AI 代理。

這是當今許多重大工程問題所在的地方。會議和聚會不斷出現,因為沒有人有所有的答案。我們仍在撰寫解釋代理迴圈如何運作的文章,而那些甚至不是初學者主題。像 MCP 這樣的協定發展得如此迅速,以至於保持最新狀態需要真正的努力。

這讓我想起 2017 年的前端開發。

而就像我們當時所做的那樣,在開發過程中做出危險的假設非常容易。

有人使用正規表達式而不是結構化輸出來解析模型的自由格式回應。

有人假設模型總是會將 JSON 包裝在完全相同的 Markdown 區塊中。

有人在供應商回傳的未記錄欄位上建構商業邏輯。

有人假設完全相同的提示總是會觸發完全相同的工具。

今天一切正常運作。明天可能還會正常運作。下個月,一個小小的更新改變了一個看似微不足道的細節……

……突然間一切都崩潰了。

就像我們那個小小的 i18n 屬性一樣。


真正的教訓

回顧過去,我不認為這個故事真的是關於 Angular。它是關於更普遍的東西。

作為工程師,我們經常將可觀察的行為誤認為是有保證的合約。某件事今天存在,並不意味著作者打算讓你依賴它。

如果你的解決方案依賴於未記錄的行為,那你不是建立在穩固的基礎上。你只是到目前為止都很幸運。

但是!

這並不意味著我們應該為錯誤而自責。沒有人能做對所有事情。重要的是從中學習。

而這個特定的專案呢?它有一個美好的結局。我幾年前離開了它。但這個應用程式仍然存在且運作良好。我的大部分程式碼現在可能已經消失了……但我在撰寫這篇文章之前檢查過。

登入畫面看起來仍然和我近十年前設計的方式完全一樣。這讓我笑了起來。這太酷了 🤣❤️