大規模應用TypeScript「2019 JSConf -Brie Bunge」(下)

特別說明

這是一個由simviso團隊進行的關於Airbnb大規模應用TypeScript分享的翻譯文檔,分享者是Airbnb的高級前端開發Brie Bungejavascript

視頻連接:大規模應用TypeScript「2019 JSConf -Brie Bunge」 或點擊文章底部閱讀原文觀看視頻前端

接上篇文章:大規模應用TypeScript「2019 JSConf -Brie Bunge」(上)java

視頻翻譯文字版權歸 simviso全部,未經受權,請勿轉載!!!

本次參與翻譯人員react

如何解決問題

這聽起來很棒! 但對於咱們的初步提案,還有不少問題和疑慮。讓咱們來對TypeScript進行更深刻的瞭解。

咱們的主倉庫依賴了一些咱們內部的NPM包。爲了得到自動完成和類型檢查的能力,咱們須要先將它們轉換成TypeScript,這樣作是否值得?git

這也是咱們目前面臨的困境,咱們的TypeScript 項目依賴於一個JS NPM包。那咱們該如何獲取這個包的類型?程序員

這看起來像是須要首先將這個包轉換爲TypeScript。但這裏有個問題,由於維護人員不容許咱們對它作TS轉換操做,可能他們也不情願這麼作。由於在咱們提案的早期,咱們並不肯定是否要一直按照這個提案走下去。可是從另外一個層面來說,咱們使用TypeScript是爲了可讓開發人員能夠有更好的體驗。咱們須要TypeScript提供的類型安全性。那麼咱們該如何解決看似雞和蛋的問題呢?github

TypeScript有一個叫作聲明文件的功能。即一個以.d.ts爲擴展名的文件,經過它咱們能夠爲JavaScript文件定義類型。編程

讓咱們來看一個例子。安全

如圖所示,一塊兒來看咱們以前看到的greeter方法。它上面是對應的.d.ts文件,方法裏沒有實現細節,它只描述了類型。TypeScript將它們組合到一塊兒,這樣,在編譯是使用這個聲明文件,在運行時使用這個原生的JS文件。前端工程師

那麼咱們回到咱們剛纔提的問題(要不要一開始就轉換),看看聲明文件是如何提供幫助的。

固然,若是那個項目已轉換爲 TypeScript。咱們就沒有必要再生成一個.d.ts文件來做爲TypeScript構建時的一部分(由於在使用TS編程的時候,要經過它對原生JS進行調用)。但咱們認爲這不止一種選擇。相反,咱們能夠將聲明文件放在咱們的 TypeScript 項目中。

另外一個選擇則是咱們能夠建立一個單獨的NPM包,並將聲明全部聲明文件放入其中。這很棒,由於如今能夠如今跨多個倉庫共享聲明這些文件。經過這些你能夠在使用相似React時,進行相應的類型檢查。在安裝React的同時你能夠安裝@types/react包。

在這個@types/react包中針對React的5000個經常使用包作了類型聲明。@types/react與其餘5000個其餘包都在DefinitelyTyped 這個倉庫中,它由社區在維護。在咱們的主倉庫中,絕大多數的公共依賴都已經由DefinitelyTyped 作到了類型聲明。有活躍的社區氛圍是TypeScript 的一個主要賣點。咱們也回饋了一點力量,相信在這個房間裏也有人作出了貢獻。謝謝。

這些公共的NPM包的類型聲明已經有DefinitelyTyped 在作了,但那些內部的包該怎麼辦?咱們本身安裝了一個DefinitelyTyped鏡像,在它經過建立一個單獨的NPM域(@airbnb-types/*)。這樣,你只須要安裝@airbnb-types便可。這個倉庫的設置與DefinitelyTyped相似,因此咱們能夠在裏面添加併發布這些內部類型。

咱們開源了一個starter 工具包,若是你有興趣的話,能夠來參與下。它裏面沒有類型定義,它只是在教你如何進行一些配置以便於進行測試或者發佈本身的類型定義。

那麼 TypeScript 究竟能幫忙避免多少 bugs 呢?近期,一個叫作「該不應作類型定義」的研究代表,在選擇了TypeScript 的 github 倉庫中,有 15% 的 bugs 獲得了避免。

在咱們內部,有一個記錄生產環境事故的流程。這個流程的本意並非爲了責怪誰,而是要從錯誤中進行學習,這樣咱們以後就不會再犯相似的錯誤。因此我坐下來讀了六個月的總結報告,閱讀這些總結報告頗有意思。我最喜歡的就是未捕獲的異常以及危險的參數計算。

好吧,也許這些錯誤的名字並無那麼使人激動。不管如何,我將這些錯誤歸類爲與 JavaScript 相關或無關,以此肯定哪些錯誤能夠經過使用TypeScript 來避免。

讓咱們一塊兒看個例子,使用TypeScript會帶來哪些幫助。咱們對所分享的這個 Input 組件進行修改,經過一些設置來還原bug。用戶沒法提交表單是由於它再也不能經過驗證,這是所分享的 Input 組件更改前的簡化版本。

它接收一個叫onBlur的變量,並將其直接傳遞給input元素。所作的改變就是添加一個新的onBlur 事件處理。但這裏有一個不明顯的bug,你能發現它嗎?

就是事件參數再也不傳遞給onBlur prop。這就致使在好幾個不一樣倉庫中都出現了這同一個問題。

這裏 Input 組件做爲Redux Form的一部分進行使用,指望獲得一個事件或值,以便驗證正常工做。若是沒有該事件,表單將再也不經過驗證,這就意味着提交按鈕始終處於禁用狀態。

TypeScript在這裏是如何幫到咱們的?從文檔中咱們能夠看到Redux Form有類型捕獲約束。

onBlur 事件屬性必須傳遞一個事件或值。所以,若是咱們使用了TypeScript下的Redux Form,那麼在函數調用那裏就能夠看到一個當沒有傳遞事件參數時所產生的錯誤。

另外一類常見的問題就是涉及嚴格的空值檢查。即對使用屬性來構造或嘗試調用可能爲null或undefined的內容進行檢查。你可能之前有見過這個錯誤。

另外一種是類型不匹配。當咱們嘗試使用彼此不匹配的類型時,TypeScript就會提示咱們。

因此如今咱們對常見的檢查出來的問題有了更好的理解,TypeScript能夠幫助預防這種bug。

那整體百分率是多少?(那個事故日誌所表現的)38%!

咱們發現有38%的事故致使了生產階段的bug。這些對咱們用戶產生實際影響的bug,可使用TypeScript來阻止。這對咱們來講是個巨大的發現。它有助於將這種(積極)效果轉換到現實中。咱們複製了一些BUG事件,並向你們展現了TypeScript所給出的Error提示,而後對bug進行修復(也就是咱們看到的bug提示燈泡滅掉了)。的確,咱們也能夠經過寫測試代碼來捕捉這些,可是經過靜態類型檢查能夠額外增長一層保護層。所以,若是你所在公司有相似的歷史,那麼你可能就有必要和懂TypeScript小夥伴一塊兒來看一下這些問題在大家的代碼裏所佔的比例。

那麼團隊是否但願切換到TypeScript呢?咱們在幾個團隊試用了TypeScript,專門針對以前沒有使用過TypeScript的團隊來獲取更多的使用反饋經驗。咱們幫他們設置好 TypeScript 環境,而後收集他們的反饋。在用了一段時間後,咱們向他們發送了一份調查問卷,詢問他們是否應繼續使用TypeScript。反饋結果是很是確定的。

咱們建議使用這種試用期(的形式,其實就是金絲雀模式)來測試新技術或模式。前端工做組的開發也是基於這個形式來進行的,由於它是獨立的,它能夠很容易回滾到以前的狀態。這也對提案頗有幫助,由於咱們能夠判斷團隊是否真的喜歡使用TypeScript。

這裏可能會有一些關於構建時間上的擔心。咱們測量了,發現並無明顯的影響。咱們在主倉庫啓用超過了500條eslint規則,也使用TypeScript eslint解析器。咱們很高興地發現它們中的大多數均可以工做。若是咱們在未來要棄用TypeScript的話,咱們能夠剝離類型,並最終獲得大體相同的JavaScript。因此咱們逐一記錄、思考、跟進,而且針對提出的問題和擔心找到解決辦法。

與批評者合做並聽取他們的擔心對咱們來講很是重要,最後這些批評者中的大部分轉而會支持咱們,咱們的提案也從他們的反饋中變得更爲健壯。

在充分解決了這些問題以後,針對全部前端工程師進行了咱們是否應該採用TypeScript的調查。咱們收到了確定的回答以後,咱們有足夠的證據向前推動,並經過這項提案。

逐步採用

在此基礎上,咱們逐步擴大了採用範圍。

此時,咱們已經度過了試驗階段,這對於驗證 TypeScript 和打好基礎是頗有用的。咱們已經解決了早期的矛盾,並改進了工具和文檔,因此以後團隊成員會更容易入門。

咱們一直有與 TypeScript 團隊進行着聯繫。並幫忙解決一些問題,好比,更好的默認屬性優先級處理。在這個階段,咱們本身內部的 TypeScript 社區也獲得了發展。可是大部分 Airbnb 的員工還不知道 TypeScript。這也意味着更多的人能夠去幫助和回答他們的問題。接下來咱們將會進入測試狀態,團隊能夠選擇使用它。爲了幫助團隊,咱們建立了內部文檔和風格指南,並舉辦了一些學習課程。咱們創建了一個聊天組,一個內部的類Stack Overflow,谷歌Email主題,github組,來供組內成員交流。咱們想確保人們能獲得他們須要的幫助。最後一步是將 TypeScript 徹底普及化。此時就意味着它是穩定狀態,每一個人都應該開始使用它。

咱們目前正在努力地去接近這個目標。剩下的步驟就是鞏固風格指南、文檔、增強內部培訓和遷移更多代碼。到目前爲止,咱們大約有50%的團隊使用 TypeScript,在主倉庫中,有10%的文件已經被轉換成 TypeScript。經過這種漸進的方法,使團隊遷移至 TypeScript 的過程更加順暢。

若是從第一天開始就要求每一個人應該使用TypeScript,那麼一個接一個的人就會遇到一樣的問題。相反,咱們先在小範圍內使用 TypeScript ,而後總結一些經驗技巧。當咱們準備把它大規模推廣時,這些經驗技巧也會用得上。

遷移策略

咱們已經探索出了幾種將代碼遷移至 TypeScript 的方式。咱們最初的遷移策略的是混合使用 JavaScript 、TypeScript。

讓咱們看看,在主倉庫中這個策略是如何進行的。這是我在 airbnb.com 上找到的一個簡化版本,而且給它們起了一個比較合理的名字。因此這裏不存在公司的隱私信息。

讓咱們放大homes 項目,看看使用混合策略轉換它會是什麼樣子。

咱們添加了一個TypeScript配置文件,並將各個文件從js重命名爲ts或jsx重命名爲tsx。若是TypeScript 報錯了,那咱們動手去修復他們吧。TypeScript 的一個很棒的特性是,在編譯和運行以前,並不須要轉換全部代碼。這個配置選項(allowJS)容許 javascript 和 TypeScript 文件共存。在這一點上,咱們能夠看到網站仍能繼續運行。咱們不須要暫停開發而去遷移整個項目,咱們能夠挨個遷移文件。咱們會重複這個過程,直到整個項目被遷移。

在關於遷移的話題上,我想花些時間和你們分享一些咱們認爲有用的技巧。第一個是$TSFixMe。

咱們經過TypeScript的any類型添加了一個全局類型別名,這意味着它能夠爲任何類型。咱們將它稱之爲$TSFixedMe,表名咱們在代碼向TypeScript遷移完成後,再來將類型修正。平時最佳實踐是避免使用any,由於它會形成類型安全丟失,但它在遷移過程當中會頗有幫助。

使用@ts-ignore註解能夠作到忽略下一行錯誤。正確地輸入一個文件可能涉及一些深層依賴鏈解析(相似於複雜對象)。咱們能夠嘗試經過首先轉換子文件來避免這種狀況,但有時這是不可避免的。所以,$TSFixedMe和@ts-ignore註解可以幫助拆分這些內容,同時則會增長這些檢查工做。這些都是暫時的,咱們計劃添加類型覆蓋工具,並在後面咱們改進類型時提供幫助。

在JSX中,咱們在React組件上使用propTypes 進行運行時類型檢查。在將jsx轉換爲tsx的時候,咱們能夠刪除proptypes直接用TypeScript,也能夠在proptypes基礎上添加TypeScript。在咱們所分享的react項目中,咱們想保留傳參類型,以便別人使用的時候仍然能夠得到運行時檢查。爲了不重複兩次類型聲明,那就須要與這些類型保持同步。咱們建立了一個Props類型,經過它將給定的propTypes和defaultProps來派生出一個TypeScript類型。這樣,propTypes和defaultProps組合並獲得這個最終類型。若是你好奇它是如何工做的,你能夠查看我在gist上分享的代碼片斷。

最近咱們已經在使用修訂遷移策略All-in TS進行實驗。讓咱們回過來在看看這個Homes項目,而後對它們進行使用all in策略,而後在看它工做怎麼樣。

咱們從js形式的文件開始,咱們把全部的文件都改爲TS形式的,而後讓項目編譯。可能咱們使用一些比咱們想要的更寬鬆的類型,但其實咱們已經開啓了TS最嚴格的檢查配置。

而後咱們接下來再繼續改進類型,移除ts fix語句,好比@ts-ignore(@ts-ignore 註釋隱藏 .ts 文件中的錯誤)。這與js和ts混合策略相比起來有一些優點。經過類型逐步改進比經過文件逐步改進更爲簡單(兩種策略的對比)。若是你正在開發一個新功能,你只須要關注新添加的類型,而後簡單的修復這個它便可,而不是先轉換整個文件來修復全部錯誤,而後再添加你所須要類型。

不用重命名文件也意味着更方便查看。有時候,若是一個文件在一次提交被重命名,而後在別的提交中修改。他們會在code review中單獨出現,程序員必需要合在一塊兒看才能知道變化了什麼。後一種策略還能清楚地知道缺乏哪些類型。

TypeScript類型推導能力十分強大,咱們能夠在編寫代碼的時候大量使用它。爲了經過編譯,有些文件須要進行一些TS Fixed。TS就能夠推斷出剩餘部分。

還有就是開發者們可能有一個固定的思惟模式,他們並不會根據文件擴展名來切換思惟,因而就出現了好比爲何我不能在這裏添加類型?爲何我不能在那裏獲得編譯錯誤的疑問(.js和.ts混用)?那些類型在全部文件中均可以添加、使用、檢查。

這聽起來很不錯,可是咱們該如何遷移咱們整個代碼呢?對於大規模代碼修改而言,Codemod是一種十分強大的工具。拿最簡單的形式來講,就比如是咱們在咱們的項目中所使用的全局搜索和替換。你也許在你以前的IDE裏面幹過這件事(全局查找和替換),這些Codemod庫能夠經過正則來替換,但它們很不穩定,可能會因細微的代碼風格變化而終止。

或者咱們可使用以前某人已經講過的抽象語法樹。so,這就是這段代碼用AST(抽象語法樹)來表達的形式。如圖所示,左側的代碼都一 一對應着右側抽象語法樹上的節點。因此爲了好玩,咱們想寫一個Codemod來反轉代碼中的全部標識符。咱們將咱們的代碼做爲輸入參數,根據這個建立出AST(抽象語法樹),修改AST樹而後產生新的代碼。這裏的關鍵是咱們以編程方式進行此更改。若是你手上須要修改的文件數並很少的話,咱們能夠一個個的去修改。但若是一旦文件數量達到數千個以上,這種手動去修改的想法可能會使人感到十分心累。

所以咱們Airbnb採用了Facebook的Jscodeshift來進行這種大量的代碼重構。這個轉換庫能夠捕獲咱們剛剛對該ast進行的修改而且反轉標識符。咱們找到與標識符對應的全部節點,用名字反轉,用新節點去替換這些節點,而後獲得新的代碼。Missy Elliott(歌手)也將會咱們感到自豪,因此咱們反轉了它。

咱們拿到了代碼而且從新改裝,找到了成員的標識符而後翻轉它。yeah!

astexplorer.net這個網站沒法幫你掌握好說唱技巧,但能夠幫助你查看你的Codemods。在這個網頁下,它有一個能夠經過源代碼輸出對應的AST樹的功能,以及在你對代碼的改變同時反映到AST樹上。

我也在DefinitelyTyped 這個庫提交了關於Jscodeshift的PR,這樣的話能夠來下降你們在使用TypeScript與Codemod的交互門檻。

在將JavaScript代碼遷移到TypeScript時,有這幾種模式。對於react組件,咱們一次次的將靜態類屬性移動到class body裏面。建立一個PropsType表示react生命週期方法。咱們將它們編碼爲Codemod,以便咱們能夠在更多代碼上重複運行它們。咱們經過使用一個叫做TS Migrate的工具來將它們進行打包。這個工具的功能是當如一個JS項目,而後獲得一個編譯好的TS項目。隨着時間的推移,你仍然須要慢慢找到類型,但它爲你提供了一個工做前提。咱們將此工具應用於咱們的內部分享的React組件庫,如今在咱們的網站上已經頻繁地使用。咱們有內部的類型定義庫(DefinitelyTyped),可是由於react分享組件庫的快速發展,因此作到與時俱進地更新太難了。因此,咱們想直接從源碼類型出發,這也是咱們遷移TS的第一個目標。咱們已經將超過3萬行以上的代碼都進行了TypeScript化,大家可能認爲咱們整個團隊花了四周的時間才能完成這個。事實上,咱們用了一套咱們本身的Codemod工具,僅需數分鐘就完成了。

咱們使用來自proptypes的類型信息,同時使用$TSFixMe,並基於此來繼續進行優化。但即使如此,咱們也生成了有意義的TypeScript聲明文件,這樣咱們能夠在其餘倉庫中進行使用。在這個例子中,咱們能夠看到須要合併的代碼行數多的有點可怕。經過使用TypeScript編譯器以及在可視化迴歸測試的幫助下,咱們將在CI上運行測試。經過這些測試我能夠很自信的說,個人這些改變不會對原來的系統產生任何不利的影響。固然咱們還能確保咱們的站點仍舊在正常工做,並不須要回滾代碼。難以想象!

咱們如今已經將TS Migrate運用在其它的一些地方,同時也在不斷優化和迭代它。咱們計劃在之後會將它運用於更多的代碼上(JS代碼)。咱們打算以後將它開源,這樣大家也能將它運用在大家的本身的代碼遷移上。

我想給你一些咱們能夠從TypeScript遷移中得出關鍵點,而且是能夠普遍應用的。在大型組織中實施變革多是一項挑戰,但強有力的事實依據和相關問題和擔心的解決,可使咱們信服。採用逐步變化的方式有助於減小摩擦並證實其價值。一條明確的遷移路線能幫助團隊更好的轉向新的模式,同時好的工具也能促進這個過渡的過程。

我之因此開始這個工做,是由於以前有個產品組對個人工具感到失望。當我得知公司內部其餘人也有這種改變的想法的時候,我便與他們合做並將之進行下去。與其怨天尤人接受現狀,只有經過行動才能發生積極的改變。因此我鼓勵你去追求那些可讓你對組織充滿激情的事情,讓你和你周圍的人的生活變得更好。

感謝你們的傾聽,同時感謝AirBnb爲這個項目做出貢獻的每個人,尤爲是臺下的Joe和Mohsen。還有對其餘一些優秀的Airbnb工程師表示感謝。我手上也有些TypeScript主題的小便籤和一些鑰匙鏈,先到先得,只限前30人。

感謝你們的傾聽

相關文章
相關標籤/搜索