将你的 React Router 应用迁移到 Remix
全球部署的数百万个 React 应用程序由 React Router 提供支持。您很可能已经发布了其中的一些应用程序!由于 Remix 是基于 React Router 构建的,因此我们努力使迁移成为一个简单的过程,您可以反复进行,以避免进行大规模重构。
如果您尚未使用 React Router,我们认为有几个令人信服的理由值得您重新考虑!历史管理、动态路径匹配、嵌套路由等等。查看 React Router 文档 并了解我们提供的所有内容。
确保你的应用使用 React Router v6
如果您使用的是旧版本的 React Router,第一步是升级到 v6。查看 从 v5 到 v6 的迁移指南 和我们的 向后兼容包,以快速迭代的方式将您的应用升级到 v6。
安装 Remix
首先,您需要一些我们的软件包来在 Remix 上构建。按照以下说明,从项目根目录运行所有命令。
创建服务器和浏览器入口点
大多数 React Router 应用主要在浏览器中运行。服务器的唯一工作是发送单个静态 HTML 页面,而 React Router 则管理基于路由的客户端视图。这些应用通常有一个浏览器入口点文件,如根目录 index.js
,如下所示:
服务器渲染的 React 应用略有不同。浏览器脚本不会渲染您的应用,而是水化
服务器提供的 DOM。水化是将 DOM 中的元素映射到其对应的 React 组件并设置事件监听器的过程,以便您的应用具有交互性。
让我们首先创建两个新文件:
app/entry.server.tsx
(或entry.server.jsx
)app/entry.client.tsx
(或entry.client.jsx
)
app
目录中。如果您现有的应用使用同名目录,请将其重命名为 src
或 old-app
等,以便在我们迁移到 Remix 时进行区分。
您的客户端入口点将如下所示:
创建root
路由
我们提到过,Remix 是建立在 React Router 之上的。您的应用可能会使用 JSX Route
组件中定义的路由来渲染 BrowserRouter
。我们不需要在 Remix 中执行此操作,但稍后会详细介绍。目前,我们需要提供 Remix 应用运行所需的最低级别路由。
根路由(如果您是 Wes Bos,则为 root root
)负责提供应用程序的结构。其默认导出是一个组件,用于呈现其他所有路由加载并依赖的完整 HTML 树。可以将其视为应用程序的脚手架或外壳。
在客户端呈现的应用中,您将拥有一个索引 HTML 文件,其中包含用于挂载 React 应用的 DOM 节点。根路由将呈现与此文件结构相符的标记。
在 app
目录中创建一个名为 root.tsx
(或 root.jsx
)的新文件。该文件的内容会有所不同,但我们假设您的 index.html
如下所示:
在你的 root.tsx
中,导出一个镜像其结构的组件:
请注意以下几点:
- 我们删除了
noscript
标签。我们现在是服务器渲染,这意味着禁用 JavaScript 的用户仍然可以看到我们的应用(随着时间的推移,当您进行 一些调整以改进渐进式增强 时,您的大部分应用仍应可以正常工作)。 - 在根元素内,我们从
@remix-run/react
渲染一个Outlet
组件。这是您通常用于在 React Router 应用中渲染匹配路由的相同组件;它在这里提供相同的功能,但它适用于 Remix 中的路由器。
public
目录中删除 index.html
。保留该文件可能会导致您的服务器在访问 /
路由时发送该 HTML,而不是您的 Remix 应用。
调整您现有的应用程序代码
首先,将现有 React 代码的根目录移至 app
目录。因此,如果您的根应用代码位于项目根目录中的 src
目录中,则它现在应该位于 app/src
中。
我们还建议重命名此目录,以明确表明这是您的旧代码,以便最终在迁移所有内容后将其删除。这种方法的优点在于,您不必一次性完成所有操作,您的应用就可以正常运行。在我们的演示项目中,我们将此目录命名为 old-app
。
最后,在您的根 App
组件(将被挂载到 root
元素的组件)中,从 React Router 中删除 <BrowserRouter>
。Remix 会为您处理此问题,而无需直接呈现提供程序。
创建索引和捕获所有路由
Remix 需要根路由以外的路由来了解在 <Outlet />
中要呈现的内容。幸运的是,您已经在应用中呈现了 <Route>
组件,并且当您迁移到使用我们的 路由约定 时,Remix 可以使用这些组件。
首先,在 app
中创建一个名为 routes
的新目录。在该目录中,创建两个名为_index.tsx
和 $.tsx
的文件。$.tsx
称为catch-all 或splat
路由,它对于让您的旧应用处理尚未移入 routes
目录的路由非常有用。
在你的 _index.tsx
和 $.tsx
文件中,我们需要做的就是从旧的根 App
中导出代码:
使用 Remix 替换打包工具
Remix 提供自己的打包器和 CLI 工具,用于开发和构建您的应用。您的应用可能使用类似 Create React App 的工具进行引导,或者您可能使用 Webpack 设置了自定义构建。
在您的 package.json
文件中,更新您的脚本以使用 remix
命令而不是当前的构建和开发脚本。
然后噗!你的应用现在已由服务器渲染,构建时间从 90 秒缩短至 0.5 秒⚡
创建你的路由
随着时间的推移,您将需要将 React Router 的 <Route>
组件呈现的路由迁移到它们自己的路由文件中。我们的 路由约定 中概述的文件名和目录结构将指导此迁移。
路由文件中的默认导出是在 <Outlet />
中渲染的组件。因此,如果你的 App
中有一个如下所示的路由:
你的路由文件看起来应该像这样:
创建此文件后,您可以从 App
中删除 <Route>
组件。迁移所有路由后,您可以删除 <Routes>
,并最终删除 old-app
中的所有代码。
问题和后续步骤
此时,您可能可以说您已完成初始迁移。恭喜!但是,Remix 的做法与典型的 React 应用程序略有不同。如果没有,我们为什么要费心构建它呢?😅
不安全的浏览器引用
将客户端渲染的代码库迁移到服务器渲染的代码库时,一个常见的痛点是,你可能在服务器上运行的代码中引用了浏览器 API。在初始化状态中的值时可以找到一个常见的例子:
在此示例中,localStorage
用作全局存储,以在页面重新加载时保留某些数据。我们在 useEffect
中使用 count
的当前值更新 localStorage
,这非常安全,因为 useEffect
只会在浏览器中调用!但是,基于 localStorage
初始化状态是一个问题,因为此回调在服务器和浏览器中都执行。
您的首选解决方案可能是检查 window
对象并仅在浏览器中运行回调。然而,这可能会导致另一个问题,即可怕的hydration mismatch。React 依赖于服务器呈现的标记与客户端 hydration 期间呈现的标记相同。这确保了 react-dom
知道如何将 DOM 元素与其相应的 React 组件匹配,以便它可以附加事件侦听器并在状态更改时执行更新。因此,如果本地存储给我们的值与我们在服务器上启动的值不同,我们将面临一个新的问题需要处理。
仅限客户端的组件
这里的一个潜在解决方案是使用不同的缓存机制,该机制可以在服务器上使用,并通过从路由的 loader data 传递的 props 传递给组件。但是,如果你的应用不需要在服务器上渲染组件,那么更简单的解决方案可能是完全跳过服务器上的渲染,等到 hydration 完成后再在浏览器中渲染。
为了简化此解决方案,我们建议使用 remix-utils
社区包中的 ClientOnly
组件。其使用示例可在 examples
存储库 中找到。
React.lazy
和 React.Suspense
如果您使用 React.lazy
和 React.Suspense
延迟加载组件,您可能会遇到问题,具体取决于您使用的 React 版本。在 React 18 之前,这在服务器上不起作用,因为 React.Suspense
最初是作为仅限浏览器的功能实现的。
如果你使用的是 React 17,则有以下几个选择:
请记住,Remix 会自动处理它管理的所有路由的代码拆分,因此当您将内容移动到 routes
目录时,您很少(如果有的话)需要手动使用 React.lazy
。
配置
进一步的配置是可选的,但以下内容可能有助于优化您的开发工作流程。
remix.config.js
每个 Remix 应用都会在项目根目录中接受一个 remix.config.js
文件。虽然其设置是可选的,但为了清晰起见,我们建议您添加一些设置。有关所有可用选项的更多信息,请参阅 配置文档。
jsconfig.json
或 tsconfig.json
如果您使用的是 TypeScript,您的项目中可能已经有一个 tsconfig.json
。jsconfig.json
是可选的,但为许多编辑器提供了有用的上下文。这些是我们建议在您的语言配置中包含的最低设置。
路径别名,无论您的文件位于项目中的哪个位置,都可以轻松地从根目录导入模块。如果您更改 /_remix.config.js
中的 appDirectory
,您还需要更新 /_
的路径别名。
如果您使用 TypeScript,您还需要在项目根目录中创建具有适当全局类型引用的 remix.env.d.ts
文件。
关于非标准导入的说明
此时,您可能无需任何更改即可运行您的应用。如果您使用 Create React App 或高度配置的捆绑器设置,则可能会使用 import
来包含非 JavaScript 模块,例如样式表和图像。
Remix 不支持大多数非标准导入,我们认为这是有原因的。下面是您在 Remix 中会遇到的一些差异的非详尽列表,以及如何在迁移时进行重构。
资产导入
许多打包器使用插件来导入各种资产,例如图片和字体。这些资产通常以字符串形式进入您的组件,表示资产的文件路径。
在 Remix 中,其工作原理基本相同。对于通过 <link>
元素加载的字体等资产,您通常会在路由模块中导入这些资产,并将文件名包含在 links
函数返回的对象中。有关更多信息,请参阅我们关于路由 links
的文档。
SVG 导入
Create React App 和其他一些构建工具允许您将 SVG 文件导入为 React 组件。这是 SVG 文件的常见用例,但 Remix 默认不支持。
如果要使用 SVG 文件作为 React 组件,则需要先创建组件并直接导入它们。React SVGR 是一个很棒的工具集,可以帮助您从 命令行 或 在线游乐场 生成这些组件(如果您喜欢复制和粘贴)。
CSS 导入
Create React App 和许多其他构建工具都支持以各种方式在组件中导入 CSS。Remix 支持导入常规 CSS 文件以及下面介绍的几种流行的 CSS 捆绑解决方案。
路由 links
导出
在 Remix 中,常规样式表可以从路由组件文件中加载。导入它们不会对您的样式产生任何神奇的影响,而是会返回一个 URL,可用于根据需要加载样式表。您可以直接在组件中呈现样式表,也可以使用我们的 links
导出。
让我们将应用程序的样式表和一些其他资产移动到根路由中的 links
函数:
您会注意到,在第 32 行,我们渲染了一个 <Links />
组件,该组件替换了我们所有的单个 <link />
组件。如果我们只在根路由中使用链接,那么这无关紧要,但所有子路由都可以导出自己的链接,这些链接也将在此处渲染。links
函数还可以返回一个 PageLinkDescriptor
对象,它允许您预取用户可能导航到的页面的资源。
如果您目前在现有路由组件中将 <link />
标签注入到页面客户端,无论是直接注入还是通过 react-helmet
之类的抽象注入,您可以停止这样做,而是使用 links
导出。您可以删除大量代码,甚至删除一两个依赖项!
CSS 打包
Remix 内置支持 CSS 模块、Vanilla Extract 和 CSS 副作用导入。要使用这些功能,您需要在应用程序中设置 CSS 捆绑。
首先,要访问生成的 CSS 包,请安装 @remix-run/css-bundle
包。
然后,导入 cssBundleHref
并将其添加到链接描述符 - 最有可能在 root.tsx
中,以便它适用于整个应用程序。
<文档信息>
注意:Remix 目前不直接支持 Sass/Less 处理,但您仍然可以将它们作为单独的进程运行以生成 CSS 文件,然后可以将其导入到您的 Remix 应用程序中。
在 <head>
中渲染组件
正如 <link>
在您的路由组件内呈现并最终在您的根 <Links />
组件中呈现一样,您的应用可能会使用一些注入技巧来在文档 <head>
中呈现其他组件。通常这样做是为了更改文档的 <title>
或 <meta>
标签。
与 links
类似,每个路由也可以导出一个 meta
函数,该函数返回负责呈现该路由的 <meta>
标签的值(以及一些与元数据相关的其他标签,例如 <title>
、<link rel="canonical">
和 <script type="application/ld+json">
)。
meta
的行为与 links
略有不同。每个叶路由负责呈现自己的标签,而不是合并路由层次结构中其他 meta
函数的值。这是因为:
- 为了实现最佳 SEO,您通常希望对元数据进行更细粒度的控制
- 对于遵循 Open Graph 协议 的某些标签,某些标签的排序会影响爬虫和社交媒体网站对它们的解释方式,并且 Remix 很难预测复杂元数据的合并方式
- 某些标签允许多个值,而另一些则不允许,Remix 不应假设您希望如何处理所有这些情况
更新导入
Remix 会重新导出您从 react-router-dom
获得的所有内容,我们建议您更新导入以从 @remix-run/react
获取这些模块。在许多情况下,这些组件都包含专门针对 Remix 优化的附加功能和特性。
前:
后:
最后的想法
虽然我们已尽力提供全面的迁移指南,但需要注意的是,我们从头开始构建 Remix,并遵循了一些关键原则,这些原则与目前构建的许多 React 应用有很大不同。虽然您的应用此时可能会运行,但当您浏览我们的文档并探索我们的 API 时,我们认为您将能够大幅降低代码的复杂性并改善应用的最终用户体验。这可能需要一些时间,但您可以一次一口地解决这头大象。
现在,开始_重新组合你的应用_吧。我们认为你会喜欢你在此过程中构建的东西!💿