React Router v7 已發布。 查看文件
一般 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

網址 樣式表
/dashboard dashboard.css
/dashboard/accounts dashboard.css
accounts.css
/dashboard/sales dashboard.css
sales.css

這很微妙,但是這個小功能消除了使用一般樣式表來設定應用程式樣式時的許多困難。

共用元件樣式

無論大小的網站通常都有一組在整個應用程式中使用的共用元件:按鈕、表單元素、版面配置等。在 Remix 中使用一般樣式表時,我們建議兩種方法。

共用樣式表

第一種方法非常簡單。將它們全部放入 app/root.tsx 中包含的 shared.css 檔案中。這使元件本身可以輕鬆共用 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 背景會與下一路由的資料、模組、樣式表和任何其他預先載入平行預先載入。

使用純樣式表和 <link> 標籤也開啟了減少使用者瀏覽器在繪製螢幕時必須處理的 CSS 量的能力。連結標籤支援 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