¥Migrating your React Router App to Remix
React Router 为全球部署的数百万个 React 应用提供支持。你可能已经发布了一些这样的请求!由于 Remix 构建于 React Router 之上,我们致力于使迁移过程变得简单,你可以迭代完成,从而避免大规模重构。
¥React Router powers millions of React applications deployed worldwide. Chances are you've shipped a few of them! Because Remix is built on top of React Router, we have worked to make migration an easy process you can work through iteratively to avoid huge refactors.
如果你尚未使用 React Router,我们认为有几个令人信服的理由值得你重新考虑!历史记录管理、动态路径匹配、嵌套路由等等。查看 React Router 文档 并了解我们提供的所有内容。
¥If you aren't already using React Router, we think there are several compelling reasons to reconsider! History management, dynamic path matching, nested routing, and much more. Take a look at the React Router docs and see all that we have to offer.
¥Ensure your app uses React Router v6
如果你使用的是旧版本的 React Router,第一步是升级到 v6。查看 从 v5 到 v6 的迁移指南 和我们的 向后兼容包,以便快速迭代地将你的应用升级到 v6。
¥If you are using an older version of React Router, the first step is to upgrade to v6. Check out the migration guide from v5 to v6 and our backwards compatibility package to upgrade your app to v6 quickly and iteratively.
¥Installing Remix
首先,你需要一些我们的软件包来在 Remix 上构建。按照以下说明,从项目根目录运行所有命令。
¥First, you'll need a few of our packages to build on Remix. Follow the instructions below, running all commands from the root of your project.
npm install @remix-run/react @remix-run/node @remix-run/serve
npm install -D @remix-run/dev
¥Creating server and browser entrypoints
大多数 React Router 应用主要在浏览器中运行。服务器的唯一工作是发送单个静态 HTML 页面,而 React Router 则在客户端管理基于路由的视图。这些应用通常有一个浏览器入口点文件,类似于根 index.js
,如下所示:
¥Most React Router apps run primarily in the browser. The server's only job is to send a single static HTML page while React Router manages the route-based views client-side. These apps generally have a browser entrypoint file like a root index.js
that looks something like this:
import { render } from "react-dom";
import App from "./App";
render(<App />, document.getElementById("app"));
服务器渲染的 React 应用略有不同。浏览器脚本不会渲染你的应用,而是服务器提供的 DOM。Hydration 是将 DOM 中的元素映射到其对应的 React 组件,并设置事件监听器的过程,从而使你的应用具有交互性。
¥Server-rendered React apps are a little different. The browser script is not rendering your app but is "hydrating" the DOM provided by the server. Hydration is the process of mapping the elements in the DOM to their React component counterparts and setting up event listeners so that your app is interactive.
我们先创建两个新文件:
¥Let's start by creating two new files:
app/entry.server.tsx
(或 entry.server.jsx
)
¥app/entry.server.tsx
(or entry.server.jsx
)
app/entry.client.tsx
(或 entry.client.jsx
)
¥app/entry.client.tsx
(or entry.client.jsx
)
app
目录中。如果你现有的应用使用同名目录,请将其重命名为 src
或 old-app
等名称,以便在迁移到 Remix 时进行区分。
import { PassThrough } from "node:stream";
import type {
AppLoadContext,
EntryContext,
} from "@remix-run/node";
import { createReadableStreamFromReadable } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";
const ABORT_DELAY = 5_000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext: AppLoadContext
) {
return isbot(request.headers.get("user-agent") || "")
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
);
}
function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onAllReady() {
const body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(
createReadableStreamFromReadable(body),
{
headers: responseHeaders,
status: responseStatusCode,
}
)
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
console.error(error);
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}
function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onShellReady() {
const body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(
createReadableStreamFromReadable(body),
{
headers: responseHeaders,
status: responseStatusCode,
}
)
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
console.error(error);
responseStatusCode = 500;
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}
你的客户端入口点将如下所示:
¥Your client entrypoint will look like this:
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});
root
路由¥Creating The root
route
我们提到过,Remix 是基于 React Router 构建的。你的应用可能会使用在 JSX Route
组件中定义的路由来渲染 BrowserRouter
。在 Remix 中我们不需要这样做,但稍后会详细介绍。目前,我们需要提供 Remix 应用运行所需的最底层路由。
¥We mentioned that Remix is built on top of React Router. Your app likely renders a BrowserRouter
with your routes defined in JSX Route
components. We don't need to do that in Remix, but more on that later. For now, we need to provide the lowest level route our Remix app needs to work.
根路由(如果你是 Wes Bos,则为 "根目录 root")负责提供应用的结构。它的默认导出组件会渲染其他所有路由加载并依赖的完整 HTML 树。把它想象成你应用的脚手架或外壳。
¥The root route (or the "root root" if you're Wes Bos) is responsible for providing the structure of the application. Its default export is a component that renders the full HTML tree that every other route loads and depends on. Think of it as the scaffold or shell of your app.
在客户端渲染的应用中,你将拥有一个包含用于挂载 React 应用的 DOM 节点的索引 HTML 文件。根路由将渲染与该文件结构对应的标记。
¥In a client-rendered app, you will have an index HTML file that includes the DOM node for mounting your React app. The root route will render markup that mirrors the structure of this file.
在 app
目录中创建一个名为 root.tsx
(或 root.jsx
)的新文件。该文件的内容会有所不同,但我们假设你的 index.html
如下所示:
¥Create a new file called root.tsx
(or root.jsx
) in your app
directory. The contents of that file will vary, but let's assume that your index.html
looks something like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="My beautiful React app"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>My React App</title>
</head>
<body>
<noscript
>You need to enable JavaScript to run this
app.</noscript
>
<div id="root"></div>
</body>
</html>
在你的 root.tsx
文件中,导出一个与其结构相同的组件:
¥In your root.tsx
, export a component that mirrors its structure:
import { Outlet } from "@remix-run/react";
export default function Root() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="My beautiful React app"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>My React App</title>
</head>
<body>
<div id="root">
<Outlet />
</div>
</body>
</html>
);
}
请注意以下几点:
¥Notice a few things here:
我们删除了 noscript
标签。我们现在支持服务器渲染,这意味着即使禁用 JavaScript,用户仍然可以看到我们的应用(并且随着时间的推移,随着 一些改进以改进渐进增强的调整 的推出,你的应用的大部分功能应该仍然可以正常工作)。
¥We got rid of the noscript
tag. We're server rendering now, which means users who disable JavaScript will still be able to see our app (and over time, as you make a few tweaks to improve progressive enhancement, much of your app should still work).
在根元素中,我们从 @remix-run/react
渲染一个 Outlet
组件。这与你在 React Router 应用中通常用于渲染匹配路由的组件相同;它在这里提供相同的功能,但它适用于 Remix 中的路由。
¥Inside the root element we render an Outlet
component from @remix-run/react
. This is the same component that you would normally use to render your matched route in a React Router app; it serves the same function here, but it's adapted for the router in Remix.
public
目录中删除 index.html
。保留该文件可能会导致服务器在访问 /
路由时发送该 HTML 而不是 Remix 应用。
¥Adapting your existing app code
首先,将现有 React 代码的根目录移动到 app
目录中。如果你的根应用代码位于项目根目录下的 src
目录中,那么它现在应该位于 app/src
目录中。
¥First, move the root of your existing React code into your app
directory. So if your root app code lives in an src
directory in the project root, it should now be in app/src
.
我们还建议重命名此目录,以明确表明这是你的旧代码,以便你最终在迁移所有内容后将其删除。这种方法的优点在于,你不必一次性完成所有操作,你的应用就能正常运行。在我们的演示项目中,我们将此目录命名为 old-app
。
¥We also suggest renaming this directory to make it clear that this is your old code so that, eventually, you can delete it after migrating all of its contents. The beauty of this approach is that you don't have to do it all at once for your app to run as usual. In our demo project we name this directory old-app
.
最后,在你的根 App
组件(原本应该挂载到 root
元素的组件)中,从 React Router 中移除 <BrowserRouter>
。Remix 会为你处理这些,而无需你直接渲染提供程序。
¥Lastly, in your root App
component (the one that would have been mounted to the root
element), remove the <BrowserRouter>
from React Router. Remix takes care of this for you without needing to render the provider directly.
¥Creating an index and a catch-all route
Remix 需要根路由以外的路由才能知道在 <Outlet />
中渲染什么。幸运的是,你已经在应用中渲染了 <Route>
组件,Remix 可以在你迁移到 路由约定 时使用这些组件。
¥Remix needs routes beyond the root route to know what to render in <Outlet />
. Fortunately you already render <Route>
components in your app, and Remix can use those as you migrate to use our routing conventions.
首先,在 app
中创建一个名为 routes
的新目录。在该目录中,创建两个名为 _index.tsx
和 $.tsx
的文件。$.tsx
被称为 catch-all 或 "splat" 路由,它对于让你的旧应用处理尚未移入 routes
目录的路由非常有用。
¥To start, create a new directory in app
called routes
. In that directory, create two files called _index.tsx
and $.tsx
. $.tsx
is called a catch-all or "splat" route, and it will be useful to let your old app handle routes that you haven't moved into the routes
directory yet.
在 _index.tsx
和 $.tsx
文件中,我们需要做的就是从旧的根 App
导出代码:
¥Inside your _index.tsx
and $.tsx
files, all we need to do is export the code from our old root App
:
export { default } from "~/old-app/app";
export { default } from "~/old-app/app";
¥Replacing the bundler with Remix
Remix 提供了自己的打包工具和 CLI 工具,用于开发和构建你的应用。你的应用可能使用了类似 Create React App 的工具进行引导,或者你可能使用 Webpack 设置了自定义构建。
¥Remix provides its own bundler and CLI tools for development and building your app. Chances are your app used something like Create React App to bootstrap, or perhaps you have a custom build set up with Webpack.
在你的 package.json
文件中,请更新你的脚本以使用 remix
命令,而不是你当前的构建和开发脚本。
¥In your package.json
file, update your scripts to use remix
commands instead of your current build and dev scripts.
{
"scripts": {
"build": "remix build",
"dev": "remix dev",
"start": "remix-serve build/index.js",
"typecheck": "tsc"
}
}
噗!你的应用现已进行服务器渲染,构建时间从 90 秒缩短至 0.5 秒。 ⚡
¥And poof! Your app is now server-rendered, and your build went from 90 seconds to 0.5 seconds ⚡
¥Creating your routes
随着时间的推移,你需要将 React Router 的 <Route>
组件渲染的路由迁移到它们自己的路由文件中。我们 路由约定 中概述的文件名和目录结构将指导此次迁移。
¥Over time, you'll want to migrate the routes rendered by React Router's <Route>
components into their own route files. The filenames and directory structure outlined in our routing conventions will guide this migration.
路由文件中的默认导出是 <Outlet />
中渲染的组件。如果你的 App
路由如下所示:
¥The default export in your route file is the component rendered in the <Outlet />
. So if you have a route in your App
that looks like this:
function About() {
return (
<main>
<h1>About us</h1>
<PageContent />
</main>
);
}
function App() {
return (
<Routes>
<Route path="/about" element={<About />} />
</Routes>
);
}
你的路由文件应如下所示:
¥Your route file should look like this:
export default function About() {
return (
<main>
<h1>About us</h1>
<PageContent />
</main>
);
}
创建此文件后,你可以从 App
中删除 <Route>
组件。所有路由迁移完成后,你可以删除 <Routes>
,并最终删除 old-app
中的所有代码。
¥Once you create this file, you can delete the <Route>
component from your App
. After all of your routes have been migrated you can delete <Routes>
and ultimately all the code in old-app
.
¥Gotchas and next steps
此时,你可能可以说初始迁移已经完成。恭喜!然而,Remix 的做法与典型的 React 应用略有不同。如果不是,我们当初为什么要费心构建它呢?😅
¥At this point you might be able to say you are done with the initial migration. Congrats! However, Remix does things a bit differently than your typical React app. If it didn't, why would we have bothered building it in the first place? 😅
¥Unsafe browser references
将客户端渲染的代码库迁移到服务器渲染的代码库时,一个常见的痛点是,在服务器上运行的代码中可能会引用浏览器 API。在初始化状态值时可以找到一个常见示例:
¥A common pain-point in migrating a client-rendered codebase to a server-rendered one is that you may have references to browser APIs in code that runs on the server. A common example can be found when initializing values in state:
function Count() {
const [count, setCount] = React.useState(
() => localStorage.getItem("count") || 0
);
React.useEffect(() => {
localStorage.setItem("count", count);
}, [count]);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
在此示例中,localStorage
用作全局存储,用于在页面重新加载时持久化部分数据。我们在 useEffect
中使用 count
的当前值更新 localStorage
,这非常安全,因为 useEffect
只会在浏览器中调用!但是,基于 localStorage
初始化状态会存在问题,因为此回调会在服务器和浏览器中同时执行。
¥In this example, localStorage
is used as a global store to persist some data across page reloads. We update localStorage
with the current value of count
in useEffect
, which is perfectly safe because useEffect
is only ever called in the browser! However, initializing state based on localStorage
is a problem, as this callback is executed on both the server and in the browser.
你的首选解决方案可能是检查 window
对象并仅在浏览器中运行回调。然而,这可能会导致另一个问题,那就是可怕的 hydration 不匹配。React 依赖于服务器渲染的标记与客户端 hydration 期间渲染的标记完全相同。这确保 react-dom
知道如何将 DOM 元素与其对应的 React 组件匹配,以便它可以附加事件监听器并在状态更改时执行更新。因此,如果本地存储返回的值与我们在服务器上初始化的值不同,我们将面临新的问题。
¥Your go-to solution may be to check for the window
object and only run the callback in the browser. However, this can lead to another problem, which is the dreaded hydration mismatch. React relies on markup rendered by the server to be identical to what is rendered during client hydration. This ensures that react-dom
knows how to match DOM elements with their corresponding React components so that it can attach event listeners and perform updates as state changes. So if local storage gives us a different value than whatever we initiate on the server, we'll have a new problem to deal with.
¥Client-only components
一个潜在的解决方案是使用不同的缓存机制,该机制可以在服务器上使用,并通过从路由的 加载器数据 传递的 props 传递给组件。但是如果你的应用不需要在服务器上渲染组件,那么一个更简单的解决方案可能是完全跳过服务器上的渲染,等到 hydration 完成后再在浏览器中渲染。
¥One potential solution here is using a different caching mechanism that can be used on the server and passed to the component via props passed from a route's loader data. But if it isn't crucial for your app to render the component on the server, a simpler solution may be to skip rendering altogether on the server and wait until hydration is complete to render it in the browser.
// We can safely track hydration in memory state
// outside the component because it is only
// updated once after the version instance of
// `SomeComponent` has been hydrated. From there,
// the browser takes over rendering duties across
// route changes and we no longer need to worry
// about hydration mismatches until the page is
// reloaded and `isHydrating` is reset to true.
let isHydrating = true;
function SomeComponent() {
const [isHydrated, setIsHydrated] = React.useState(
!isHydrating
);
React.useEffect(() => {
isHydrating = false;
setIsHydrated(true);
}, []);
if (isHydrated) {
return <Count />;
} else {
return <SomeFallbackComponent />;
}
}
为了简化此解决方案,我们建议使用 remix-utils
社区包中的 ClientOnly
组件。其用法示例可在 examples
代码库 中找到。
¥To simplify this solution, we recommend the using the ClientOnly
component in the remix-utils
community package. An example of its usage can be found in the examples
repository.
React.lazy
和 React.Suspense
¥React.lazy
and React.Suspense
如果你使用 React.lazy
和 React.Suspense
进行延迟加载组件,则可能会遇到问题,具体取决于你使用的 React 版本。在 React 18 之前,此功能在服务器上无法工作,因为 React.Suspense
最初是作为仅用于浏览器的功能实现的。
¥If you are lazy-loading components with React.lazy
and React.Suspense
, you may run into issues depending on the version of React you are using. Until React 18, this would not work on the server as React.Suspense
was originally implemented as a browser-only feature.
如果你使用的是 React 17,你有以下几种选择:
¥If you are using React 17, you have a few options:
升级到 React 18
¥Upgrade to React 18
使用上面概述的 仅客户端方法
¥Use the client-only approach outlined above
使用其他延迟加载解决方案,例如 可加载组件
¥Use an alternative lazy-loading solution such as Loadable Components
同时删除 React.lazy
和 React.Suspense
¥Remove React.lazy
and React.Suspense
altogether
继续,我们会到达那里,但我们无需更改任何已编写的代码即可获得核心功能。
¥Keep in mind that Remix automatically handles code-splitting for all your routes that it manages, so as you move things into the routes
directory you should rarely—if ever—need to use React.lazy
manually.
¥Configuration
进一步的配置是可选的,但以下内容可能有助于优化你的开发工作流程。
¥Further configuration is optional, but the following may be helpful to optimize your development workflow.
remix.config.js
每个 Remix 应用都会在项目根目录中接受一个 remix.config.js
文件。虽然它的设置是可选的,但为了清晰起见,我们建议你包含其中的一些设置。有关所有可用选项的更多信息,请参阅 配置文档。
¥Every Remix app accepts a remix.config.js
file in the project root. While its settings are optional, we recommend you include a few of them for clarity's sake. See the docs on configuration for more information about all available options.
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
appDirectory: "app",
ignoredRouteFiles: ["**/*.css"],
assetsBuildDirectory: "public/build",
};
jsconfig.json
或 tsconfig.json
¥jsconfig.json
or tsconfig.json
如果你使用的是 TypeScript,你的项目中可能已经有 tsconfig.json
。jsconfig.json
是可选的,但它为许多编辑器提供了有用的上下文。这些是我们建议在你的语言配置中包含的最低设置。
¥If you are using TypeScript, you likely already have a tsconfig.json
in your project. jsconfig.json
is optional but provides helpful context for many editors. These are the minimal settings we recommend including in your language configuration.
路径别名轻松地从根目录导入模块。如果你在 /_remix.config.js
中更改了 appDirectory
,则还需要更新 /_
的路径别名。
{
"compilerOptions": {
"jsx": "react-jsx",
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
}
}
}
{
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"resolveJsonModule": true,
"moduleResolution": "Bundler",
"baseUrl": ".",
"noEmit": true,
"paths": {
"~/*": ["./app/*"]
}
}
}
如果你使用的是 TypeScript,你还需要在项目根目录中创建 remix.env.d.ts
文件,并添加相应的全局类型引用。
¥If you are using TypeScript, you also need to create the remix.env.d.ts
file in the root of your project with the appropriate global type references.
/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/node" />
¥A note about non-standard imports
此时,你可能无需任何更改即可运行你的应用。如果你使用的是 Create React App 或高度配置的打包器设置,则你很可能使用 import
来添加非 JavaScript 模块,例如样式表和图片。
¥At this point, you might be able to run your app with no changes. If you are using Create React App or a highly configured bundler setup, you likely use import
to include non-JavaScript modules like stylesheets and images.
Remix 不支持大多数非标准导入,我们认为这是有原因的。以下列出了你在 Remix 中会遇到的一些差异,以及如何在迁移过程中进行重构。
¥Remix does not support most non-standard imports, and we think for good reason. Below is a non-exhaustive list of some of the differences you'll encounter in Remix and how to refactor as you migrate.
¥Asset imports
许多打包器使用插件来允许导入各种资源,例如图片和字体。它们通常以表示资源文件路径的字符串形式出现在你的组件中。
¥Many bundlers use plugins to allow importing various assets like images and fonts. These typically come into your component as string representing the filepath of the asset.
import logo from "./logo.png";
export function Logo() {
return <img src={logo} alt="My logo" />;
}
在 Remix 中,这基本以相同的方式工作。对于由 <link>
元素加载的资源(例如字体),通常会在路由模块中导入它们,并将文件名包含在 links
函数返回的对象中。有关更多信息,请参阅我们关于路由 links
的文档。
¥In Remix, this works basically the same way. For assets like fonts that are loaded by a <link>
element, you'll generally import these in a route module and include the filename in an object returned by a links
function. See our docs on route links
for more information.
¥SVG imports
Create React App 和一些其他构建工具允许你将 SVG 文件导入为 React 组件。这是 SVG 文件的常见用例,但 Remix 默认不支持。
¥Create React App and some other build tools allow you to import SVG files as a React component. This is a common use case for SVG files, but it's not supported by default in Remix.
// This will not work in Remix!
import MyLogo from "./logo.svg";
export function Logo() {
return <MyLogo />;
}
如果你想使用 SVG 文件作为 React 组件,你需要先创建组件并直接导入它们。React SVGR 是一个很棒的工具集,可以帮助你从 命令行 或 在线在线运行(如果你喜欢复制粘贴)生成这些组件。
¥If you want to use SVG files as React components, you'll need to first create the components and import them directly. React SVGR is a great toolset that can help you generate these components from the command line or in an online playground if you prefer to copy and paste.
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.414V13a1 1 0 102 0V9.414l1.293 1.293a1 1 0 001.414-1.414z" />
</svg>
export default function Icon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className="icon"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.414V13a1 1 0 102 0V9.414l1.293 1.293a1 1 0 001.414-1.414z"
/>
</svg>
);
}
¥CSS imports
Create React App 和许多其他构建工具支持以各种方式在组件中导入 CSS。Remix 支持导入常规 CSS 文件以及下文介绍的几种流行的 CSS 打包解决方案。
¥Create React App and many other build tools support importing CSS in your components in various ways. Remix supports importing regular CSS files along with several popular CSS bundling solutions described below.
links
导出¥Route links
exports
在 Remix 中,常规样式表可以从路由组件文件中加载。导入它们不会对你的样式产生任何神奇的影响,而是返回一个 URL,你可以根据需要使用该 URL 加载样式表。你可以直接在组件中渲染样式表,也可以使用我们的 links
导出。
¥In Remix, regular stylesheets can be loaded from route component files. Importing them does not do anything magical with your styles, rather it returns a URL that can be used to load the stylesheet as you see fit. You can render the stylesheet directly in your component or use our links
export.
让我们将应用的样式表和其他一些资源移动到根路由中的 links
函数中:
¥Let's move our app's stylesheet and a few other assets to the links
function in our root route:
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
import { Links } from "@remix-run/react";
import App from "./app";
import stylesheetUrl from "./styles.css";
export const links: LinksFunction = () => {
// `links` returns an array of objects whose
// properties map to the `<link />` component props
return [
{ rel: "icon", href: "/favicon.ico" },
{ rel: "apple-touch-icon", href: "/logo192.png" },
{ rel: "manifest", href: "/manifest.json" },
{ rel: "stylesheet", href: stylesheetUrl },
];
};
export default function Root() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<Links />
<title>React App</title>
</head>
<body>
<App />
</body>
</html>
);
}
你会注意到在第 32 行,我们渲染了一个 <Links />
组件,它替换了所有单独的 <link />
组件。如果我们只在根路由中使用链接,那么这无关紧要,但所有子路由都可以导出自己的链接,这些链接也会在这里渲染。links
函数还可以返回一个 PageLinkDescriptor
对象 对象,允许你预取用户可能导航到的页面的资源。
¥You'll notice on line 32 that we've rendered a <Links />
component that replaced all of our individual <link />
components. This is inconsequential if we only ever use links in the root route, but all child routes may export their own links that will also be rendered here. The links
function can also return a PageLinkDescriptor
object that allows you to prefetch the resources for a page the user is likely to navigate to.
如果你目前在现有路由组件中将 <link />
标签直接或通过类似 react-helmet
的抽象注入到页面客户端,你可以停止这样做,而改用 links
导出。你可以删除大量代码,甚至可能删除一两个依赖!
¥If you currently inject <link />
tags into your page client-side in your existing route components, either directly or via an abstraction like react-helmet
, you can stop doing that and instead use the links
export. You get to delete a lot of code and possibly a dependency or two!
¥CSS bundling
Remix 内置了对 CSS 模块、Vanilla Extract 和 CSS 副作用导入 的支持。要使用这些功能,你需要在应用中设置 CSS 打包。
¥Remix has built-in support for CSS Modules, Vanilla Extract and CSS side effect imports. To make use of these features, you'll need to set up CSS bundling in your application.
首先,要访问生成的 CSS 包,请安装 @remix-run/css-bundle
包。
¥First, to get access to the generated CSS bundle, install the @remix-run/css-bundle
package.
npm install @remix-run/css-bundle
然后,导入 cssBundleHref
并将其添加到链接描述符中 - 很可能是在 root.tsx
中,以便它适用于你的整个应用。
¥Then, import cssBundleHref
and add it to a link descriptor—most likely in root.tsx
so that it applies to your entire application.
import { cssBundleHref } from "@remix-run/css-bundle";
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
export const links: LinksFunction = () => {
return [
...(cssBundleHref
? [{ rel: "stylesheet", href: cssBundleHref }]
: []),
// ...
];
};
¥See our docs on CSS bundling for more information.
<head>
中渲染组件¥Rendering components in <head>
正如 <link>
会在路由组件内部渲染,并最终在根组件 <Links />
中渲染一样,你的应用可能会使用一些注入技巧在文档 <head>
中渲染其他组件。这通常是为了更改文档的 <title>
或 <meta>
标签。
¥Just as a <link>
is rendered inside your route component and ultimately rendered in your root <Links />
component, your app may use some injection trickery to render additional components in the document <head>
. Often this is done to change the document's <title>
or <meta>
tags.
与 links
类似,每个路由也可以导出一个 meta
函数,该函数返回负责渲染该路由的 <meta>
标签的值(以及其他一些与元数据相关的标签,例如 <title>
、<link rel="canonical">
和 <script type="application/ld+json">
)。
¥Similar to links
, each route can also export a meta
function that returns values responsible for rendering <meta>
tags for that route (as well as a few other tags relevant for metadata, such as <title>
, <link rel="canonical">
and <script type="application/ld+json">
).
meta
的行为与 links
略有不同。无需合并路由层次结构中其他 meta
函数的值,每个叶路由负责渲染自己的标签。这是因为:
¥The behavior for meta
is slightly different from links
. Instead of merging values from other meta
functions in the route hierarchy, each leaf route is responsible for rendering its own tags. This is because:
为了实现最佳 SEO,你通常需要对元数据进行更细粒度的控制
¥You often want more fine-grained control over metadata for optimal SEO
对于 Open Graph 协议 之后的某些标签,其顺序会影响爬虫和社交媒体网站对它们的解释方式。Remix 很难预测复杂的元数据应该如何合并。
¥In the case of some tags that follow the Open Graph protocol, the ordering of some tags impacts how they are interpreted by crawlers and social media sites. It's less predictable for Remix to assume how complex metadata should be merged
有些标签允许多个值,而有些则不允许,Remix 不应该假设你希望如何处理所有这些情况。
¥Some tags allow for multiple values while others do not, and Remix shouldn't assume how you want to handle all of those cases
¥Updating imports
Remix 重新导出了你从 react-router-dom
获得的所有内容,我们建议你更新导入以从 @remix-run/react
获取这些模块。在许多情况下,这些组件都包含专门针对 Remix 优化的附加功能和特性。
¥Remix re-exports everything you get from react-router-dom
and we recommend that you update your imports to get those modules from @remix-run/react
. In many cases, those components are wrapped with additional functionality and features specifically optimized for Remix.
之前:
¥Before:
import { Link, Outlet } from "react-router-dom";
之后:
¥After:
import { Link, Outlet } from "@remix-run/react";
¥Final Thoughts
虽然我们已尽力提供全面的迁移指南,但需要注意的是,我们从零开始构建 Remix 时,遵循了一些关键原则,这些原则与目前许多 React 应用的构建方式截然不同。虽然你的应用此时很可能已经运行,但随着你仔细阅读我们的文档并探索我们的 API,我们相信你将能够大幅降低代码的复杂性,并提升应用的终端用户体验。这可能需要一些时间,但你可以一点一点地解决它。
¥While we've done our best to provide a comprehensive migration guide, it's important to note that we built Remix from the ground up with a few key principles that differ significantly from how many React apps are currently built. While your app will likely run at this point, as you dig through our docs and explore our APIs, we think you'll be able to drastically reduce the complexity of your code and improve the end-user experience of your app. It might take a bit of time to get there, but you can eat that elephant one bite at a time.
现在,开始重新组合你的应用吧。我们相信你会喜欢你一路走来所构建的内容!💿
¥Now then, go off and remix your app. We think you'll like what you build along the way! 💿
¥Further reading