常规 CSS

常规 CSS

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

CSS 维护问题可能由于以下几个原因潜入 Web 应用程序中。它可能难以知道

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

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

如果您使用的是 经典 Remix 编译器 而不是 Remix Vite,则应从 CSS 导入路径的末尾删除 ?url

路由样式

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

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 },
];
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 },
];
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

URL 样式表
/dashboard dashboard.css
/dashboard/accounts dashboard.css
accounts.css
/dashboard/sales dashboard.css
sales.css

这很细微,但这个小功能消除了使用普通样式表为您的应用程序设置样式时的许多困难。

共享组件样式

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

共享样式表

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

/* 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 路由之外的任何地方的组件。

.PrimaryButton {
  background: blue;
}

呈现样式

第二种方法是为每个组件编写单独的 css 文件,然后将样式“呈现”到使用它们的路由。

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

请注意,这些不是路由,但它们导出 links 函数,就像它们是路由一样。我们将使用此功能将它们的样式呈现到使用它们的路由。

[data-button] {
  border: solid 1px;
  background: white;
  color: #454545;
}
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>

[data-primary-button] {
  background: blue;
  color: white;
}
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";

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

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

假设 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 的样式。

最初的反应可能是路由需要知道比你想要的更多信息。请记住,每个组件都必须已经导入,所以它不会引入新的依赖项,只是为了获取资源而添加了一些样板代码。例如,考虑一个像这样的产品类别页面

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>
  );
}

组件导入已经存在,我们只需要显示资源即可

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 图标背景添加资源预加载

[data-copy-to-clipboard] {
  background: url("/icons/clipboard.svg");
}
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 会将该 preload 转换为 prefetch,因此 SVG 背景会与下一页的 data、模块、样式表和任何其他预加载内容并行预取。

使用普通样式表和 <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)",
    },
  ];
};
文档和示例根据以下许可获得授权 MIT