Next.js中的全局与局部样式设计介绍及示例

1,367 阅读16分钟

我在使用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也会因为造成额外的渲染和对全局状态的依赖而影响性能。

此外,使用Next.js这样的库的全部意义在于尽可能地静态渲染资产,所以编写需要在浏览器中运行的JS来生成CSS就没有那么多意义了。

在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设置中,将所有全局共享的值存储在变量中是有明显好处的。如果客户要求改变颜色,实现改变只需一句话,而不是大量的查找和替换。因此,我们Next.js CSS设置的一个关键部分是将所有网站的值存储为_设计_标记。

我们将使用内置的CSS自定义属性来存储这些标记(如果你不熟悉这种语法,你可以查看《CSS自定义属性策略指南》)。我应该提到,(在一些项目中)我选择使用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> 标签。(我喜欢把这些样式看作是 "合理的全局默认值"。)我们可以在特定情况下覆盖它们,但它们是一个很好的猜测,说明如果我们不这样做,我们会想要什么。

我将把这些放在_style/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文件_style/utilities.css_并添加:

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

然后让我们在我们的_pages/_app.jsx_中添加import'../styles/utilities.css' 。最后,让我们把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>
}

然后,我们将需要一些茶叶来显示。我们将包括每一种茶的名称、描述和图片(在公共/目录中):

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

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

接下来,我们需要定义一个组件来显示我们的茶叶。让我们先做一个components/ 目录(Next.js默认不做这个)。然后,让我们添加一个components/TeaList 目录。对于任何最终需要一个以上文件的组件,我通常会把所有相关文件放在一个文件夹里。这样做可以防止我们的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} /> 元素到我们的商店页面来把它们连接起来。现在,我们的茶叶将显示在一个列表中,但它不会那么漂亮。

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

让我们先为我们的卡片(TeaListLitem 组件)设定样式。现在,在我们的项目中第一次,我们要为一个组件添加特定的样式。让我们创建一个新文件_components/TeaList/TeaListItem.module.css_。

你可能想知道文件扩展名中的模块是什么。这是一个CSS模块。Next.js支持CSS模块,并包括一些关于它们的良好文档。当我们从一个CSS模块中写出一个类名,如.TeaListItem ,它将自动被转化为更像. 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 ,导入样式。样式变量像一个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" 道具从框架中得到position: absolute; ,所以我们可以通过放入一个带有position: relative;的容器来限制尺寸。

让我们在_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> 。现在,我们的卡片看起来不那么糟糕了。

结合全局和局部风格

接下来,我们想让我们的卡片以网格布局显示。在这种情况下,我们正处于本地和全局风格的交界处。我们当然可以直接在TeaList 组件上编码我们的布局。但是,我也可以想象,有一个将列表变成网格布局的实用类,在其他一些地方也会很有用。

让我们在这里采取全局的方法,在我们的_样式/实用程序.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 Withminmax() andmin()"。

由于我们已经全局地写了这个样式,所以不需要任何花哨的东西来给我们的TeaList 组件添加className="grid" 。但是,假设我们想把这个全局样式与一些额外的本地存储结合起来。例如,我们想把更多的 "茶叶美学 "带进来,让其他每张卡片都有一个绿色的背景。我们所需要做的就是制作一个新的_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")}>

现在,我们已经完成了我们的商店页面,并且我们已经使我们的TeaList 组件利用_了_全局和局部样式。

平衡法

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

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

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

随着我们继续建设我们的网站,我们的设计标记和实用类的列表将会增加。任何不适合作为实用类的样式,我们可以使用CSS模块添加到组件样式中。因此,我们可以在局部和全局样式之间找到一个持续的平衡。我们还可以生成高性能的、直观的CSS代码,与我们的Next.js网站一起自然成长。