The Future is Now
2023 年 3 月 17 日

让你的 Remix 应用未来可期

Matt Brophy
开发人员

在 Remix,我们亲身经历过进行重大版本升级是多么痛苦,尤其对于像框架或路由器这样的应用程序基础来说。我们希望尽我们所能为您提供一流的升级体验,让我们谈谈 **“未来标志”**。

现状

每一个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
为迁移指南创建长期存在的特性分支

**准备版本** 往往将工作分成两个特性分支——一个用于升级到准备版本,另一个用于升级到主版本。这是一种略好一点的方法,但这些单独的分支仍然会带来同样的缺点。

Diagram of 2 shorter-lived feature branches for implementing the changes from a preparation version
为准备版本创建两个中等寿命的特性分支

**迁移构建** 和/或向后兼容性标志在消除长期存在的特性分支方面做得更好,但它们仍然存在两个不太理想的方面。首先,它们带来了一定的底层技术风险,因为同时运行两个包(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 升级到 Vue 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 文件中称为 **未来标志** 的方式来实现这一点。将其视为 **未来特性的特性标志**(现在快速说五遍 😉)。当我们实现新特性时,我们总是尝试以向后兼容的方式进行。但当我们无法做到这一点,并决定进行破坏性变更时,我们不会将该特性推迟到最终的 v2 版本发布。相反,我们添加一个 **未来标志**,并在 v1 的次版本中与当前行为一起实现新特性。这使得用户可以立即开始使用该特性,并提供反馈和报告错误。

这样,您不仅可以逐步采用功能(并在没有重大版本升级的情况下急切地采用),我们还可以逐步解决任何问题,发布 v2 之前。最终,我们也会将弃用警告添加到 v1 版本中,以提示用户使用新行为。然后在 v2 中,我们将删除旧的 v1 方法,删除弃用,并删除标志——从而使标志行为成为 v2 中的新默认行为。如果在发布 v2 时,应用程序已选择加入所有未来的标志并更新了其代码——那么他们应该只需将其 Remix 依赖项更新到 v2,然后从其remix.config.js中删除未来的标志,并在几分钟内运行 v2。

不稳定标志与 V2 标志

未来标志可以采用两种形式之一:future.unstable_featurefuture.v2_feature,标志的生命周期将取决于更改的性质以及它是否为破坏性更改。新功能的决策流程如下所示

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

引入新功能的流程图(点击在新标签页中打开)

因此生命周期为以下两种之一

  • 非破坏性 + 稳定 API 功能 -> 落入 v1
  • 非破坏性 + 不稳定 API -> future.unstable_标志 -> 落入 v1
  • 破坏性 + 稳定 API 功能 -> future.v2_标志 -> 落入 v2
  • 破坏性 + 不稳定 API -> future.unstable_标志 -> future.v2_标志 -> 落入 v2

为了澄清——这里的unstable_并不意味着我们认为该功能存在错误!这意味着我们不能 100% 确定 API 在稳定之前是否会发生一些小的更改。我们绝对希望早期采用者开始使用这些功能,以便我们可以迭代(或对 API 充满信心)。

此外,v2_标志并不意味着该功能没有错误——没有软件是完美的!这意味着我们对 API 有信心,并将其视为 v2 中默认行为的稳定 API。这意味着,如果您在 v1 中更新代码以使用此新 API,则可以使您的 v2 升级更加顺利。

Remix v1 中的当前未来标志

以下是当今 Remix v1 中的当前标志列表

  • unstable_cssModules - 启用 CSS 模块支持
  • unstable_cssSideEffectImports - 启用 CSS 副作用导入
  • 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_标志都将稳定为future.v2_标志(除了那些不是破坏性更改的标志,例如 PostCSS/Tailwind/Vanilla Extract 支持)。这包括为仍使用旧方法的应用程序添加弃用警告。一旦我们全部稳定它们,我们将进行最终的 Remix 1.15.0 版本发布,并让它运行一段时间,以便用户有时间选择加入他们尚未添加的任何标志。然后我们将计划发布 Remix 2.0.0 并开始着手发布基于标志的 Remix v3 功能。

将来,请查看文档以了解有关此策略的最新信息,其中包含最新活跃的未来标志列表。

脚注

  1. 是“每个”,而不是“所有”,因为我相信有些add这样的库已经运行多年,并且一直保持在 v1.0.0 版本,没有发生破坏性更改,因为......好吧,数学的语义并不经常发生变化。但是您明白了,事物会不断发展,需要进行破坏性更改,除非您是DOM,它在向后兼容性方面做得很好。

  2. 这可能与我特别相关,因为在那个演讲之前仅仅两个月,AngularJs 1.5.0 刚刚发布,旨在为 Angular v2 提供更平滑的升级路径。当时,我是大型 AngularJs 1.4.0 电子商务结账应用程序的首席开发人员,我们正在意识到 Angular v2 不会是一个升级,而是一个完整的重写😕。


获取有关最新 Remix 新闻的更新

成为第一个了解新 Remix 功能、社区活动和教程的人。