Next.js 中的全局与本地样式

已发表: 2022-03-10
快速总结 ↬ Next.js 对如何组织 JavaScript 而不是 CSS 有强烈的意见。 我们如何开发鼓励最佳 CSS 实践的模式,同时遵循框架的逻辑? 答案非常简单——编写结构良好的 CSS 以平衡全局和局部样式问题。

我在使用 Next.js 管理复杂的前端项目方面拥有丰富的经验。 Next.js 对如何组织 JavaScript 代码有意见,但它没有关于如何组织 CSS 的内置意见。

在框架内工作后,我发现了一系列组织模式,我认为它们既符合 Next.js 的指导理念,又能实践最佳 CSS 实践。 在本文中,我们将一起构建一个网站(一家茶店!)来演示这些模式。

注意您可能不需要以前的 Next.js 经验,尽管对 React 有基本的了解并乐于学习一些新的 CSS 技术会很好。

编写“老式”CSS

在第一次查看 Next.js 时,我们可能会考虑使用某种 CSS-in-JS 库。 尽管根据项目的不同可能会有好处,但 CSS-in-JS 引入了许多技术考虑。 它需要使用一个新的外部库,这会增加包的大小。 CSS-in-JS 还可以通过导致额外的渲染和对全局状态的依赖来产生性能影响。

推荐阅读Aggelos Arvanitakis 的“现代 CSS-in-JS 库在 React 应用程序中的看不见的性能成本”

此外,使用 Next.js 之类的库的全部意义在于尽可能静态渲染资源,因此编写需要在浏览器中运行以生成 CSS 的 JS 并没有多大意义。

在 Next.js 中组织样式时,我们必须考虑几个问题:

我们如何适应框架的约定/最佳实践?

我们如何平衡“全局”样式问题(字体、颜色、主要布局等)与“本地”样式问题(关于单个组件的样式)?

对于第一个问题,我想出的答案是简单地编写好的老式 CSS 。 Next.js 不仅无需额外设置就支持这样做; 它还产生高性能和静态的结果。

为了解决第二个问题,我采取的方法可以概括为四部分:

  1. 设计令牌
  2. 全局样式
  3. 实用程序类
  4. 组件样式

我在这里感谢 Andy Bell 的CUBE CSS (“Composition, Utility, Block, Exception”)理念。 如果您以前没有听说过这种组织原则,我建议您查看其官方网站或 Smashing Podcast 上的功能。 我们将从 CUBE CSS 中获得的原则之一是我们应该接受而不是害怕 CSS 级联的想法。 让我们通过将它们应用于网站项目来学习这些技术。

入门

我们将建立一家茶叶店,因为,嗯,茶很好吃。 我们将首先运行yarn create next-app来创建一个新的 Next.js 项目。 然后,我们将删除styles/ directory中的所有内容(都是示例代码)。

注意如果你想跟随完成的项目,你可以在这里查看。

设计代币

在几乎任何 CSS 设置中,将所有全局共享值存储在 variables 中都有明显的好处。 如果客户要求更改颜色,则实施更改是单行的,而不是大量的查找和替换混乱。 因此,我们 Next.js CSS 设置的一个关键部分是将所有站点范围的值存储为设计标记

我们将使用内置的 CSS 自定义属性来存储这些标记。 (如果你不熟悉这种语法,你可以查看“A Strategy Guide To CSS Custom Properties”。)我应该提到(在一些项目中)我选择使用 SASS/SCSS 变量来达到这个目的。 我没有发现任何真正的优势,所以如果我发现我需要其他SASS 功能(混合、迭代、导入文件等),我通常只会在项目中包含 SASS。 相比之下,CSS 自定义属性也可以与级联一起使用,并且可以随时间更改而不是静态编译。 所以,今天,让我们坚持使用纯 CSS

在我们的styles/目录中,让我们创建一个新的design_tokens.css文件:

 :root { --green: #3FE79E; --dark: #0F0235; --off-white: #F5F5F3; --space-sm: 0.5rem; --space-md: 1rem; --space-lg: 1.5rem; --font-size-sm: 0.5rem; --font-size-md: 1rem; --font-size-lg: 2rem; }

当然,这个列表可以而且会随着时间的推移而增长。 添加此文件后,我们需要跳转到pages/_app.jsx文件,这是我们所有页面的主要布局,并添加:

 import '../styles/design_tokens.css'

我喜欢将设计令牌视为在整个项目中保持一致性的粘合剂。 我们将在全球范围内以及在单个组件内引用这些变量,以确保统一的设计语言。

跳跃后更多! 继续往下看↓

全局样式

接下来,让我们在我们的网站上添加一个页面! 让我们进入pages/index.jsx文件(这是我们的主页)。 我们将删除所有样板并添加如下内容:

 export default function Home() { return <main> <h1>Soothing Teas</h1> <p>Welcome to our wonderful tea shop.</p> <p>We have been open since 1987 and serve customers with hand-picked oolong teas.</p> </main> }

不幸的是,它看起来很简单,所以让我们为基本元素设置一些全局样式,例如<h1>标签。 (我喜欢将这些样式视为“合理的全局默认值”。)我们可能会在特定情况下覆盖它们,但它们是一个很好的猜测,如果我们不这样做,我们会想要什么。

我将把它放在styles/globals.css文件中(默认来自 Next.js):

 *, *::before, *::after { box-sizing: border-box; } body { color: var(--off-white); background-color: var(--dark); } h1 { color: var(--green); font-size: var(--font-size-lg); } p { font-size: var(--font-size-md); } p, article, section { line-height: 1.5; } :focus { outline: 0.15rem dashed var(--off-white); outline-offset: 0.25rem; } main:focus { outline: none; } img { max-width: 100%; }

当然,这个版本是相当基本的,但我的globals.css文件通常最终实际上不需要变得太大。 在这里,我设计了基本的 HTML 元素(标题、正文、链接等)。 无需将这些元素包装在 React 组件中,也无需为了提供基本样式而不断添加类。

我还包括默认浏览器样式的任何重置。 有时,我会使用一些站点范围的布局样式来提供“粘性页脚”,例如,但它们仅在所有页面共享相同布局时才属于这里。 否则,它需要在单个组件内进行限定。

我总是包含某种:focus样式,以便在聚焦时为键盘用户清楚地指示交互元素。 最好让它成为网站设计 DNA 中不可或缺的一部分!

现在,我们的网站开始成型:

正在进行中的网站的图片。页面背景现在是深蓝色,标题“舒缓茶”是绿色。该网站没有布局/间距,因此完全扩展到浏览器窗口的宽度。
正在进行中的网站的图片。 页面背景现在是深蓝色,标题“舒缓茶”是绿色。 该网站没有布局/间距,因此完全扩展到浏览器窗口的宽度。 (大预览)

实用程序类

我们的主页肯定可以改进的一个方面是文本当前总是延伸到屏幕的两侧,所以让我们限制它的宽度。 我们在这个页面上需要这个布局,但我想我们可能在其他页面上也需要它。 这是实用程序类的一个很好的用例!

我尝试谨慎地使用实用程序类,而不是仅仅作为编写 CSS 的替代品。 我个人对何时向项目添加一个有意义的标准是:

  1. 我反复需要它;
  2. 它做好一件事;
  3. 它适用于一系列不同的组件或页面。

我认为这个案例符合所有三个条件,所以让我们创建一个新的 CSS 文件styles/utilities.css并添加:

 .lockup { max-width: 90ch; margin: 0 auto; }

然后让我们将 import '../styles/utilities.css'添加到我们的pages/_app.jsx中。 最后,让我们将 pages/index.jsx 中的<main>标签更改为<main className="lockup">

现在,我们的页面更​​加紧密。 因为我们使用了max-width属性,所以我们不需要任何媒体查询来使我们的布局具有移动响应性。 而且,因为我们使用了ch度量单位——它大约相当于一个字符的宽度——我们的大小是动态的,与用户的浏览器字体大小有关。

和以前一样的网站,但现在文本被夹在中间并且不会太宽
和以前一样的网站,但现在文本被夹在中间并且不会太宽。 (大预览)

随着我们网站的发展,我们可以继续添加更多实用程序类。 我在这里采取了一种相当实用的方法:如果我正在工作并且发现我需要另一个类来获得颜色或其他东西,我会添加它。 我不会在阳光下添加所有可能的类——它会膨胀 CSS 文件大小并使我的代码混乱。 有时,在较大的项目中,我喜欢将内容分解为带有几个不同文件的styles/utilities/目录; 这取决于项目的需要。

我们可以将实用程序类视为全球共享的通用、重复样式命令的工具包。 它们有助于防止我们在不同组件之间不断重写相同的 CSS。

组件样式

目前我们已经完成了我们的主页,但我们仍然需要建立我们网站的一部分:在线商店。 我们的目标是显示我们想要销售的所有茶的卡片网格,因此我们需要向我们的网站添加一些组件。

让我们首先在pages/shop.jsx添加一个新页面:

 export default function Shop() { return <main> <div className="lockup"> <h1>Shop Our Teas</h1> </div> </main> }

然后,我们需要一些茶来展示。 我们将为每种茶添加名称、描述和图像(在 public/ 目录中):

 const teas = [ { name: "Oolong", description: "A partially fermented tea.", image: "/oolong.jpg" }, // ... ]

注意这不是一篇关于数据获取的文章,所以我们采取了简单的方法,并在文件的开头定义了一个数组。

接下来,我们需要定义一个组件来显示我们的茶。 让我们从创建一个components/目录开始(Next.js 默认不创建)。 然后,让我们添加一个components/TeaList目录。 对于任何最终需要多个文件的组件,我通常将所有相关文件放在一个文件夹中。 这样做可以防止我们的components/文件夹无法导航。

现在,让我们添加我们的components/TeaList/TeaList.jsx文件:

 import TeaListItem from './TeaListItem' const TeaList = (props) => { const { teas } = props return <ul role="list"> {teas.map(tea => <TeaListItem tea={tea} key={tea.name} />)} </ul> } export default TeaList

这个组件的目的是遍历我们的茶​​并为每个茶显示一个列表项,所以现在让我们定义我们的components/TeaList/TeaListItem.jsx组件:

 import Image from 'next/image' const TeaListItem = (props) => { const { tea } = props return <li> <div> <Image src={tea.image} alt="" objectFit="cover" objectPosition="center" layout="fill" /> </div> <div> <h2>{tea.name}</h2> <p>{tea.description}</p> </div> </li> } export default TeaListItem

请注意,我们使用的是 Next.js 的内置图像组件。 我将alt属性设置为空字符串,因为在这种情况下图像纯粹是装饰性的; 我们希望避免屏幕阅读器用户在此处被冗长的图像描述所困扰。

最后,让我们制作一个components/TeaList/index.js文件,这样我们的组件就可以很容易地从外部导入:

 import TeaList from './TeaList' import TeaListItem from './TeaListItem' export { TeaListItem } export default TeaList

然后,让我们通过将 import TeaList from ../components/TeaList<TeaList teas={teas} />元素添加到我们的 Shop 页面来将它们连接在一起。 现在,我们的茶会出现在列表中,但不会那么漂亮。

通过 CSS 模块将样式与组件放在一起

让我们从设置卡片样式开始( TeaListLitem组件)。 现在,在我们的项目中,我们第一次想要添加特定于一个组件的样式。 让我们创建一个新文件components/TeaList/TeaListItem.module.css

您可能想知道文件扩展名中的模块。 这是一个CSS 模块。 Next.js 支持 CSS 模块并包含一些关于它们的优秀文档。 当我们从 CSS 模块(例如.TeaListItem )中编写类名时,它会自动转换为更像 .TeaListItem 的名称. TeaListItem_TeaListItem__TFOk_ . TeaListItem_TeaListItem__TFOk_加上一堆额外的字符。 因此,我们可以使用我们想要的任何类名,而不必担心它会与我们站点中其他地方的其他类名冲突。

CSS 模块的另一个优点是性能。 Next.js 包含一个动态导入功能。 next/dynamic 允许我们延迟加载组件,以便它们的代码仅在需要时加载,而不是增加整个包的大小。 如果我们将必要的本地样式导入到单个组件中,那么用户也可以为动态导入的组件延迟加载 CSS 。 对于大型项目,我们可能会选择延迟加载大量代码,并且只预先加载最必要的 JS/CSS。 因此,我通常会为每个需要本地样式的新组件创建一个新的 CSS 模块文件。

让我们首先在我们的文件中添加一些初始样式:

 .TeaListItem { display: flex; flex-direction: column; gap: var(--space-sm); background-color: var(--color, var(--off-white)); color: var(--dark); border-radius: 3px; box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); }

然后,我们可以在TeaListitem组件中从./TeaListItem.module.css导入样式。 style 变量像 JavaScript 对象一样进来,所以我们可以访问这个类style.TeaListItem.

注意我们的类名不需要大写。 我发现模块内部(以及外部的小写)类名的大写约定在视觉上区分了本地类名和全局类名。

因此,让我们使用新的本地类并将其分配给TeaListItem组件中的<li>

 <li className={style.TeaListComponent}>

您可能想知道背景颜色线(即var(--color, var(--off-white)); )。 这个片段的意思是默认情况下背景将是我们的--off-white值。 但是,如果我们在卡片上设置--color自定义属性,它将覆盖并选择该值。

起初,我们希望所有卡片都是--off-white ,但我们可能希望稍后更改个别卡片的值。 这与 React 中的 props 非常相似。 我们可以设置一个默认值,但创建一个插槽,我们可以在特定情况下选择其他值。 因此,我鼓励我们考虑 CSS 自定义属性,例如 CSS 的 props 版本

样式仍然看起来不太好,因为我们要确保图像保留在它们的容器中。 Next.js 的带有layout="fill"属性的 Image 组件获取position: absolute; 从框架中,所以我们可以通过放置一个容器来限制大小:相对;。

让我们在TeaListItem.module.css中添加一个新类:

 .ImageContainer { position: relative; width: 100%; height: 10em; overflow: hidden; }

然后让我们在包含我们的<Image><div>上添加className={styles.ImageContainer} 。 我使用相对“简单”的名称,例如ImageContainer ,因为我们在 CSS 模块中,所以我们不必担心与外部样式冲突。

最后,我们想在文本的两侧添加一些填充,所以让我们添加最后一个类并依赖我们设置为设计标记的间距变量:

 .Title { padding-left: var(--space-sm); padding-right: var(--space-sm); }

我们可以将这个类添加到包含我们的名称和描述的<div>中。 现在,我们的卡片看起来还不错:

卡片显示了作为种子数据添加的 3 种不同的茶。它们有图像、名称和描述。它们目前显示在垂直列表中,它们之间没有空格。
卡片显示了作为种子数据添加的 3 种不同的茶。 它们有图像、名称和描述。 它们目前显示在垂直列表中,它们之间没有空格。 (大预览)

结合全球和本地风格

接下来,我们希望我们的卡片以网格布局显示。 在这种情况下,我们只是处于局部样式和全局样式之间的边界。 我们当然可以直接在TeaList组件上编写我们的布局。 但是,我也可以想象,拥有一个将列表转换为网格布局的实用程序类在其他几个地方可能很有用。

让我们在这里采用全局方法并在我们的styles/utilities.css中添加一个新的实用程序类:

 .grid { list-style: none; display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--min-item-width, 30ch), 1fr)); gap: var(--space-md); }

现在,我们可以在任何列表中添加.grid类,我们将获得一个自动响应的网格布局。 我们还可以更改--min-item-width自定义属性(默认30ch )来更改每个元素的最小宽度。

注意记住要考虑像道具这样的自定义属性! 如果这个语法看起来不熟悉,你可以查看 Chris Coyier 的“Intrinsically Responsive CSS Grid With minmax() And min() ”。

由于我们已经在全局范围内编写了这种样式,因此不需要任何花哨的操作就可以将className="grid"添加到我们的TeaList组件中。 但是,假设我们想将这种全球风格与一些额外的本地商店结合起来。 例如,我们希望在其中加入更多的“茶美学”,并让其他每张卡片都有绿色背景。 我们需要做的就是创建一个新的components/TeaList/TeaList.module.css文件:

 .TeaList > :nth-child(even) { --color: var(--green); }

还记得我们是如何在TeaListItem组件上创建--color custom属性的吗? 好了,现在我们可以根据具体情况进行设置了。 请注意,我们仍然可以在 CSS 模块中使用子选择器,并且我们选择在不同模块中设置样式的元素并不重要。 因此,我们也可以使用本地组件样式来影响子组件。 这是一个特性而不是一个错误,因为它允许我们利用 CSS 级联! 如果我们尝试以其他方式复制这种效果,我们最终可能会得到某种 JavaScript 汤而不是三行 CSS。

那么,我们如何在TeaList组件上保留全局.grid类,同时添加本地.TeaList类? 这就是语法可能变得有点古怪的地方,因为我们必须通过执行类似style.TeaList之类的操作来访问 CSS 模块之外的.TeaList类。

一种选择是使用字符串插值来获得类似:

 <ul role="list" className={`${style.TeaList} grid`}>

在这种小情况下,这可能就足够了。 如果我们混合和匹配更多的类,我发现这种语法让我的大脑有点爆炸,所以我有时会选择使用类名库。 在这种情况下,我们最终得到一个看起来更合理的列表:

 <ul role="list" className={classnames(style.TeaList, "grid")}>

现在,我们已经完成了 Shop 页面,并且我们已经让TeaList组件同时利用全局和本地样式。

我们的茶卡现在以网格形式显示。偶数项为绿色,奇数项为白色。
我们的茶卡现在以网格形式显示。 偶数项为绿色,奇数项为白色。 (大预览)

平衡法

我们现在已经建立了我们的茶馆,只使用纯 CSS 来处理样式。 您可能已经注意到,我们不必花费很长时间来处理自定义 Webpack 设置、安装外部库等等。 这是因为我们使用 Next.js 开箱即用的模式。 此外,它们鼓励最佳 CSS 实践并自然地融入 Next.js 框架架构。

我们的 CSS 组织由四个关键部分组成:

  1. 设计代币,
  2. 全球风格,
  3. 实用程序类,
  4. 组件样式。

随着我们继续构建我们的网站,我们的设计令牌和实用程序类列表将会增加。 任何添加为实用程序类没有意义的样式,我们可以使用 CSS 模块添加到组件样式中。 因此,我们可以在局部和全局样式问题之间找到持续的平衡。 我们还可以生成与 Next.js 站点一起自然增长的高性能、直观的 C​​SS 代码