常规 CSS

常规 CSS

¥Regular CSS

Remix 帮助你扩展使用嵌套路由和 links 的常规 CSS 的应用。

¥Remix helps you scale an app with regular CSS with nested routes and links.

CSS 维护问题可能会由于多种原因潜入 Web 应用。了解以下几点可能会比较困难:

¥CSS Maintenance issues can creep into a web app for a few reasons. It can get challenging to know:

  • 如何以及何时加载 CSS,因此通常每个页面上都会加载 CSS。

    ¥how and when to load CSS, so it was usually all loaded on every page

  • 如果你使用的类名和选择器意外地影响了应用中其他 UI 的样式

    ¥if the class names and selectors you were using were accidentally styling other UI in the app

  • 如果随着 CSS 源代码的不断增长,某些规则甚至不再使用

    ¥if some rules weren't even used anymore as the CSS source code grew over time

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

¥Remix alleviates these issues with route-based stylesheets. Nested routes can each add their own stylesheets to the page, and Remix will automatically prefetch, load, and unload them with the route. When the scope of concern is limited to just the active routes, the risks of these problems are reduced significantly. The only chances for conflicts are with the parent routes' styles (and even then, you will likely see the conflict since the parent route is also rendering).

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

路由样式

¥Route Styles

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

¥Each route can add style links to the page, for example:

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:

¥Given these routes, this table shows which CSS will apply at specific URLs:

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

虽然它很微妙,但这个小功能在使用普通样式表设计应用样式时,可以大大减少难度。

¥It's subtle, but this little feature removes a lot of the difficulty when styling your app with plain stylesheets.

共享组件样式

¥Shared Component Styles

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

¥Websites large and small usually have a set of shared components used throughout the rest of the app: buttons, form elements, layouts, etc. When using plain style sheets in Remix, there are two approaches we recommend.

共享样式表

¥Shared stylesheet

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

¥The first approach is basic. Put them all in a shared.css file included in app/root.tsx. That makes it easy for the components themselves to share CSS code (and your editor to provide intellisense for things like custom properties). Each component already needs a unique module name in JavaScript anyway, so you can scope the styles to a unique class name or data attribute:

/* 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 中,并由应用中的所有路由共享。

¥While this file may become large, it'll be at a single URL that will be shared by all routes in the app.

这也使得路由可以轻松调整组件的样式,而无需为该组件的 API 添加新的官方变体。你知道这不会影响组件的任何部分,除了 /accounts 路由。

¥This also makes it easy for routes to adjust the styles of a component without needing to add an official new variant to the API of that component. You know it won't affect the component anywhere but the /accounts routes.

.PrimaryButton {
  background: blue;
}

显示样式

¥Surfacing Styles

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

¥A second approach is to write individual CSS files per component and then "surface" the styles up to the routes that use them.

也许你在 app/components/button/index.tsx 中有一个 <Button>,其样式位于 app/components/button/styles.css,并且还有一个扩展了它的 <PrimaryButton>

¥Perhaps you have a <Button> in app/components/button/index.tsx with styles at app/components/button/styles.css as well as a <PrimaryButton> that extends it.

请注意,这些不是路由,但它们会像路由一样导出 links 函数。我们将使用它来将它们的样式渲染给使用它们的路由。

¥Note that these are not routes, but they export links functions as if they were. We'll use this to surface their styles to the routes that use them.

[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>

¥And then a <PrimaryButton> that extends it:

[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 导入一样)。

¥Note that the primary button's links include the base button's links. This way consumers of <PrimaryButton> don't need to know its dependencies (just like JavaScript imports).

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

¥Because these buttons are not routes, and therefore not associated with a URL segment, Remix doesn't know when to prefetch, load, or unload the styles. We need to "surface" the links up to the routes that use the components.

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

¥Consider that app/routes/_index.tsx uses the primary button component:

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

¥Now Remix can prefetch, load, and unload the styles for button.css, primary-button.css, and the route's index.css.

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

¥An initial reaction to this is that routes have to know more than you want them to. Keep in mind that each component must be imported already, so it's not introducing a new dependency, just some boilerplate to get the assets. For example, consider a product category page like this:

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

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

¥The component imports are already there, we just need to surface the assets:

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

// ...

虽然这有点老套,但它确实能带来很多好处:

¥While that's a bit of boilerplate, it enables a lot:

  • 你可以控制网络选项卡,并且 CSS 依赖在代码中清晰可见。

    ¥You control your network tab, and CSS dependencies are clear in the code

  • 与组件共置样式

    ¥Co-located styles with your components

  • 唯一加载的 CSS 是当前页面使用的 CSS

    ¥The only CSS ever loaded is the CSS used on the current page

  • 当你的组件未被路由使用时,它们的 CSS 将从页面中卸载。

    ¥When your components aren't used by a route, their CSS is unloaded from the page

  • Remix 会使用 <Link prefetch> 预取下一页的 CSS。

    ¥Remix will prefetch the CSS for the next page with <Link prefetch>

  • 当一个组件的样式发生变化时,其他组件的浏览器和 CDN 缓存不会中断,因为它们都有自己的 URL。

    ¥When one component's styles change, browser and CDN caches for the other components won't break because they all have their own URLs.

  • 当组件的 JavaScript 发生变化但其样式保持不变时,样式的缓存不会中断。

    ¥When a component's JavaScript changes but its styles don't, the cache is not broken for the styles

资源预加载

¥Asset Preloads

由于这些只是 <link> 标签,你可以做比样式表链接更多的事情,例如为元素的 SVG 图标背景添加资源预加载:

¥Since these are just <link> tags, you can do more than stylesheet links, like adding asset preloads for SVG icon backgrounds of your elements:

[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 背景会与下一个路由的数据、模块、样式表和任何其他预加载并行预加载。

¥Not only will this make the asset a high priority in the network tab, but Remix will turn that preload into a prefetch when you link to the page with <Link prefetch>, so the SVG background is prefetched, in parallel, with the next route's data, modules, stylesheets, and any other preloads.

链接媒体查询

¥Link Media Queries

使用纯样式表和 <link> 标签还可以减少用户浏览器在绘制屏幕时需要处理的 CSS 数量。链接标签支持 media,因此你可以执行以下操作:

¥Using plain stylesheets and <link> tags also opens up the ability to decrease the amount of CSS your user's browser has to process when it paints the screen. Link tags support media, so you can do the following:

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)",
    },
  ];
};
Remix v2.17 中文网 - 粤ICP备13048890号