這篇文章是一系列網誌文章的一部分,說明我們對開發人員工具架構所做的變更,以及如何建構該架構。
繼遷移至 JavaScript 模組和遷移至 Web 元件之後,我們今天將繼續撰寫一系列網誌文章,說明我們對開發人員工具架構所做的變更,以及如何建構這個架構。(如果您還沒看過,我們發布了一部影片,說明我們如何將開發人員工具的架構升級至新式網頁,並提供 14 個改善網頁專案的訣竅)。
在本篇文章中,我們將說明從 Closure Compiler 類型檢查器改用 TypeScript 的 13 個月歷程。
簡介
考量 DevTools 程式碼庫的規模,以及需要為負責開發的工程師提供信心,使用型別檢查器是必要的。為此,開發人員工具在 2013 年採用了 Closure Compiler。採用 Closure 後,開發人員工具工程師就能放心進行變更;Closure 編譯器會執行類型檢查,確保所有系統整合作業的類型正確無誤。
不過,隨著時間過去,其他類型的檢查器在現代網路開發中變得相當普遍。其中兩個知名的例子分別是 TypeScript 和 Flow。此外,TypeScript 也成為 Google 的官方程式設計語言。雖然這些新的型別檢查器越來越受歡迎,但我們也發現,我們發布的回歸現象本應由型別檢查器偵測到。因此,我們決定重新評估所選的型別檢查器,並在 DevTools 中找出開發作業的後續步驟。
評估型別檢查器
由於開發人員工具已使用型別檢查器,因此我們需要回答的問題是:
我們要繼續使用 Closure Compiler,還是改用新的型別檢查器?
為了回答這個問題,我們必須評估型別檢查器的幾項特性。我們使用型別檢查器的目的在於提升工程師的信心,因此最重視的部分是型別正確性。換句話說:型別檢查器在發現實際問題時的可靠度如何?
我們的評估著重於我們已發布的迴歸情形,並判斷其根本原因。這裡的假設是,由於我們已使用 Closure Compiler,Closure 不會偵測到這些問題。因此,我們必須判斷是否有任何其他類型檢查器可以執行此操作。
TypeScript 中的類型正確性
由於 TypeScript 是 Google 官方支援的程式設計語言,且迅速受到歡迎,我們決定先評估 TypeScript。TypeScript 是一個有趣的選擇,因為 TypeScript 團隊本身就使用 DevTools 做為其中一個測試專案,追蹤與 JavaScript 類型檢查的相容性。他們的基準參考測試輸出結果顯示,TypeScript 會擷取大量類型問題,而 Closure 編譯器不一定會偵測到這些問題。這些問題中的許多問題可能是我們在發布時發生回歸的根本原因;這也讓我們相信 TypeScript 可能是 DevTools 的可行選項。
在遷移至 JavaScript 模組的過程中,我們發現 Closure Compiler 比先前發現更多問題。改用標準模組格式後,Closure 能更有效地瞭解我們的程式碼庫,因此型別檢查器的效能也隨之提升。不過,TypeScript 團隊使用的是 DevTools 的基準版本,該版本早於 JavaScript 模組遷移作業。因此,我們必須判斷遷移至 JavaScript 模組是否也能減少 TypeScript 編譯器偵測到的錯誤數量。
評估 TypeScript
開發人員工具已存在超過十年,並發展成功能豐富且體積相當大的網頁應用程式。在撰寫本篇文章時,DevTools 包含約 150,000 行第一方 JavaScript 程式碼。當我們在原始碼上執行 TypeScript 編譯器時,錯誤的數量之多令人難以招架。我們發現,雖然 TypeScript 編譯器會產生較少的程式碼解析錯誤 (約 2,000 個錯誤),但程式碼集仍會出現 6,000 個與類型相容性相關的錯誤。
這表示雖然 TypeScript 能夠瞭解如何解析類型,但在我們的程式碼庫中,它發現了大量的類型不相容情形。手動分析這些錯誤後,我們發現 TypeScript 在 (大多數情況下) 都是正確的。之所以 TypeScript 能夠偵測這些問題,而 Closure 無法偵測,是因為 Closure 編譯器通常會將類型推斷為 Any
,而 TypeScript 會根據指派執行類型推論,並推斷出更準確的類型。因此,TypeScript 確實更能瞭解物件結構,並找出有問題的用法。
這其中一個重要的重點是,在開發人員工具中使用 Closure 編譯器時,會經常使用 @unrestricted
。使用 @unrestricted
為類別加上註解,即可有效關閉 Closure 編譯器針對該特定類別的嚴格屬性檢查,這表示開發人員可以隨意擴充類別定義,而不會影響型別安全性。我們無法找到任何歷史背景,說明為何開發人員工具程式碼集中使用 @unrestricted
,但這導致在大部分程式碼集中,Closure 編譯器的運作模式較不安全。
我們也將回歸與 TypeScript 發現的類型錯誤進行交叉分析,結果顯示兩者有重疊之處,因此我們認為 TypeScript 可以避免這些問題 (前提是類型本身正確無誤)。
撥打 any
電話
此時,我們必須決定要改進 Closure Compiler 使用方式,還是要改用 TypeScript。(由於 Google 和 Chromium 都未支援 Flow,我們不得不放棄這個選項)。 我們與負責 JavaScript/TypeScript 工具的 Google 工程師討論並取得他們的建議後,決定選擇 TypeScript 編譯器。(我們最近也發布了一篇將 Puppeteer 遷移至 TypeScript的網誌文章)。
採用 TypeScript 編譯器的主要原因是改善型別正確性,其他優點則包括 Google 內部 TypeScript 團隊的支援,以及 TypeScript 語言的功能,例如 interfaces
(與 JSDoc 中的 typedefs
相反)。
選擇 TypeScript 編譯器,就必須大幅投資 DevTools 程式碼庫及其內部架構。因此,我們預估至少需要一年的時間才能完成遷移至 TypeScript (預計於 2020 年第 3 季完成)。
執行遷移作業
最關鍵的問題是:我們要如何遷移至 TypeScript?我們有 150,000 行程式碼,無法一次全部遷移。我們也知道,在程式碼集中執行 TypeScript 會發現數千個錯誤。
我們評估了多種選項:
- 取得所有 TypeScript 錯誤,並與「黃金」輸出結果進行比較。這個方法與 TypeScript 團隊的做法類似。這種方法最大的缺點是,由於有數十位工程師在同一個程式碼庫中工作,因此合併衝突的發生頻率很高。
- 將所有有問題的類型設為
any
。這會讓 TypeScript 抑制錯誤。我們沒有選擇這個選項,因為遷移的目標是類型正確性,而這會受到抑制。 - 手動修正所有 TypeScript 錯誤。這項作業需要修正數千個錯誤,因此耗時費力。
儘管預期需要投入大量心力,我們還是選擇了第 3 個選項。我們選擇這個選項還有其他原因:例如,我們可以稽核所有程式碼,並對所有功能 (包括實作方式) 進行十年一次的審查。從商業角度來看,我們並未提供新的價值,而是維持現狀。因此,很難證明選項 3 是正確的選擇。
不過,我們深信採用 TypeScript 可避免日後發生的問題,尤其是在回歸方面。因此,這項論點並非「我們正在增加新的商業價值」,而是「我們要確保不會失去已獲得的商業價值」。
TypeScript 編譯器的 JavaScript 支援
在取得支持並擬定在同一個 JavaScript 程式碼上執行 Closure 和 TypeScript 編譯器的計畫後,我們開始處理一些小型檔案。我們的做法主要是由下而上:從核心程式碼開始,逐步向上到達高層級面板。
我們在 DevTools 中預先在每個檔案中加入 @ts-nocheck
,藉此並行執行工作。「修正 TypeScript」的程序是移除 @ts-nocheck
註解,並解決 TypeScript 找到的任何錯誤。這表示我們確信已檢查每個檔案,並解決盡可能多的類型問題。
一般來說,這種做法幾乎沒有問題。我們在 TypeScript 編譯器中遇到了幾個錯誤,但大多數都很難發現:
- 具有傳回
any
的函式類型選用參數會視為必要:#38551 - 將屬性指派給類別的靜態方法會導致宣告中斷:#38553
- 宣告子類別時,如果子類別使用無引數建構函式,而父類別使用引數建構函式,則會略過子類別建構函式:#41397
這些錯誤凸顯出,在 99% 的情況下,TypeScript 編譯器都是可靠的基礎。沒錯,這些不明的錯誤有時會導致 DevTools 發生問題,但大多數情況下,這些錯誤都很難以察覺,因此我們可以輕鬆避開。
唯一會造成混淆的問題,是 .tsbuildinfo
檔案的不確定輸出結果:#37156。在 Chromium 中,我們要求任何兩個相同 Chromium 提交版本的輸出結果都必須完全相同。很遺憾,我們的 Chromium 建構工程師發現 .tsbuildinfo
輸出內容並非確定性的:crbug.com/1054494。為解決這個問題,我們必須對 .tsbuildinfo
檔案 (主要包含 JSON) 進行猴子補丁,並進行後續處理,以便傳回確定性的輸出內容:https://crrev.com/c/2091448
幸運的是,TypeScript 團隊解決了上游問題,我們很快就能移除解決方法。感謝 TypeScript 團隊接收錯誤報告並迅速修正這些問題!
整體而言,我們對 TypeScript 編譯器的 (類型) 正確性感到滿意。我們希望 Devtools 這個大型開放原始碼 JavaScript 專案,能協助 TypeScript 提供 JavaScript 支援。
分析後續影響
我們在解決這些類型錯誤方面取得了良好進展,並且慢慢增加 TypeScript 檢查的程式碼量。不過,在 2020 年 8 月 (遷移作業開始後 9 個月),我們進行了一次檢查,發現以目前的速度無法在期限前完成遷移。我們的一位工程師建立了分析圖表,顯示「TypeScriptification」(我們為這項遷移作業命名的名稱) 的進度。
TypeScript 遷移進度 - 追蹤需要遷移的程式碼行數
我們預估在 2021 年 7 月至 12 月期間,會完成所有未完成的訂單,但實際上,我們花了將近一年的時間才完成。與管理階層和其他工程師討論後,我們同意增加工程師人數,以便將支援遷移至 TypeScript 編譯器。我們將遷移作業設計為可並行處理,因此多位工程師在多個不同檔案上作業時,不會互相衝突。
此時,TypeScriptification 程序就成為全團隊的努力目標。在額外協助下,我們在 2020 年 11 月底完成遷移作業,比起最初的預估時間提前了 13 個月,也比我們預期提前了超過一年。
總計有 18 位工程師提交了 771 份變更清單 (類似於合併要求)。我們的追蹤錯誤 (https://crbug.com/1011811) 有超過 1200 則留言 (幾乎都是變更清單中的自動化貼文)。我們的追蹤表單有超過 500 列,列出所有要轉換為 TypeScript 的檔案、這些檔案的負責人,以及「TypeScriptified」的變更清單。
減輕 TypeScript 編譯器效能影響
我們目前面臨的最大問題是 TypeScript 編譯器的效能緩慢。考量到建構 Chromium 和 DevTools 的工程師人數,這個瓶頸會造成不必要的成本。很遺憾,我們在遷移前無法識別這項風險,只有在將大部分檔案遷移至 TypeScript 後,才發現 Chromium 版本的耗用時間明顯增加:https://crbug.com/1139220
我們已將這個問題回報給上游的 Microsoft TypeScript 編譯器團隊,但很遺憾,他們認為這項行為是刻意為之。我們希望他們能重新考慮這個問題,但在此同時,我們會盡可能減輕 Chromium 方面因效能緩慢而造成的影響。
很遺憾,目前可用的解決方案不一定適合非 Google 貢獻者。由於開放原始碼對 Chromium 非常重要 (尤其是 Microsoft Edge 團隊提供的貢獻),我們積極尋找可供所有貢獻者使用的替代方案。不過,我們目前尚未找到合適的替代方案。
開發人員工具中 TypeScript 的目前狀態
目前,我們已從程式碼庫中移除 Closure 編譯器類型檢查器,並完全依賴 TypeScript 編譯器。我們可以編寫 TypeScript 撰寫的檔案,並使用 TypeScript 專屬功能 (例如介面、泛型等),這對我們日常工作很有幫助。我們對 TypeScript 編譯器能夠偵測類型錯誤和回歸的信心大增,這也是我們在最初開始這項遷移作業時所期望的結果。這次遷移作業和其他許多作業一樣,速度緩慢、細節繁多,而且經常面臨挑戰,但我們認為這一切都是值得的。
下載預覽管道
建議您將 Chrome Canary、開發人員版或Beta 版設為預設開發人員版瀏覽器。這些預覽管道可讓您存取最新的 DevTools 功能,測試最新的網路平台 API,並在使用者發現問題前,協助您找出網站的問題!
與 Chrome 開發人員工具團隊聯絡
請使用下列選項討論新功能、更新或任何與開發人員工具相關的內容。
- 請前往 crbug.com 提交意見回饋和功能要求。
- 在開發人員工具中,依序按一下「more_vert」 更多選項 >「Help」 >「Report a DevTools issue」,即可回報開發人員工具的問題。
- 在 Twitter 上傳送訊息給 @ChromeDevTools。
- 在 YouTube 影片「What's new in DevTools」或「DevTools 提示」YouTube 影片中留言。