在本篇文章中,我們將說明從
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 編譯器的效能緩慢。考量到建構 Chromium 和 DevTools 的工程師人數,這個瓶頸會造成不必要的成本。很遺憾,我們在遷移前無法識別這項風險,只有在將大部分檔案遷移至 TypeScript 後,才發現 Chromium 版本的耗用時間明顯增加:
https://crbug.com/1139220
我們已將這個問題
回報
給上游的 Microsoft TypeScript 編譯器團隊,但很遺憾,他們認為這項行為是刻意為之。我們希望他們能重新考慮這個問題,但在此同時,我們會盡可能減輕 Chromium 方面因效能緩慢而造成的影響。
很遺憾,目前可用的解決方案不一定適合非 Google 貢獻者。由於開放原始碼對 Chromium 非常重要 (尤其是 Microsoft Edge 團隊提供的貢獻),我們積極尋找可供所有貢獻者使用的替代方案。不過,我們目前尚未找到合適的替代方案。
目前,我們已從程式碼庫中移除 Closure 編譯器類型檢查器,並完全依賴 TypeScript 編譯器。我們可以編寫 TypeScript 撰寫的檔案,並使用 TypeScript 專屬功能 (例如介面、泛型等),這對我們日常工作很有幫助。我們對 TypeScript 編譯器能夠偵測類型錯誤和回歸的信心大增,這也是我們在最初開始這項遷移作業時所期望的結果。這次遷移作業和其他許多作業一樣,速度緩慢、細節繁多,而且經常面臨挑戰,但我們認為這一切都是值得的。
下載預覽管道
建議您將 Chrome
Canary
、
開發人員版
或
Beta 版
設為預設開發人員版瀏覽器。這些預覽管道可讓您存取最新的 DevTools 功能,測試最新的網路平台 API,並在使用者發現問題前,協助您找出網站的問題!
請使用下列選項討論新功能、更新或任何與開發人員工具相關的內容。
請前往
crbug.com
提交意見回饋和功能要求。
在開發人員工具中,依序按一下「more_vert」
more_vert
更多選項
>「Help」
>「Report a DevTools issue」
,即可回報開發人員工具的問題。
在 Twitter 上傳送訊息給
@ChromeDevTools
。
在
YouTube 影片「What's new in DevTools」
或「DevTools 提示」
YouTube 影片
中留言。
除非另有註明,否則本頁面中的內容是採用
創用 CC 姓名標示 4.0 授權
,程式碼範例則為
阿帕契 2.0 授權
。詳情請參閱《
Google Developers 網站政策
》。Java 是 Oracle 和/或其關聯企業的註冊商標。
上次更新時間:2021-04-08 (世界標準時間)。
[[["容易理解","easyToUnderstand","thumb-up"],["確實解決了我的問題","solvedMyProblem","thumb-up"],["其他","otherUp","thumb-up"]],[["缺少我需要的資訊","missingTheInformationINeed","thumb-down"],["過於複雜/步驟過多","tooComplicatedTooManySteps","thumb-down"],["過時","outOfDate","thumb-down"],["翻譯問題","translationIssue","thumb-down"],["示例/程式碼問題","samplesCodeIssue","thumb-down"],["其他","otherDown","thumb-down"]],["上次更新時間:2021-04-08 (世界標準時間)。"],[],[]]