Airbnb 如何順利升級(jí) React

介紹

Airbnb 的前端最近達(dá)到了一個(gè)重要的里程碑:我們所有的網(wǎng)頁(yè)面都已經(jīng)從 React 16升級(jí)到了當(dāng)前的主要版本React 18。這是一個(gè)涉及到多個(gè)頁(yè)面的大項(xiàng)目,包括客人和房東頁(yè)面以及許多內(nèi)部工具。為了安全地進(jìn)行這次升級(jí),我們創(chuàng)建了React升級(jí)系統(tǒng):可重用的基礎(chǔ)設(shè)施,允許我們?cè)趩我淮a倉(cāng)庫(kù)中逐步推出React的新版本并測(cè)量升級(jí)的結(jié)果。在這篇博客文章中,我們將討論我們的升級(jí)理念、我們創(chuàng)建的系統(tǒng)以及從這次升級(jí)中學(xué)到的經(jīng)驗(yàn)。

雖然這篇文章主要集中在 React 上,但系統(tǒng)和經(jīng)驗(yàn)同樣適用于許多需要定期升級(jí)的網(wǎng)頁(yè)框架和庫(kù)。

升級(jí)的挑戰(zhàn)

在任何長(zhǎng)期項(xiàng)目中,升級(jí)依賴項(xiàng)是一個(gè)常見(jiàn)任務(wù)。升級(jí)可以修復(fù)錯(cuò)誤、提高性能并解鎖新的API。有些升級(jí)很簡(jiǎn)單,但當(dāng)大量產(chǎn)品代碼依賴于更改的API或想當(dāng)然的行為,升級(jí)變得更加困難。在 Airbnb 的網(wǎng)頁(yè)單一代碼倉(cāng)庫(kù)中,我們只允許每個(gè)頂級(jí)依賴項(xiàng)的一個(gè)版本(少數(shù)例外情況),在倉(cāng)庫(kù)根目錄中有一個(gè) package.json。這確保了代碼庫(kù)內(nèi)部的兼容性和一致性,并避免了向用戶發(fā)布重復(fù)的包。在升級(jí)系統(tǒng)之前,只有一個(gè)依賴項(xiàng)版本意味著要進(jìn)行原子更新,這需要大量的前期遷移工作、一個(gè)長(zhǎng)期運(yùn)行的升級(jí)分支以及最終部署給用戶時(shí)的一個(gè)單一里程碑。這樣的做法容易出錯(cuò)且風(fēng)險(xiǎn)很大,因此需要“英雄式”的工程努力來(lái)完成干凈的升級(jí)。

理想情況下,我們應(yīng)該發(fā)布沒(méi)有問(wèn)題的小型增量升級(jí)。沒(méi)有某種方式測(cè)試和逐步推出這個(gè)系統(tǒng)到大型單一代碼倉(cāng)庫(kù)中,我們常常需要多次嘗試升級(jí),每次發(fā)現(xiàn)問(wèn)題時(shí)進(jìn)行降級(jí)。使用這種升級(jí)策略特別難以捕捉性能回歸問(wèn)題。因?yàn)樵诎l(fā)布之前沒(méi)有辦法收集性能數(shù)據(jù),我們?cè)诓渴饡r(shí)直接從 0% 推出到 100%。


我們的目標(biāo)是通過(guò) React升級(jí)系統(tǒng)使升級(jí)過(guò)程更加順暢,從而使之不再需要“英雄式”的努力,而是變得更加常規(guī)。具體而言,我們的目標(biāo)是能夠:

    1. 增量升級(jí),以便盡快獲得反饋和經(jīng)驗(yàn)教訓(xùn)。
    1. 經(jīng)常升級(jí),以便我們版本與升級(jí)版本之間的差距盡可能小。
    1. 測(cè)試升級(jí),以便我們可以精確衡量升級(jí)的性能影響,并使用這些數(shù)據(jù)做出有關(guān)升級(jí)路徑的明智決策。

設(shè)計(jì) React 升級(jí)系統(tǒng)

從這些目標(biāo)出發(fā),我們開(kāi)始構(gòu)思理想的架構(gòu)。我們希望避免長(zhǎng)時(shí)間運(yùn)行的升級(jí)分支,以便能夠逐步升級(jí);我們還希望能夠?qū)ι?jí)進(jìn)行 A/B 測(cè)試,從生產(chǎn)環(huán)境中獲得反饋來(lái)指導(dǎo)發(fā)布決策。


在該系統(tǒng)的最簡(jiǎn)單實(shí)現(xiàn)中,我們需要解決幾個(gè)問(wèn)題:需要選擇一個(gè)React版本進(jìn)行渲染,并且在運(yùn)行時(shí)動(dòng)態(tài)切換兩個(gè)版本具有挑戰(zhàn)性。以下是使用這種簡(jiǎn)單方法渲染一個(gè)基本應(yīng)用的代碼示例:

import React18 from 'react'; 
import React16 from 'react'; // // duplicated import?

if (shouldEnableReact18()) {
  const root = React18.createRoot(container);
  root.render(<App />);
} else {
  React16.render(<App />, container);
}

這里有兩個(gè)問(wèn)題:

    1. 我們不希望在應(yīng)用中捆綁兩個(gè)版本的React,否則會(huì)使框架包的大小翻倍。此外,我們可能需要更改構(gòu)建時(shí)使用的 JSX 轉(zhuǎn)換,使得 <App /> 與一個(gè)版本不兼容。
    1. 不清楚這些導(dǎo)入應(yīng)該來(lái)自哪里。react 依賴項(xiàng)將指向 React 16React 18,但不會(huì)同時(shí)指向兩個(gè)版本。

為了解決這些問(wèn)題,我們使用模塊別名來(lái)分割版本,并使用環(huán)境定位來(lái)構(gòu)建和運(yùn)行這兩個(gè)分割版本的 React。

模塊別名

我們使用模塊別名解決了這些導(dǎo)入來(lái)自哪里的難題。使用 yarn,我們?cè)?package.json 中添加了另一個(gè) react 依賴項(xiàng),例如:

"react-18": "npm:react@18"

這使我們能夠從 react-18 包中導(dǎo)入 React。這樣做解決了部分問(wèn)題。許多工具(如自定義解析器和構(gòu)建系統(tǒng))需要知道使用哪個(gè)版本。為了集中邏輯,我們將所有自定義工具連接到一個(gè)中心的“全局別名”配置中。這個(gè)全局別名配置允許我們?cè)谝粋€(gè)地方為所有不同的工具設(shè)置別名。Babel、Jest?、Webpack? 和其他自定義解析邏輯都需要知道在什么條件下我們希望將導(dǎo)入從 react 重定向到 react-18。通過(guò)我們的“全局別名”配置對(duì)模塊進(jìn)行別名意味著用戶代碼無(wú)需更改,我們可以在幕后處理這些重定向。

TypeScript 差異

鑒于任何組件都可能在 React 16 或 18 中運(yùn)行,我們希望在升級(jí)期間使用適用于兩者的組件類(lèi)型。幸運(yùn)的是,React團(tuán)隊(duì)保持了向后兼容性,即使是在主要版本之間。

我們安裝了 React 18的類(lèi)型,并為 React 18中新增的 API創(chuàng)建了在 React 16和 18 中都能工作的模擬層(例如,useTransition在 16 中不執(zhí)行任何操作)。對(duì)于無(wú)法模擬的API(例如 useId),我們通過(guò)類(lèi)型擴(kuò)展表明該鉤子在運(yùn)行時(shí)可能未定義。

對(duì)于 React 18TypeScript僅有的破壞性更改,我們等到 React 18 升級(jí)完成后再逐步修復(fù)這些問(wèn)題。我們通過(guò)類(lèi)型擴(kuò)展來(lái)修補(bǔ)差異,以便在單一代碼倉(cāng)庫(kù)中逐步修復(fù)這些新的 TypeScript 錯(cuò)誤。

環(huán)境定位

為了解決重復(fù)導(dǎo)入的問(wèn)題,我們需要生成兩個(gè)不同的構(gòu)建產(chǎn)物:一個(gè)包含React 16,另一個(gè)包含 React 18。我們分別稱(chēng)這些為“控制”和“處理”產(chǎn)物。由于 Airbnb使用服務(wù)器端渲染(SSR),我們還需要在服務(wù)器上以不同的節(jié)點(diǎn)進(jìn)程運(yùn)行這兩個(gè)不同的產(chǎn)物。通過(guò)Kubernetes?,我們?cè)O(shè)置了兩個(gè)不同的 Kubernetes 環(huán)境來(lái)運(yùn)行這些控制和處理產(chǎn)物。我們稱(chēng)這種設(shè)置為環(huán)境定位。


模塊別名和環(huán)境定位結(jié)合使用,以在生產(chǎn)環(huán)境中部署框架的不同版本
我們還在構(gòu)建時(shí)將環(huán)境變量(REACT_UPGRADE)寫(xiě)入資產(chǎn),并在運(yùn)行時(shí)在我們的 Node SSR服務(wù)中設(shè)置該變量。這使我們能夠執(zhí)行可能僅在升級(jí)系統(tǒng)的一側(cè)或另一側(cè)需要的條件邏輯。

這種設(shè)置也適用于本地開(kāi)發(fā)。我們的“本地”開(kāi)發(fā)環(huán)境也被部署,因此我們能夠像生產(chǎn)環(huán)境一樣配置本地開(kāi)發(fā)的 React版本。隨著每個(gè)SSR服務(wù)升級(jí)到 React 18,我們還將該服務(wù)的開(kāi)發(fā)環(huán)境切換到 React 18,以保持生產(chǎn)和本地開(kāi)發(fā)版本的同步。

測(cè)試升級(jí)

Airbnb 擁有全面的測(cè)試流程,這有助于在將升級(jí)暴露給用戶之前建立對(duì)升級(jí)安全性的信心。我們的測(cè)試流程包括視覺(jué)回歸測(cè)試、集成測(cè)試和單元測(cè)試。在向用戶推出之前,我們修復(fù)了這些套件中的所有新故障。

單元測(cè)試是最難從框架內(nèi)部抽象出來(lái)的。因?yàn)槲覀兪褂?EnzymeReact Testing Library的組合,所以我們需要修復(fù)單元測(cè)試、模擬和適配器中關(guān)于 API和框架內(nèi)部的假設(shè)。為此,我們?cè)?React 1618下運(yùn)行所有單元測(cè)試,在逐步修復(fù)現(xiàn)有故障的同時(shí)允許 React 18測(cè)試套件中的現(xiàn)有故障。我們使用這個(gè)“允許的故障”列表來(lái)逐步減少測(cè)試失敗的數(shù)量,從而防止倒退,因?yàn)椴辉试S在列表中出現(xiàn)新的故障。這種方法使我們能夠逐步修復(fù)組件和測(cè)試環(huán)境中的問(wèn)題。

我們通過(guò)儀表板跟蹤解決數(shù)百個(gè)測(cè)試失敗的問(wèn)題,使用升級(jí)系統(tǒng)逐步合并修復(fù),并將工作分配給少數(shù)開(kāi)發(fā)人員。這使得遷移工作對(duì)更廣泛的前端團(tuán)隊(duì)來(lái)說(shuō)基本透明,并幫助我們?cè)谕瞥銮敖⒘藢?duì)升級(jí)的信心。

逐步上線

一旦我們有了模塊別名和環(huán)境定位,我們就有能力從同一個(gè)代碼庫(kù)編寫(xiě)和交付兩個(gè)不同版本的 React代碼。為了確保安全性和可測(cè)試性,我們還需要一種逐步推出這種新環(huán)境的方法。為了減少一次性發(fā)生的變更量,我們希望控制跨流量和產(chǎn)品界面的推出。我們的實(shí)驗(yàn)基礎(chǔ)設(shè)施允許我們隨意將流量引導(dǎo)到我們的兩個(gè)生產(chǎn)環(huán)境(控制和處理)。這種設(shè)置還允許我們首先在內(nèi)部測(cè)試升級(jí),如果發(fā)現(xiàn)問(wèn)題可以完全關(guān)閉升級(jí)。

控制不同界面的推出更加困難。在單頁(yè)應(yīng)用中,管理多個(gè)React版本意味著卸載和加載React根。這會(huì)導(dǎo)致性能下降并降低用戶體驗(yàn)。

因此,我們?cè)趹?yīng)用級(jí)別管理界面推出升級(jí)。Airbnb的單一代碼庫(kù)包含許多單頁(yè)應(yīng)用,因此擁有React升級(jí)系統(tǒng)來(lái)為這些應(yīng)用中的每一個(gè)打開(kāi)和關(guān)閉升級(jí)是很有用的。使用我們的React升級(jí)系統(tǒng),我們能夠首先在內(nèi)部向單個(gè)應(yīng)用推出,允許開(kāi)發(fā)人員在開(kāi)發(fā)和我們的預(yù)發(fā)布站點(diǎn)中選擇加入和退出升級(jí)。這種方法使我們避免了長(zhǎng)時(shí)間運(yùn)行的功能分支,幫助我們實(shí)現(xiàn)了增量升級(jí)的目標(biāo)。

功能正式通過(guò)和未來(lái)工作

使用該系統(tǒng),我們將 React 18完全推出到Airbnb的所有網(wǎng)頁(yè)面,且無(wú)需回滾。升級(jí)后,我們開(kāi)始測(cè)試新的API,如新的根API和并發(fā)渲染功能。在采納這些功能之前,我們故意等待了幾周,以確保升級(jí)已經(jīng)穩(wěn)定。這樣我們可以確信不需要降級(jí)和撤銷(xiāo)代碼更改。

看到采用這些新功能后性能的提升令人興奮,我們正在繼續(xù)試驗(yàn)將它們擴(kuò)展到可能受益的關(guān)鍵UI界面。

為了確保我們的頻繁升級(jí)目標(biāo)得以實(shí)現(xiàn),我們將使用React升級(jí)系統(tǒng)測(cè)試 React的金絲雀頻道。我們可以指向金絲雀標(biāo)簽,而不是 React 18,以預(yù)覽現(xiàn)在需要進(jìn)行的遷移工作,以便為 React 19做準(zhǔn)備。為了使升級(jí)不需要“英雄式”的努力,保持最新應(yīng)該是一項(xiàng)持續(xù)的工作,而不是一次性的大變更。

結(jié)論

我們的 React升級(jí)系統(tǒng)目標(biāo)是使我們能夠逐步升級(jí)、測(cè)試升級(jí)和頻繁升級(jí)。結(jié)合環(huán)境定位和我們的別名系統(tǒng),使我們能夠逐步升級(jí)和測(cè)試升級(jí)。我們已經(jīng)開(kāi)始針對(duì) React 19 beta 運(yùn)行我們的前端,為 React 19提前做好準(zhǔn)備。

我們要感謝 React團(tuán)隊(duì)為保持React版本之間,甚至是主要版本之間的向后兼容性所付出的努力。沒(méi)有這種努力,這種升級(jí)方法將不可能實(shí)現(xiàn)。

使用React升級(jí)系統(tǒng),我們對(duì) React 18 的推出充滿信心,并將使用這種方法進(jìn)行未來(lái)的升級(jí)。我們相信投資于升級(jí)系統(tǒng)是值得的,因?yàn)殡S著時(shí)間的推移,升級(jí)將繼續(xù)需要。React升級(jí)系統(tǒng)使我們能夠逐步測(cè)試和推出升級(jí),確保我們?yōu)橛脩籼峁┳罴训挠脩趔w驗(yàn)和性能。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容