跳转到内容

Regular CSS

常规 CSS

Remix 可帮助您使用带有嵌套路由和 links 的常规 CSS 来扩展应用程序。

CSS 维护问题可能会因多种原因而潜入 Web 应用中。可能很难知道:

  • 如何以及何时加载 CSS,因此通常每个页面都会加载
  • 如果您使用的类名和选择器意外地为应用程序中的其他 UI 设定了样式
  • 如果随着 CSS 源代码的不断增长,某些规则甚至不再使用

Remix 使用基于路由的样式表缓解了这些问题。嵌套路由可以各自将自己的样式表添加到页面,Remix 将使用路由自动预取、加载和卸载它们。当关注范围仅限于活动路由时,这些问题的风险会大大降低。唯一可能出现冲突的是父路由的样式(即使如此,您也可能会看到冲突,因为父路由也在渲染)。

如果您使用的是 Classic Remix Compiler 而不是 Remix Vite,则应从 CSS 导入路径末尾删除 ?url

路由样式

每个路由都可以在页面中添加样式链接,例如:

app/routes/dashboard.tsx
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
import styles from "~/styles/dashboard.css?url";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: styles },
];
app/routes/dashboard.accounts.tsx
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
import styles from "~/styles/accounts.css?url";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: styles },
];
app/routes/dashboard.sales.tsx
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
import styles from "~/styles/sales.css?url";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: styles },
];

给定这些路由,下表显示哪些 CSS 将应用于特定的 URL:

URLStylesheets
/dashboarddashboard.css
/dashboard/accountsdashboard.css
accounts.css
/dashboard/salesdashboard.css
sales.css

这个小功能很微妙,但是它可以消除使用普通样式表设计应用程序样式时遇到的很多困难。

共享组件样式

大型和小型网站通常都有一组在应用程序的其余部分使用的共享组件:按钮、表单元素、布局等。在 Remix 中使用普通样式表时,我们推荐两种方法。

共享样式表

第一种方法非常简单。将它们全部放在 app/root.tsx 中包含的 shared.css 文件中。这使得组件本身可以轻松共享 CSS 代码(并且您的编辑器可以为 自定义属性 等内容提供智能感知),并且每个组件在 JavaScript 中已经需要一个唯一的模块名称,因此您可以将样式范围限定为唯一的类名或数据属性:

app/styles/shared.css
/* scope with class names */
.PrimaryButton {
/* ... */
}
.TileGrid {
/* ... */
}
/* or scope with data attributes to avoid concatenating
className props, but it's really up to you */
[data-primary-button] {
/* ... */
}
[data-tile-grid] {
/* ... */
}

虽然这个文件可能会变得很大,但它将位于一个由应用程序中的所有路由共享的单个 URL 上。

这也使得路由可以轻松调整组件的样式,而无需向该组件的 API 添加官方新变体。您知道它不会影响除 /accounts 路由之外的任何地方的组件。

app/styles/accounts.css
.PrimaryButton {
background: blue;
}

表面样式

第二种方法是每个组件编写单独的 CSS 文件,然后将样式显示到使用它们的路由上。

也许你在 app/components/button/index.tsx 中有一个 <Button>,其样式在 app/components/button/styles.css,以及一个扩展它的 <PrimaryButton>

请注意,这些不是路由,但它们会导出 links 函数,就像是路由一样。我们将利用这一点将它们的样式展示给使用它们的路由。

app/components/button/styles.css
[data-button] {
border: solid 1px;
background: white;
color: #454545;
}
app/components/button/index.tsx
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
import styles from "./styles.css?url";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: styles },
];
export const Button = React.forwardRef(
({ children, ...props }, ref) => {
return <button {...props} ref={ref} data-button />;
}
);
Button.displayName = "Button";

然后是一个扩展它的 <PrimaryButton>

app/components/primary-button/styles.css
[data-primary-button] {
background: blue;
color: white;
}
app/components/primary-button/index.tsx
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
import { Button, links as buttonLinks } from "../button";
import styles from "./styles.css?url";
export const links: LinksFunction = () => [
...buttonLinks(),
{ rel: "stylesheet", href: styles },
];
export const PrimaryButton = React.forwardRef(
({ children, ...props }, ref) => {
return (
<Button {...props} ref={ref} data-primary-button />
);
}
);
PrimaryButton.displayName = "PrimaryButton";

请注意,主按钮的链接包含基本按钮的链接。这样,<PrimaryButton> 的使用者就不需要知道它的依赖项(就像 JavaScript 导入一样)。

由于这些按钮不是路由,因此不与 URL 段关联,Remix 不知道何时预取、加载或卸载样式。我们需要将链接显示到使用这些组件的路由。

假设 app/routes/_index.tsx 使用主按钮组件:

app/routes/_index.tsx
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
import {
PrimaryButton,
links as primaryButtonLinks,
} from "~/components/primary-button";
import styles from "~/styles/index.css?url";
export const links: LinksFunction = () => [
...primaryButtonLinks(),
{ rel: "stylesheet", href: styles },
];

现在 Remix 可以预取、加载和卸载 button.cssprimary-button.css 和路由的 index.css 的样式。

对此的第一反应是,路由必须知道比你希望它们知道的更多。请记住,每个组件都必须已经导入,因此它不会引入新的依赖项,只是一些获取资产的样板。例如,考虑这样的产品类别页面:

// app/routes/$category.tsx
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
import { AddFavoriteButton } from "~/components/add-favorite-button";
import { ProductDetails } from "~/components/product-details";
import { ProductTile } from "~/components/product-tile";
import { TileGrid } from "~/components/tile-grid";
import styles from "~/styles/$category.css?url";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: styles },
];
export default function Category() {
const products = useLoaderData<typeof loader>();
return (
<TileGrid>
{products.map((product) => (
<ProductTile key={product.id}>
<ProductDetails product={product} />
<AddFavoriteButton id={product.id} />
</ProductTile>
))}
</TileGrid>
);
}

组件导入已经存在,我们只需要显示资产:

// app/routes/$category.tsx
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
import {
AddFavoriteButton,
links as addFavoriteLinks,
} from "~/components/add-favorite-button";
import {
ProductDetails,
links as productDetailsLinks,
} from "~/components/product-details";
import {
ProductTile,
links as productTileLinks,
} from "~/components/product-tile";
import {
TileGrid,
links as tileGridLinks,
} from "~/components/tile-grid";
import styles from "~/styles/$category.css?url";
export const links: LinksFunction = () => {
return [
...tileGridLinks(),
...productTileLinks(),
...productDetailsLinks(),
...addFavoriteLinks(),
{ rel: "stylesheet", href: styles },
];
};
// ...

虽然这有点陈词滥调,但它可以实现很多功能:

  • 您可以控制网络选项卡,并且 CSS 依赖关系在代码中清晰可见
  • 样式与您的组件共置
  • 唯一加载的 CSS 是当前页面上使用的 CSS
  • 当您的组件未被路由使用时,其 CSS 将从页面中卸载
  • Remix 将使用 <Link prefetch> 预取下一页的 CSS
  • 当一个组件的样式发生变化时,其他组件的浏览器和 CDN 缓存不会中断,因为它们都有自己的 URL。
  • 当组件的 JavaScript 发生变化但其样式不变时,样式的缓存不会中断

资源预加载

由于这些只是 <link> 标签,因此您可以执行比样式表链接更多的操作,例如为元素的 SVG 图标背景添加资产预加载:

app/components/copy-to-clipboard.css
[data-copy-to-clipboard] {
background: url("/icons/clipboard.svg");
}
app/components/copy-to-clipboard.tsx
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno
import styles from "./styles.css?url";
export const links: LinksFunction = () => [
{
rel: "preload",
href: "/icons/clipboard.svg",
as: "image",
type: "image/svg+xml",
},
{ rel: "stylesheet", href: styles },
];
export const CopyToClipboard = React.forwardRef(
({ children, ...props }, ref) => {
return (
<Button {...props} ref={ref} data-copy-to-clipboard />
);
}
);
CopyToClipboard.displayName = "CopyToClipboard";

这不仅会使资产在网络选项卡中具有高优先级,而且当您使用 <Link prefetch> 链接到页面时,Remix 会将该 预加载 转变为 预取,因此 SVG 背景会与下一个路由的数据、模块、样式表和任何其他预加载同时进行预取。

链接媒体查询

使用普通样式表和 <link> 标签还可以减少用户浏览器在绘制屏幕时必须处理的 CSS 数量。Link 标签支持 media,因此您可以执行以下操作:

export const links: LinksFunction = () => {
return [
{
rel: "stylesheet",
href: mainStyles,
},
{
rel: "stylesheet",
href: largeStyles,
media: "(min-width: 1024px)",
},
{
rel: "stylesheet",
href: xlStyles,
media: "(min-width: 1280px)",
},
{
rel: "stylesheet",
href: darkStyles,
media: "(prefers-color-scheme: dark)",
},
];
};