React Router v7 已發布。 查看文件
CSS 檔案

CSS 檔案

在 Remix 中管理 CSS 檔案主要有兩種方式

本指南涵蓋了每種方法的優缺點,並根據您的專案具體需求提供一些建議。

CSS 打包

CSS 打包是在 React 社群中最常見的管理 CSS 檔案的方法。在這種模型中,樣式被視為模組副作用,並由打包工具自行決定將樣式打包成一個或多個 CSS 檔案。它使用起來更簡單,所需的樣板程式碼更少,並給予打包工具更多優化輸出的能力。

例如,假設您有一個基本的 Button 元件,並附加了一些樣式

.Button__root {
  background: blue;
  color: white;
}
import "./Button.css";

export function Button(props) {
  return <button {...props} className="Button__root" />;
}

要使用此元件,您可以簡單地引入它並在您的路由檔案中使用它

import { Button } from "../components/Button";

export default function HelloRoute() {
  return <Button>Hello!</Button>;
}

在取用此元件時,您不必擔心管理個別的 CSS 檔案。CSS 被視為元件的私有實作細節。這是許多元件庫和設計系統中常見的模式,並且可以很好地擴展。

某些 CSS 解決方案需要 CSS 打包

某些管理 CSS 檔案的方法需要使用打包的 CSS。

例如,CSS 模組是建立在假設 CSS 已打包的基礎上。即使您明確地將 CSS 檔案的類別名稱作為 JavaScript 物件引入,樣式本身仍然被視為副作用並自動打包到輸出中。您無法存取 CSS 檔案的底層 URL。

另一個需要 CSS 打包的常見使用案例是當您使用第三方元件庫時,該元件庫將 CSS 檔案作為副作用引入,並依賴您的打包工具為您處理它們,例如 React Spectrum

CSS 順序在開發和生產環境之間可能有所不同

當與 Vite 的隨需編譯方法結合使用時,CSS 打包會帶來顯著的權衡。

使用先前呈現的 Button.css 範例,此 CSS 檔案將在開發期間轉換為以下 JavaScript 程式碼

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 檔案。

Vite 這樣做的目的是為了在引入時可以延遲編譯 CSS,然後在開發期間進行熱重載。一旦引入此檔案,CSS 檔案的內容就會以副作用的形式注入到頁面中。

這種方法的缺點是這些樣式沒有與路由生命週期相關聯。這表示當從路由導覽離開時,樣式不會被卸載,導致在應用程式中導覽時,文件中會累積舊的樣式。這可能會導致 CSS 規則順序在開發和生產環境之間有所不同。

為了緩解這個問題,以一種能夠彈性應對檔案順序變化的方式編寫 CSS 會很有幫助。例如,您可以使用 CSS 模組,以確保 CSS 檔案的作用域僅限於引入它們的檔案。您也應該嘗試限制針對單一元素的 CSS 檔案數量,因為這些檔案的順序無法保證。

打包的 CSS 在開發環境中可能會消失

Vite 在開發期間處理 CSS 打包的另一個顯著權衡是,React 可能會無意中從文件中移除樣式。

當使用 React 渲染整個文件(如同 Remix 所做的那樣)時,如果元素被動態注入到 head 元素中,您可能會遇到問題。如果文件被重新掛載,現有的 head 元素會被移除,並替換為一個全新的元素,這會移除 Vite 在開發期間注入的任何 style 元素。

在 Remix 中,這個問題可能會因為 hydration 錯誤而發生,因為它會導致 React 從頭開始重新渲染整個頁面。hydration 錯誤可能是由您的應用程式程式碼引起的,但也可能是由操縱文件的瀏覽器擴充功能引起的。

這是 React 已知的問題,他們在 canary 版本通道中已修復此問題。如果您了解相關風險,您可以將您的應用程式釘選到特定的 React 版本,然後使用 package overrides 來確保整個專案中只使用這個版本的 React。例如:

{
  "dependencies": {
    "react": "18.3.0-canary-...",
    "react-dom": "18.3.0-canary-..."
  },
  "overrides": {
    "react": "18.3.0-canary-...",
    "react-dom": "18.3.0-canary-..."
  }
}

作為參考,這就是 Next.js 在內部代表您處理 React 版本的方式,因此這種方法比您想像的更為廣泛使用,即使它不是 Remix 作為預設提供的方式。

再次強調,這個由 Vite 注入的樣式問題只會在開發期間發生。生產版本不會有這個問題,因為會產生靜態 CSS 檔案。

CSS URL 引入

管理 CSS 檔案的另一種主要方法是使用 Vite 的明確 URL 引入

Vite 允許您在 CSS 檔案引入時附加 ?url 來取得該檔案的 URL(例如:import href from "./styles.css?url")。然後,這個 URL 可以透過路由模組的 links export 傳遞給 Remix。這將 CSS 檔案與 Remix 的路由生命週期聯繫在一起,確保在應用程式中導航時,樣式會被注入和從文件中移除。

例如,使用先前相同的 Button 元件範例,您可以將 links 陣列與元件一起匯出,以便使用者可以存取其樣式。

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 輸出中。

import {
  Button,
  links as buttonLinks,
} from "../components/Button";

export const links = () => [...buttonLinks];

export default function HelloRoute() {
  return <Button>Hello!</Button>;
}

這種方法在規則排序方面更具可預測性,因為它可以讓您精細控制每個檔案,並在開發和生產之間提供一致的行為。與開發期間的捆綁 CSS 不同,當不再需要時,樣式會從文件中移除。如果頁面的 head 元素被重新掛載,您的路由定義的任何 link 標籤也會被重新掛載,因為它們是 React 生命週期的一部分。

這種方法的缺點是它可能導致很多樣板程式碼。

如果您有很多可重複使用的元件,每個元件都有自己的 CSS 檔案,您需要手動將每個元件的所有 links 向上傳遞到您的路由元件,這可能需要將 CSS URL 通過多個層級的元件傳遞。這也容易出錯,因為很容易忘記導入元件的 links 陣列。

儘管有其優點,您可能會覺得與 CSS 捆綁相比,這種方法太過繁瑣,或者您可能會覺得額外的樣板程式碼是值得的。這沒有對錯之分。

結論

在您的 Remix 應用程式中管理 CSS 檔案時,最終還是個人偏好,但這裡有一個很好的經驗法則:

  • 如果您的專案只有少量的 CSS 檔案(例如,在使用 Tailwind 時,您可能只有一個 CSS 檔案),您應該使用 CSS URL 導入。增加的樣板程式碼很少,而且您的開發環境將更接近生產環境。
  • 如果您的專案有大量的 CSS 檔案與較小的可重複使用的元件相關聯,您可能會發現 CSS 捆綁的減少樣板程式碼更符合人體工學。只需注意權衡取捨,並以使 CSS 能夠抵禦檔案排序更改的方式編寫 CSS。
  • 如果您在開發期間遇到樣式消失的問題,您應該考慮使用 React canary 版本,以便 React 在重新掛載頁面時不會移除現有的 head 元素。
文件和範例授權於 MIT