我們用了一個周末,將 370 萬行代碼遷移到了 TypeScript
TypeScript 是增長最快的語言之一,最近幾年逐漸成為很多大廠的首選工具。最近,Stripe 將最大的 JavaScript 代碼庫(用于支持 Stripe Dashboard 功能)從 Flow 遷移到了 TypeScript。于是通過單一 PR 請求,轉(zhuǎn)換了超過 370 萬行代碼。第二天,幾百名工程師快速跟進,開始為自己的項目編寫 TypeScript。
TypeScript 目前已經(jīng)成為 JavaScript 類型檢查的客觀標準,Stripe 已經(jīng)把這次使用的 TypeScript 轉(zhuǎn)換工具分享到 GitHub(https://github.com/stripe-archive/flow-to-typescript-codemod),希望幫助更多朋友能夠輕松完成類似的大規(guī)模遷移。
以下是他們遷移的具體步驟:
1
Stripe 的 JavaScript 類型檢查簡史
Stripe 是一款誕生于 2012 年的大規(guī)模前端應(yīng)用程序,共包含 stripe.com、Stripe JS 和 Stripe Dashboard 幾大組成部分。隨著業(yè)務(wù)的發(fā)展,我們開始對 JS 代碼進行類型檢查以提高產(chǎn)品質(zhì)量和可靠性。2016 年,我們率先采用了 Flow——這是 Facebook 當時專門開發(fā)的 JavaScript 類型檢查系統(tǒng)。之后幾年間,F(xiàn)low 一直為我們大部分前端應(yīng)用程序的類型安全保駕護航。
為 API 資源和關(guān)聯(lián)端點生成的 Flow 類型示例。
然而,工程師們在實際使用中發(fā)現(xiàn) Flow 仍有諸多不足。首先,這款類型檢查器會輕松耗盡筆記本電腦的內(nèi)存,而編輯器內(nèi)集成也速度緩慢、可靠性低下。與此同時,微軟開發(fā)的另外一種類型系統(tǒng) TypeScript 卻在異軍突起,憑借著完善的工具組合和強大社區(qū)而廣受好評。于是,越來越多的 Stripe 工程師呼吁轉(zhuǎn)向 TypeScript。
Stripe 擁有專門的開發(fā)者生產(chǎn)力團隊,職責就是為工程師們提供最高效、最順手的開發(fā)環(huán)境,所以他們的滿意度就是生產(chǎn)力團隊的使命。我們一直在努力確定開發(fā)者們最關(guān)心的緊迫問題:例如,我們在全部開發(fā)工具中都集成了上報錯誤 / 不便的功能,確保將情況快速發(fā)送給相關(guān)團隊以評判優(yōu)先級。對 TypeScript 的支持已經(jīng)相當緊迫,于是支持團隊決定在整個公司內(nèi)幫助前端工程師轉(zhuǎn)向 TypeScript。
2
選擇正確的遷移策略
在所有前端代碼庫中,最大的那個負責為 Stripe Dashboard 和其他一些面向用戶的產(chǎn)品提供支持。Dashboard 代碼庫中的不同組件保持著緊密耦合,而且沒有清晰拆分的依賴圖表。如果選擇面向 TypeScript 開展增量遷移,就意味著開發(fā)人員在一段時間內(nèi)必須同時使用兩種語言來完成常見任務(wù)。此外,我們還需要一個互操作層來同步兩種語言之間的類型定義,并在整個開發(fā)過程中始終保持二者一致。
2020 年末,我們組建了一支新的橫向 JavaScript 基礎(chǔ)設(shè)施團隊:在這里,工程師們只關(guān)注一項工作——提升 Stripe 編寫 JS 代碼時的體驗。而團隊的首要挑戰(zhàn)之一,就是用 TypeScript 替換掉 Flow,同時回避掉漫長且充滿不確定性的遷移過程。
我們首先與其他開展過類似遷移的企業(yè)進行交談,并參考了 Airtable 和 Zapier 的經(jīng)歷回顧文章。這兩家企業(yè)都開發(fā)出自動化腳本,用于將一種語言轉(zhuǎn)換成另一種語言、貫穿整個代碼庫運行,再把輸出結(jié)果合并成單一提交。Airtable 已經(jīng)把自己的轉(zhuǎn)換腳本以“codemod”源到源轉(zhuǎn)換工具的形式上傳至 GitHub,它就完全能夠解析 Flow 代碼并生成相應(yīng)的 TypeScript。
這種遷移方式大大降低了工程師們的工作負擔,也不需要為相同的產(chǎn)品維護兩套類型系統(tǒng)。這么一看,從 Flow 到 TypeScript 的道路頓時平坦了起來。
3
規(guī)劃、籌備和迭代
Airtable 工具那出色的代碼轉(zhuǎn)換質(zhì)量給我們留下了深刻印象,于是 Stripe 決定把它作為遷移工作的基礎(chǔ)。這里要感謝 Airtable 團隊開發(fā)并分享的這份工作成果——開源社區(qū)正是在無數(shù)這類案例的支持下,才變得如此興盛蓬勃。
我們首先將 Airtable 的 codemod 復制到 Stripe 的 monorepo 當中,從而指向內(nèi)部代碼來運行。我們的 JS 項目中大量用到了 Sail——一個由嚴格類型化 React 組件構(gòu)成的共享設(shè)計系統(tǒng),所以我們決定在遷移之初先從 Sail 入手。
我們?yōu)?Sail 生成了 TypeScript 定義,而非直接把代碼轉(zhuǎn)換成 TypeScript,這樣就能保證它同時支持用 Flow 和 TypeScript 編寫的應(yīng)用程序。為了安全支持這兩種類型系統(tǒng),我們編寫了測試來驗證 TypeScript 定義對于底層 Flow 代碼做出的具體更改。這種方法對于大規(guī)模代碼庫來說可能太過麻煩,好在 Sail 組件擁有明確且嚴格的接口,所以我們的測試倒是相當順遂。
還有個問題,codemod 的底子很好、但功能并不全面:對于很多文件,它在轉(zhuǎn)換中可能發(fā)生崩潰,輸出結(jié)果也不夠完善。所以在幾個月時間里,我們通過一次次迭代解決了這些較為極端的句法和語義案例。
舉個簡單的例子,JS 箭頭函數(shù)可以在沒有 return 語句時直接返回單一表達式,如下所示:
const linesOfCode = () => 7;
JS 對象字面量會使用大括號來體現(xiàn)屬性定義。但因為大括號也被用于描述語句塊,所以要從箭頭函數(shù)返回對象字面量,還需要引入一組額外的括號來消除歧義:
const currencyMap = () => ({ca:'CAD',us:'USD'});
我們注意到,codemod 會錯誤刪除掉箭頭函數(shù)中這些額外的括號,但這個問題只發(fā)生在泛型函數(shù)(接受類型參數(shù)的函數(shù))當中。可一旦刪除,結(jié)果語法在標準 JS 中將不再可用:
// bad!
const wrapper = (arg:T) => {wrapped: T};
于是我們修復了這個問題,并添加測試以防止其再次發(fā)生。整個遷移過程中,我們進行了大量類似的語法修復,才最終讓之前龐大的代碼庫“舊月的換新顏”。
在確保 Sail 能夠在 TypeScript 中正常起效之后,我們又開發(fā)了幾個包含數(shù)百個 JS 模塊的內(nèi)部應(yīng)用程序。我們還向 codemod 中添加二次檢查,希望進一步減少生成代碼中的錯誤,同時使用 TypeScript 的 @ts-expect-error 注釋來標記這些錯誤。可以看到,我們的基本思路并不是提前解決掉每個錯誤,而是盡快替換掉 Flow,并在過程當中跟蹤實際發(fā)生的 TypeScript 錯誤抑制并加以解決。
Dashboard 代碼庫的初始階段共引發(fā)超過 97000 個錯誤抑制。在更新了 codemod 的迭代方法之后,這個數(shù)字被控制到了 37000 個,相當于每千行代碼有 1 個錯誤抑制。相比之下,F(xiàn)low 代碼這邊的錯誤抑制大概是 5000 個。
Flow 和 TypeScript 都支持對類型覆蓋率進行測量,而我們驚喜地發(fā)現(xiàn)雖然 TypeScript 這邊的抑制數(shù)字更大,但這主要是因為其報告覆蓋率要比 Flow 更高。這應(yīng)該是因為 TypeScript 中的可用第三方類型定義在數(shù)量和質(zhì)量上都優(yōu)于 Flow,而后者則因為缺少這些定義而導致類型覆蓋率不足。
不過面對包含數(shù)萬個模塊的 Dashboard 時,我們的方法對 TypeScript 編譯器產(chǎn)生了巨大的內(nèi)存壓力。而解決這個問題的主要工具,就是 TypeScript 項目引用:盡管 Dashboard 并不進行模塊區(qū)分,但我們還是正確推斷出了它的模塊結(jié)構(gòu),并據(jù)此建立起項目引用。通過這種方式,我們得以直接在代碼庫之上運行 TypeScript,且無需重構(gòu)大量應(yīng)用程序代碼。
4
正式上線
每周,都有數(shù)百名工程師在奮力推進 Dashboard 項目的遷移工作。但如此徹底的變動不容小覷,我們也不想在周內(nèi)工作量合并這些更新。因此,團隊決定選擇 3 月 6 日星期天鎖定 Stripe monorepo,同時上線我們的新分支。
在合并前一周,我們開始通過 CI 系統(tǒng)將 build 傳遞并部署到 QA 環(huán)境當中。畢竟除了 TypeScript 對項目本體的檢查之外,我們還得更新 ESLint、Jest、Webpack、Metro 等負責處理源代碼的其他工具。
這里出現(xiàn)了一個特別的痛點:Jest 快照測試。Jest 生成的快照文件中,會包含一條對快照生成文件的硬編碼引用。由于 codemod 會給 TypeScript 文件生成.ts 或者.tsx 的擴展名,所以快照文件所引用的測試源將直接失效。為此,我們決定把生成文件的擴展名統(tǒng)一成.tsx,這樣就可以批量重寫快照并保證測試 100% 通過。
此外,我們還發(fā)現(xiàn)對某些 TypeScript 兼容代碼的修復會帶來不少工作量,甚至把日程安排推遲數(shù)周。其中的典型案例就是我們自定義的 ESLint 規(guī)則:其中一項規(guī)則會重新排序?qū)胍詮娭票WC各文件間的一致性,但該規(guī)則是針對 Babel 的 Flow 解析器編寫的,所以生成的抽象語法樹與 TypeScript 解析器會略有不同。在這種情況下,我們決定先禁用某些檢查,并在轉(zhuǎn)換完成后再行恢復。
通過手動上傳 build,我們在 Dashboard 中與面向用戶功能的產(chǎn)品團隊成功會合。盡管 Dashboard 擁有廣泛的單元和功能測試,但端到端測試覆蓋率卻比較有限。因此,各產(chǎn)品相關(guān)方就必須有能力開展手動測試。測試中同樣暴露出不少小 bug,我們搶在最后一周成功將其解決:例如,由于翻譯加載代碼中存在一個硬編碼.js 擴展名,因此我們無法為非英文版 Dashboard 用戶正確加載翻譯內(nèi)容。
整個過程給了我們很大信心,但這種顛覆性的變更還是讓大家有點忐忑:雖然我們牢牢掌握著開發(fā)工具和構(gòu)建過程,但畢竟代碼庫中的每個文件都發(fā)生了變化。轉(zhuǎn)換腳本中的任何一點細微錯誤(例如從多個組件間共享的對象中刪除一個空字段)都有可能引發(fā)面向用戶的錯誤,而任何現(xiàn)有自動化測試都發(fā)現(xiàn)不了這樣的錯誤。
另外,這類故障可能會有多種表現(xiàn)方式,例如引發(fā)下游開發(fā)工具報錯、或者導致構(gòu)建失敗等。為了及時發(fā)現(xiàn)這些意外狀況,我們只能依靠自動化與環(huán)境監(jiān)控工具,同時建立了專門的協(xié)調(diào)部署 Slack 頻道,保證面向用戶的團隊能夠及時收到報告并快速著手修復。
3 月 5 日星期六,團隊生成了新的遷移分支并運行了我們的自動化腳本。之后,我們將該分支部署到 QA 環(huán)境并重復驗證過程,包括產(chǎn)品團隊提議的手動測試。期間沒有發(fā)現(xiàn)任何新問題,看起來一切合并準備均已就緒。
3 月 6 號星期天一大早,我們就鎖定了 Stripe monorepo,又對遷移分支進行衛(wèi)次 QA 測試,之后果斷提交了變更。整個合并過程干凈利落,我們的自動化測試也全部通過。就這樣,TypeScript 順順當當進入了生產(chǎn)部署。
憑借這一年來的細心調(diào)整與嚴謹測試,新代碼在接收生產(chǎn)流量后沒有發(fā)生任何意外。我們隨后解鎖了 repo,讓開發(fā)者們看到現(xiàn)在的 Dashboard 已經(jīng)運行在 TypeScript 當中了。
有一天我正在面新員工,碰巧聽說公司打算從 Flow 遷移到 TypeScript。
其實我是有點懷疑的,畢竟之前不少團隊在小型代碼庫上都身陷泥潭、糾纏不清,這么大規(guī)模的遷移能順利完成嗎?但禮拜一的現(xiàn)實證明我想多了——一切如常。
Eric Clemmons, Stripe 軟件工程師
遷移一結(jié)束,公司內(nèi)可以說是好評如潮。完善順暢的遷移給工程師們留下了深刻印象,甚至有人認為這是 Stripe 多年以來最成功的一次開發(fā)者生產(chǎn)力提升。我們很高興這一年的付出沒有白費,Stripe 的代碼庫終于獲得了顯著、甚至可以說是顛覆性的改進。
5
TypeScript……兩個月之后
轉(zhuǎn)換當然不可能毫無瑕疵。在接下來的幾周內(nèi),我們的 JS 基礎(chǔ)設(shè)施團隊又先后解決了幾個意外問題。但最讓人吃驚的,是有工程師報告 CI 和本地 TypeScript 運行間存在不一致。在 TypeScript 中,我們直接使用由 npm 安裝的各種第三方類型定義,而如果定義被更新,工程師們就得安裝新版本。而這明顯跟我們的 Flow 配置不同,其中的依賴更新很少會改變具體類型,因此我們只能提醒工程師們運行 yarn install 進行調(diào)試。
此外還有其他工作要做:我們知道更細粒度的項目引用可以進一步提高性能,更好的緩存設(shè)計則能加快 CI 運行速度。然而,這么點好處并不值得大費周章。工程師們喜歡使用自動依賴導入和代碼補齊之類的功能,也離不開 TypeScript 社區(qū)中廣泛的第三方類型定義和集成語料庫。這也保證了當有新工程師加入 Stripe 編寫前端代碼時,他們能第一時間使用自己最熟悉的語言、把全部精力都投入到功能設(shè)計上。
隨著 Dashboard 遷移工作的完成,JS 基礎(chǔ)設(shè)施團隊開始進一步提高 TypeScript 在整個公司內(nèi)的采用率。我們使用相同的工具又先后轉(zhuǎn)移了不少其他代碼庫,包括我們的全部支付 UI Stripe Checkout。Stripe 的前端工程師們很快就適應(yīng)了這一切,開始用 TypeScript 編寫所有開發(fā)項目。
而且我們從遷移計劃立項之初就在發(fā)布更新,相當于搞了個全程直播,反響同樣熱烈。來自整個行業(yè)的開發(fā)者紛紛給予關(guān)注,并在自己的代碼庫中嘗試應(yīng)用相同的改進。為了支持大家,我們決定在 GitHub 上分享 Stripe 的 TypeScript 轉(zhuǎn)換代碼(https://github.com/stripe-archive/flow-to-typescript-codemod),希望能起到些許積極作用。
除了關(guān)于 JavaScript、Flow 和 TypeScript 的種種細節(jié)之外,我們還從此次遷移中總結(jié)出另一條重要經(jīng)驗:只要勤奮、專注、樂觀,對大規(guī)模代碼庫做出顯著改進并非不可能。我們將保持住這份熱情,為 Stripe 乃至整個行業(yè)內(nèi)的工程師帶來更高的生產(chǎn)效率與更絲滑的工作體驗。
原文鏈接:https://stripe.com/blog/migrating-to-typescript
作者:Andrew Lunny
來源:InfoQ
作者:Andrew Lunny
歡迎關(guān)注微信公眾號 :前端Q