Remix 帮助您使用嵌套路由和 links
来扩展使用常规 CSS 的应用程序。
CSS 维护问题可能由于以下几个原因潜入 Web 应用程序中。它可能难以知道
Remix 通过基于路由的样式表缓解了这些问题。嵌套路由可以分别向页面添加自己的样式表,Remix 将自动预取、加载和卸载它们以及路由。当关注范围仅限于活动路由时,这些问题的风险会大大降低。唯一可能发生冲突的机会是父路由的样式(即使那样,您也可能会看到冲突,因为父路由也在渲染)。
?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.css
、primary-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 },
];
};
// ...
虽然这有点样板代码,但它能够实现很多功能
<Link prefetch>
预取下一页的 CSS。由于这些只是 <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)",
},
];
};