The Future is Now
2023 年 3 月 17 日

為你的 Remix 應用程式提供未來保障

Matt Brophy
資深開發人員

在 Remix,我們親身經歷過主要版本升級的痛苦。特別是對於應用程式的基礎架構,如其所建構的框架或路由器。我們希望盡最大努力為您提供一流的升級體驗——讓我們來談談「未來標誌」(Future Flags)

現狀

每個1框架(或函式庫)在某個時間點都必須引入重大變更。某些變更會導致您今天撰寫的程式碼在新版本上失效。這可能會導致建置時(甚至更糟,執行時)錯誤。但這些變更都是好的!這是我們框架演進、變得更快、採用新平台功能、實作社群驅動的功能請求等等的方式。

由於這種對重大變更的固有需求,因此產生了 語意化版本 (SemVer) 規範,其中定義了重大變更會指示新的主要版本發佈。這非常好,因為它可以讓應用程式開發人員知道何時應該預期他們的程式碼需要在升級時進行變更,而不是何時應該預期升級「一切順利」。但請記住,您應該始終閱讀發行說明,而不僅僅是盲目升級😉。

很巧的是,就在我開始撰寫這篇文章的同一天,@devagrawal09 發推文了以下內容,這引發了關於框架及其處理「主要重寫」的現況的相關討論串。

Tweet from @devagrawal09 asking 'which javascript framework has lived more than 5 years without causing major rewrites?'
2026 年請再回來關注 Remix!

從討論串中可以清楚看出,人們對「主要重寫」有不同的解釋,而且多年來框架在這方面的成功程度也各不相同。事情並非如此明確的部分原因是,雖然 SemVer 為我們提供了一種溝通何時存在重大變更的方式,但我們沒有類似的共識流程來如何在我們的框架中引入重大變更並將其傳達給應用程式開發人員。

一般來說,主要 SemVer 版本的最低標準是一組發行說明,其中概述了主要版本中的重大變更。理想情況下,這些說明還包括關於如何更改程式碼以採用重大變更的說明。但這真的僅此而已——除此之外,關於如何最好地準備和幫助使用者跨主要版本採用重大變更,幾乎沒有標準化。

因此,我們多年來看到各種不同的方法,包括但不限於

  • 撰寫詳細的遷移指南 1 2 3
  • 在主要版本發佈之前,發佈準備版本,以便更好地準備您的程式碼以採用重大變更 1
  • 發佈相容性套件,讓您可以同時執行兩個版本 1 2 3

我們已經看到一些效果不錯的方法,也看到一些效果不好的方法。然而,在成功案例中似乎有一個一致的概念,那就是為應用程式開發人員提供迭代式升級其應用程式的路徑。大規模而言,無法迭代式升級應用程式的某些部分會變得有問題。您最終會得到一個長期存在的 version-N-upgrade 分支,某位工程師最終會不知感恩地定期將其重定基底到最新的 main 分支上,而且可能在此過程中逐漸抓狂。

這些長期存在的功能分支往往也很慢才能取得進展。我們的利害關係人不停止幾週的功能開發,以便我們可以升級我們的堆疊(這對客戶來說是不可見的)——他們希望繼續並行發佈新功能。因此,團隊不僅只將部分精力投入到升級中,他們還必須處理新舊世界之間固有的上下文切換。這導致升級速度更慢。

功能分支

如果我們看看上述某些方法對應用程式開發人員的影響,我們通常會看到它們都涉及某種形式的長期存在的功能分支,這會導致上述缺點。在所有情況下,功能分支的生命週期都取決於重大變更的數量,但即使只有一些重大變更,也可能需要一些時間才能在大型程式碼庫中解決這些變更。

遷移指南通常在功能分支中遵循和實作。

Diagram of a long lived feature branch for implementing the changes from a migration guide
遷移指南的長期存在功能分支

準備版本傾向於將工作分成 2 個功能分支——一個用於升級到準備版本,另一個用於主要版本。這是一種稍微好一點的方法,但這些個別分支仍然存在相同的缺點。

Diagram of 2 shorter-lived feature branches for implementing the changes from a preparation version
準備版本的 2 個中長期存在功能分支

遷移建置和/或向後相容性標誌在消除長期存在的功能分支方面做得更好,但它們仍然有 2 個不理想的方面。首先,它們存在一些潛在的技術風險,因為並排執行兩個套件(v2 和 v2「向後相容」)與執行 v2 並不完全相同——因此,套件之間的相互溝通中存在非零的錯誤出現可能性。其次,可能更重要的是,這些仍然會一次性地將所有的新功能(和重大變更)傾倒給您。您通常幾乎無法事先做任何事情來準備您的程式碼庫以進行升級並減少影響。一旦 v2 發佈,您就可以可能透過升級到新版本和向後相容性套件來避免長期存在的功能分支。但是,當您迭代式採用重大變更並最終移除相容性建置或向後相容性標誌時,您將在主分支上追趕一段時間。

Diagram of many short-lived branches to implement features via a migration build
遷移建置允許在發佈後迭代式採用功能

我們對這些方法都不太滿意,並希望我們可以提供更順暢的主要升級路徑。

推出未來標誌

當我們第一次開始討論如何處理 Remix 的重大變更時,我不禁回想起我觀看 Yehuda Katz 在 Philly ETE 2016 上發表的 沒有停滯的穩定性 演講。我不是 Ember 開發人員,但那次演講給我留下了深刻的印象2,了解框架如何透過使用功能標誌來減輕使用者採用新功能的痛苦。然而,Ryan Florence 一位活躍的 Ember 開發人員,因此當我提到這次演講時,他立刻知道「沒有停滯的穩定性」這個詞組。

在我的職業生涯後期,在一個 Vue SSR 應用程式上工作時,我們正在準備從 Vue 2 -> 3 升級,我很興奮看到他們在其建置中引入的 功能標誌(儘管我在執行升級之前換了工作,所以我不知道過程是否順利)。

我們在 Remix 知道,如果我們希望能夠為使用者提供順暢的升級體驗,則功能標誌的概念至關重要。但我們希望在我們的 OSS 中走得更遠。即使在上述具有向後相容性標誌的最佳方法中——開發人員仍然需要在主要版本中「一次性接收所有新內容」,導致他們在一段時間內追趕。此外,這也將所有 v2 程式碼變更一個接一個地堆疊起來,讓您有一個壓縮的表面區域,可能會出現細微的錯誤。我們想看看是否可以做得更好。

在 Remix,我們在主要版本中引入重大變更的目標有兩個

  1. 消除對長期存在功能分支的需求
  2. 讓您可以在目前版本中發佈時,個別選擇針對下一個版本的重大變更

換句話說,我們看到的大多數方法都試圖在v2 發佈後為您提供從 v1 到 v2 的轉轍器。相反,Remix 的目標是為您提供許多小型的轉轍器,以在v1 版本中發佈時,最終採用 v2 功能。如果一切按計劃進行,並且您隨著新的「轉轍器」出現而保持最新狀態,那麼您的程式碼如今天所寫,在您升級到新的主要版本時將「一切順利」。這有效地使主要版本升級不再比次要版本升級更痛苦🤯。

Diagram of the lack of a feature branches for adopting v2 features via future flags
未來標誌消除了在發佈後採用的需求

此外,透過在 v1 中隨著時間的推移引入這些功能,我們為應用程式開發人員提供了更大的表面區域來分散其與 v2 相關的程式碼變更。

Diagram of the gradual adoption of v2 feature via future flags through the v1 lifetime
功能可以在整個 v1 生命週期中逐步採用

我們知道這是一個崇高的目標,並且我們知道它可能不會總是完全按照我們的計劃進行,但我們對穩定性是認真的,並且希望確保我們的流程考慮到主要版本升級可能給我們的應用程式開發人員帶來的負擔。

我們計劃透過在 remix.config.js 檔案中稱為未來標誌的方式來實現這一目標。將這些視為未來功能的特色標誌(現在快速說 5 次😉)。當我們實作新功能時,我們總是嘗試以向後相容的方式進行。但是,當我們無法做到並決定有理由進行重大變更時,我們不會將該功能擱置起來以用於最終的 v2 版本。相反,我們會新增一個未來標誌,並在 v1 次要版本中與目前行為一起實作新功能。這讓使用者可以立即開始使用該功能、提供回饋和報告錯誤。

這樣,您不僅可以逐步採用功能(並且無需主要版本升級即可積極採用),我們還可以在發佈 v2 之前逐步解決任何問題。最終,我們也會在 v1 版本中新增棄用警告,以促使使用者採用新行為。然後在 v2 中,我們將移除舊的 v1 方法、移除棄用,並移除標誌——從而使標記的行為成為 v2 中的新預設值。如果在 v2 發佈時,應用程式選擇加入所有未來標誌並更新了其程式碼——那麼他們應該只需將其 Remix 相依性更新到 v2,並從其 remix.config.js 中刪除未來標誌,即可在幾分鐘內在 v2 上執行。

不穩定 vs. V2 Flags

未來的 Flags 可能會有兩種形式:future.unstable_featurefuture.v2_feature,而一個 Flag 的生命週期會取決於變更的本質以及是否為破壞性變更。新功能的決策流程大致如下:

Flowchart of the decision process for how to introduce a new feature

引入新功能的流程圖(點擊在新分頁中開啟)

因此,生命週期為下列其中之一:

  • 非破壞性 + 穩定 API 功能 -> 加入 v1
  • 非破壞性 + 不穩定 API -> future.unstable_ Flag -> 加入 v1
  • 破壞性 + 穩定 API 功能 -> future.v2_ Flag -> 加入 v2
  • 破壞性 + 不穩定 API -> future.unstable_ Flag -> future.v2_ Flag -> 加入 v2

為了澄清,這裡的 unstable_ *並不表示* 我們認為該功能充滿錯誤!它表示我們並非 100% 確定該 API 在穩定之前不會進行一些小幅度的變更。我們 *絕對* 希望早期採用者開始使用這些功能,以便我們可以對 API 進行迭代(或增加信心)。

此外,v2_ Flag 並不表示該功能沒有錯誤 — 沒有軟體是沒有錯誤的!這表示我們對 API 充滿信心,並認為它是 v2 中預設行為的穩定 API。這表示如果您在 v1 中更新程式碼以使用這個新的 API,您可以讓 v2 的升級 *更* 為順暢。

Remix v1 中目前的 Future Flags

以下是目前 Remix v1 中的 Flags 清單:

  • unstable_cssModules - 啟用 CSS Modules 支援
  • unstable_cssSideEffectImports - 啟用 CSS Side Effect 引入
  • unstable_dev - 啟用新的開發伺服器(包括 HMR/HDR 支援)
  • unstable_postcss - 啟用 PostCSS 支援
  • unstable_tailwind - 啟用 TailwindCSS 支援
  • unstable_vanillaExtract - 啟用 Vanilla Extract 支援
  • v2_errorBoundary - 將 ErrorBoundary/CatchBoundary 合併為單一 ErrorBoundary
  • v2_meta - 為您的 meta 函式啟用新的 API
  • v2_routeConvention - 啟用基於檔案路由的扁平式路由風格

我們正在準備 v2 的發布,因此所有的 future.unstable_ Flags 都將穩定為 future.v2_ Flags(除了那些不是破壞性變更的,例如 PostCSS/Tailwind/Vanilla Extract 支援)。這包括為仍然使用舊方法的應用程式新增棄用警告。一旦我們將它們全部穩定下來,我們將發布最終的 Remix 1.15.0 版本,並讓它執行一段時間,讓大家有時間選擇加入他們尚未新增的任何 Flags。然後,我們將計劃發布 Remix 2.0.0 並開始著手發布由 Flag 驅動的 Remix v3 功能。

未來,請查看關於此策略的文件,以取得最新且活躍的 Future Flags 清單。

註腳

  1. 使用「每一個」而不是「所有」,因為我確定有一些像 add 這樣的函式庫在 v1.0.0 版本中運行多年而沒有任何破壞性變更,因為... 嗯,數學的語意不會經常改變。但是您知道我的意思 — 事情會發展並需要破壞性變更,除非您是 DOM,它在回溯相容性方面做得非常出色。

  2. 這對我來說可能如此相關,是因為就在那次演講的 2 個月前,AngularJs 1.5.0 版本發布,試圖為 Angular v2 提供更順暢的途徑。當時,我是一個大型 AngularJs 1.4.0 電子商務結帳應用程式的首席開發人員,而我們正充分意識到 Angular v2 將不是升級,而是一個完整的重寫 😕。


取得最新的 Remix 新聞更新

成為第一個了解新 Remix 功能、社群活動和教學的人。