React Router v7 已发布。 查看文档
CSS 文件

CSS 文件

在 Remix 中,管理 CSS 文件主要有两种方式

本指南涵盖了每种方法的优缺点,并根据你项目的具体需求提供一些建议。

CSS 打包

CSS 打包是 React 社区中管理 CSS 文件最常见的方法。在这个模型中,样式被视为模块的副作用,并由打包器酌情打包到一个或多个 CSS 文件中。它使用起来更简单,需要的样板代码更少,并且使打包器有更大的能力来优化输出。

例如,假设你有一个基本的 Button 组件,并附加了一些样式

.Button__root {
  background: blue;
  color: white;
}
import "./Button.css";

export function Button(props) {
  return <button {...props} className="Button__root" />;
}

要使用此组件,你可以简单地导入它并在你的路由文件中使用它

import { Button } from "../components/Button";

export default function HelloRoute() {
  return <Button>Hello!</Button>;
}

当使用此组件时,你无需担心管理单个 CSS 文件。CSS 被视为组件的私有实现细节。这是许多组件库和设计系统中的常见模式,并且可以很好地扩展。

某些 CSS 解决方案需要 CSS 打包

某些管理 CSS 文件的方法需要使用打包的 CSS。

例如,CSS Modules 是建立在 CSS 被打包的假设之上的。即使你显式地将 CSS 文件的类名作为 JavaScript 对象导入,样式本身仍然被视为副作用并自动打包到输出中。你无法访问 CSS 文件的底层 URL。

另一个需要 CSS 打包的常见用例是,当你使用导入 CSS 文件作为副作用并依赖你的打包器为你处理它们的第三方组件库时,例如 React Spectrum

开发和生产环境之间的 CSS 顺序可能不同

当与 Vite 的按需编译方法结合使用时,CSS 打包会带来一个显著的权衡。

使用前面介绍的 Button.css 示例,此 CSS 文件将在开发过程中转换为以下 JavaScript 代码

import {createHotContext as __vite__createHotContext} from "/@vite/client";
import.meta.hot = __vite__createHotContext("/app/components/Button.css");
import {updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle} from "/@vite/client";
const __vite__id = "/path/to/app/components/Button.css";
const __vite__css = ".Button__root{background:blue;color:white;}"
__vite__updateStyle(__vite__id, __vite__css);
import.meta.hot.accept();
import.meta.hot.prune(()=>__vite__removeStyle(__vite__id));

值得强调的是,这种转换仅在开发过程中发生。生产版本不会像这样,因为会生成静态 CSS 文件。

Vite 这样做是为了在导入时可以延迟编译 CSS,然后在开发过程中进行热重载。一旦导入此文件,CSS 文件的内容就会作为副作用注入到页面中。

这种方法的缺点是这些样式没有与路由生命周期绑定。这意味着当从路由导航离开时,样式不会被卸载,从而导致在应用程序中导航时文档中积累旧样式。这可能会导致开发和生产环境之间的 CSS 规则顺序不同。

为了缓解这种情况,以一种使其能够抵抗文件顺序变化的方式编写 CSS 会很有帮助。例如,你可以使用 CSS Modules 来确保 CSS 文件的作用域限定为导入它们的文件。你还应该尽量限制以单个元素为目标的 CSS 文件数量,因为这些文件的顺序无法保证。

打包的 CSS 在开发环境中可能会消失

在开发期间,Vite 的 CSS 打包方法的另一个显著缺点是,React 可能会无意中从文档中删除样式。

当 React 用于渲染整个文档(如 Remix 所做的那样)时,当元素被动态注入到 head 元素中时,你可能会遇到问题。如果文档被重新挂载,则现有的 head 元素将被删除,并替换为全新的元素,从而删除 Vite 在开发过程中注入的任何 style 元素。

在 Remix 中,这个问题可能会由于 hydration 错误而发生,因为它会导致 React 从头开始重新渲染整个页面。Hydration 错误可能是由你的应用程序代码引起的,但也可能是由操作文档的浏览器扩展引起的。

这是一个已知的 React 问题,已在其 canary 发布渠道中修复。如果你了解其中涉及的风险,你可以将你的应用程序固定到特定的 React 版本,然后使用 package overrides 来确保这是整个项目中使用的唯一 React 版本。例如

{
  "dependencies": {
    "react": "18.3.0-canary-...",
    "react-dom": "18.3.0-canary-..."
  },
  "overrides": {
    "react": "18.3.0-canary-...",
    "react-dom": "18.3.0-canary-..."
  }
}

作为参考,这正是 Next.js 在内部代表你处理 React 版本的方式,所以这种方法比你想象的更广泛使用,即使 Remix 没有将其作为默认设置提供。

再次强调,这个问题与 Vite 注入的样式仅在开发过程中发生。生产构建不会有这个问题,因为会生成静态 CSS 文件。

CSS URL 导入

管理 CSS 文件的另一种主要方法是使用 Vite 的显式 URL 导入

Vite 允许你将 ?url 附加到你的 CSS 文件导入,以获取文件的 URL(例如,import href from "./styles.css?url")。然后可以通过路由模块的 links export 将此 URL 传递给 Remix。这会将 CSS 文件绑定到 Remix 的路由生命周期,确保在应用程序中导航时将样式注入并从文档中删除。

例如,使用之前相同的 Button 组件示例,你可以在组件旁边导出 links 数组,以便消费者可以访问其样式。

import buttonCssUrl from "./Button.css?url";

export const links = [
  { rel: "stylesheet", href: buttonCssUrl },
];

export function Button(props) {
  return <button {...props} className="Button__root" />;
}

在导入此组件时,消费者现在还需要导入此 links 数组并将其附加到其路由的 links export 中

import {
  Button,
  links as buttonLinks,
} from "../components/Button";

export const links = () => [...buttonLinks];

export default function HelloRoute() {
  return <Button>Hello!</Button>;
}

这种方法在规则排序方面更具可预测性,因为它让你能够精细控制每个文件,并在开发和生产之间提供一致的行为。与开发期间捆绑的 CSS 相反,当样式不再需要时,它们会从文档中删除。如果页面的 head 元素被重新挂载,则由你的路由定义的任何 link 标签也将被重新挂载,因为它们是 React 生命周期的一部分。

这种方法的缺点是它可能导致大量的样板代码。

如果你有许多可重用的组件,每个组件都有自己的 CSS 文件,你需要手动将每个组件的所有 links 都放到你的路由组件中,这可能需要将 CSS URL 传递多个组件级别。这也容易出错,因为很容易忘记导入组件的 links 数组。

尽管有其优点,你可能会发现这与 CSS 捆绑相比过于麻烦,或者你可能会发现额外的样板代码是值得的。这方面没有对错之分。

结论

在 Remix 应用程序中管理 CSS 文件最终取决于个人喜好,但这里有一个很好的经验法则

  • 如果你的项目只有少量 CSS 文件(例如,当使用 Tailwind 时,你可能只有一个 CSS 文件),你应该使用 CSS URL 导入。增加的样板代码最少,你的开发环境将更接近生产环境。
  • 如果你的项目有大量与较小的可重用组件绑定的 CSS 文件,你可能会发现 CSS 捆绑的减少的样板代码更符合人体工程学。请注意权衡,并以一种使其能够抵抗文件排序变化的方式编写你的 CSS。
  • 如果你在开发过程中遇到样式消失的问题,你应该考虑使用 React canary 版本,这样 React 在重新挂载页面时不会删除现有的 head 元素。
文档和示例在以下许可下授权 MIT