从 React Router 迁移
On this page

如果你想要一个 TL;DR 版本以及一个概述简化迁移的仓库,请查看我们的 example React Router-to-Remix repo

将 React Router 应用迁移到 Remix

¥Migrating your React Router App to Remix

本指南目前假设你使用的是 Classic Remix 编译器 而不是 Remix Vite

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.

确保你的应用使用 React Router v6

¥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.

安装 Remix

¥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)

按照惯例,Remix 中的所有应用代码都位于 app 目录中。如果你现有的应用使用同名目录,请将其重命名为 srcold-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.

创建索引和 catch-all 路由

¥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";

用 Remix 替换打包器

¥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.lazyReact.Suspense

¥React.lazy and React.Suspense

如果你使用 React.lazyReact.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:

继续,我们会到达那里,但我们无需更改任何已编写的代码即可获得核心功能。

¥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.jsontsconfig.json

¥jsconfig.json or tsconfig.json

如果你使用的是 TypeScript,你的项目中可能已经有 tsconfig.jsonjsconfig.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 都使用 /_ 路径别名轻松地从根目录导入模块。如果你在 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 导入

¥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 导入

¥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.

¥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 打包

¥CSS bundling

Remix 内置了对 CSS 模块Vanilla ExtractCSS 副作用导入 的支持。要使用这些功能,你需要在应用中设置 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 }]
      : []),
    // ...
  ];
};

有关更多信息,请参阅我们关于 CSS 打包的文档。

¥See our docs on CSS bundling for more information.

Note: Remix 目前不支持直接处理 Sass/Less,但你仍然可以将它们作为单独的进程运行,以生成 CSS 文件,然后将其导入到你的 Remix 应用中。

<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

Remix v2.17 中文网 - 粤ICP备13048890号