¥CSS Files
在 Remix 中管理 CSS 文件主要有两种方式:
¥There are two main ways to manage CSS files in Remix:
本指南介绍了每种方法的优缺点,并根据你项目的具体需求提供了一些建议。
¥This guide covers the pros and cons of each approach and provides some recommendations based on your project's specific needs.
¥CSS bundling
CSS 打包是 React 社区中管理 CSS 文件的最常用方法。在此模型中,样式被视为模块副作用,并由打包器自行打包到一个或多个 CSS 文件中。它使用起来更简单,需要的样板更少,并且赋予打包器更强大的输出优化能力。
¥CSS bundling is the most common approach for managing CSS files in the React community. In this model, styles are treated as module side effects and are bundled into one or more CSS files at the discretion of the bundler. It's simpler to use, requires less boilerplate, and gives the bundler more power to optimize the output.
例如,假设你有一个基本的 Button
组件,并附加了一些样式:
¥For example, let's say you have a basic Button
component with some styles attached to it:
.Button__root {
background: blue;
color: white;
}
import "./Button.css";
export function Button(props) {
return <button {...props} className="Button__root" />;
}
要使用此组件,你可以将其导入并在路由文件中使用:
¥To use this component, you can import it and use it in your route file:
import { Button } from "../components/Button";
export default function HelloRoute() {
return <Button>Hello!</Button>;
}
使用此组件时,你无需担心管理单个 CSS 文件。CSS 被视为组件的私有实现细节。这是许多组件库和设计系统中的常见模式,并且扩展性很好。
¥When consuming this component, you don't have to worry about managing individual CSS files. CSS is treated as a private implementation detail of the component. This is a common pattern in many component libraries and design systems and scales quite nicely.
¥CSS bundling is required for some CSS solutions
一些管理 CSS 文件的方法需要使用打包的 CSS。
¥Some approaches to managing CSS files require the use of bundled CSS.
例如,CSS 模块 是基于 CSS 已打包的假设构建的。即使你明确地将 CSS 文件的类名导入为 JavaScript 对象,样式本身仍会被视为副作用,并自动打包到输出中。你无法访问 CSS 文件的底层 URL。
¥For example, CSS Modules is built on the assumption that CSS is bundled. Even though you're explicitly importing the CSS file's class names as a JavaScript object, the styles themselves are still treated as a side effect and automatically bundled into the output. You have no access to the underlying URL of the CSS file.
另一个需要 CSS 打包的常见用例是,当你使用第三方组件库时,它会将 CSS 文件作为副作用导入,并依赖你的打包器为你处理,例如 React Spectrum。
¥Another common use case where CSS bundling is required is when you're using a third-party component library that imports CSS files as side effects and relies on your bundler to handle them for you, such as React Spectrum.
¥CSS order can differ between development and production
CSS 打包与 Vite 的按需编译方法结合使用时,需要付出显著的代价。
¥CSS bundling comes with a notable trade-off when combined with Vite's approach to on-demand compilation.
使用前面介绍的 Button.css
示例,此 CSS 文件将在开发过程中转换为以下 JavaScript 代码:
¥Using the Button.css
example presented earlier, this CSS file will be transformed into the following JavaScript code during development:
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 文件。
¥It's worth stressing that this transformation only happens in development. Production builds won't look like this since static CSS files are generated.
Vite 这样做是为了在导入 CSS 时进行延迟编译,然后在开发过程中进行热重载。导入此文件后,CSS 文件的内容会作为副作用注入到页面中。
¥Vite does this so that CSS can be compiled lazily when imported and then hot reloaded during development. As soon as this file is imported, the CSS file's contents are injected into the page as a side effect.
这种方法的缺点是这些样式与路由生命周期无关。这意味着在离开路由时,样式不会被卸载,从而导致在应用内导航时文档中旧样式的累积。这可能导致 CSS 规则顺序在开发和生产环境中有所不同。
¥The downside of this approach is that these styles are not tied to the route lifecycle. This means that styles won't be unmounted when navigating away from the route, leading to a build-up of old styles in the document while navigating around the app. This can result in CSS rule order differing between development and production.
为了缓解这种情况,以一种能够抵御文件排序变化的方式编写 CSS 会很有帮助。例如,你可以使用 CSS 模块 确保 CSS 文件的作用域限定于导入它们的文件。你还应该尝试限制指向单个元素的 CSS 文件数量,因为这些文件的顺序无法保证。
¥To mitigate this, it's helpful to write your CSS in a way that makes it resilient against changes to file ordering. For example, you can use CSS Modules to ensure that CSS files are scoped to the files that import them. You should also try to limit the number of CSS files that target a single element since the order of those files is not guaranteed.
¥Bundled CSS can disappear in development
Vite 在开发过程中使用 CSS 打包方法的另一个值得注意的缺点是,React 可能会无意中从文档中删除样式。
¥Another notable tradeoff with Vite's approach to CSS bundling during development is that React can inadvertently remove styles from the document.
当使用 React 渲染整个文档(例如 Remix 所做的那样)时,将元素动态注入 head
元素时可能会遇到问题。如果文档被重新挂载,现有的 head
元素将被删除并替换为一个全新的元素,从而删除 Vite 在开发过程中注入的所有 style
元素。
¥When React is used to render the entire document (as Remix does) you can run into issues when elements are dynamically injected into the head
element. If the document is re-mounted, the existing head
element is removed and replaced with an entirely new one, removing any style
elements that Vite injects during development.
在 Remix 中,此问题可能是由于水合错误 (Hydration Error) 引起的,因为它会导致 React 从头开始重新渲染整个页面。Hydration 错误可能是由你的应用代码引起的,也可能是由操作文档的浏览器扩展程序引起的。
¥In Remix, this issue can happen due to hydration errors since it causes React to re-render the entire page from scratch. Hydration errors can be caused by your app code, but they can also be caused by browser extensions that manipulate the document.
这是一个已知的 React 问题,已在其 预览版发布渠道 中修复。如果你了解所涉及的风险,你可以将你的应用固定到特定的 React 版本,然后使用 软件包覆盖 来确保这是整个项目中使用的唯一 React 版本。例如:
¥This is a known React issue fixed in their canary release channel. If you understand the risks involved, you can pin your app to a specific React version and then use package overrides to ensure this is the only version of React used throughout your project. For example:
{
"dependencies": {
"react": "18.3.0-canary-...",
"react-dom": "18.3.0-canary-..."
},
"overrides": {
"react": "18.3.0-canary-...",
"react-dom": "18.3.0-canary-..."
}
}
再次强调,Vite 注入的样式问题仅在开发过程中发生。生产构建不会出现此问题,因为会生成静态 CSS 文件。
¥Again, it's worth stressing that this issue with styles that were injected by Vite only happens in development. Production builds won't have this issue since static CSS files are generated.
¥CSS URL Imports
管理 CSS 文件的另一种主要方法是使用 Vite 显式 URL 导入。
¥The other main way to manage CSS files is to use Vite's explicit URL imports.
Vite 允许你将 ?url
附加到 CSS 文件导入中以获取文件的 URL(例如 import href from "./styles.css?url"
)。此 URL 随后可以通过 links
导出 从路由模块传递给 Remix。这会将 CSS 文件绑定到 Remix 的路由生命周期中,确保在应用导航时样式会被注入和从文档中删除。
¥Vite lets you append ?url
to your CSS file imports to get the URL of the file (e.g. import href from "./styles.css?url"
). This URL can then be passed to Remix via the links
export from route modules. This ties CSS files into Remix's routing lifecycle, ensuring styles are injected and removed from the document while navigating around the app.
例如,使用之前相同的 Button
组件示例,你可以将 links
数组与组件一起导出,以便用户可以访问其样式。
¥For example, using the same Button
component example from earlier, you can export a links
array alongside the component so that consumers have access to its styles.
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
导出:
¥When importing this component, consumers now also need to import this links
array and attach it to their route's 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 生命周期的一部分。
¥This approach is much more predictable in terms of rule ordering since it gives you granular control over each file and provides consistent behavior between development and production. As opposed to bundled CSS during development, styles are removed from the document when they are no longer needed. If the page's head
element is ever re-mounted, any link
tags defined by your routes will also be re-mounted since they are part of the React lifecycle.
这种方法的缺点是会产生大量的样板代码。
¥The downside of this approach is that it can result in a lot of boilerplate.
如果你有许多可复用组件,每个组件都有自己的 CSS 文件,则需要手动将每个组件的所有 links
添加到路由组件中,这可能需要将 CSS URL 传递到多个组件级别。这也容易出错,因为很容易忘记导入组件的 links
数组。
¥If you have many re-usable components each with their own CSS file, you'll need to manually surface all links
for each component up to your route components, which may require passing CSS URLs up through multiple levels of components. This can also be error-prone since it's easy to forget to import a component's links
array.
尽管 Remix 有很多优势,但与 CSS 打包相比,你可能会觉得它过于繁琐,或者你可能会觉得额外的样板代码是值得的。这一点没有对错之分。
¥Despite its advantages, you may find this to be too cumbersome compared to CSS bundling, or you may find the extra boilerplate to be worth it. There's no right or wrong on this one.
¥Conclusion
在 Remix 应用中管理 CSS 文件时,这最终取决于个人偏好,但这里有一个好的经验法则:
¥It's ultimately a personal preference when it comes to managing CSS files in your Remix application, but here's a good rule of thumb:
如果你的项目只有少量 CSS 文件(例如,使用 Tailwind 时,可能只有一个 CSS 文件),则应该使用 CSS URL 导入。增加的样板代码很少,你的开发环境将更接近生产环境。
¥If your project only has a small number of CSS files (e.g., when using Tailwind, in which case you might only have a single CSS file), you should use CSS URL imports. The increased boilerplate is minimal, and your development environment will be much closer to production.
如果你的项目包含大量与较小的可复用组件绑定的 CSS 文件,你可能会发现精简的 CSS 打包样板文件更加符合人机工程学。请注意权衡利弊,并以一种能够抵御文件顺序变化的方式编写 CSS。
¥If your project has a large number of CSS files tied to smaller re-usable components, you'll probably find the reduced boilerplate of CSS bundling to be much more ergonomic. Just be aware of the trade-offs and write your CSS in a way that makes it resilient against changes to file ordering.
如果你在开发过程中遇到样式消失的问题,你应该考虑使用 React 预览版发布,这样 React 在重新加载页面时就不会删除现有的 head
元素。
¥If you're experiencing issues with styles disappearing during development, you should consider using a React canary release so that React doesn't remove the existing head
element when re-mounting the page.