译:如何为 React 应用程序添加样式

原文:https://alexkondov.com/full-stack-tao-styling/
作者:Alexander
译者:ChatGPT 4 Turbo

我们构建网络应用的方式与十年前大不相同。我们不再考虑页面,而是考虑组件。我们不是将数据传递给模板以渲染,我们管理动态状态。我们拥有强大的 API,将过去最难处理的一致性错误转变为简单的逻辑。

但有一点基本保持不变,那就是样式。

我可以立刻区分出一个 2014 年的代码库和一个 2024 年的代码库之间的区别。但如果你给我看样式表,它们看起来都像是昨天刚完成的一样。

虽然这意味着 CSS 已经成熟到不需要大的思维转变,但我们的前端开发实践和我们构建的产品大小已经发生了很大变化。对许多团队而言,样式仍是一个未解决的问题。

我见过很多工程师在实现复杂的状态管理方面毫无困难,但在进行恰当的样式和响应式设计时却遇到了麻烦。前端开发在将逻辑结构和美学结合在一起这一点上是独一无二的,而 CSS 经常被低估,被认为是两者中较容易的部分。

一旦逻辑和组件准备就绪且功能正常,让它们看起来漂亮就简单了,对吧?对吧?

我在 CSS 上挣扎过够多,以至于我更加明白。

简短的旁白

这不仅仅是一篇充满代码示例的文章,这是我正在公开写作的即将出版书籍《Tao of 全栈》的一章。

以下是目前已发布的所有章节:

语义类

我们将功能暂时放在一边,本章剩余部分完全专注于组件的标记和其 CSS。这里有一个渲染文章的简单组件,我们将使其更漂亮。

function Essay({ title, content, author }) {
  return (
    <article>
      <h1>{title}</h1>
      <p>{content}</p>
      <div>
        <img src={author.image} />
        <div>— {author.name}</div>
      </div>
    </article>
  )
}

HTML 规范指出我们的标记应该只包含与内容相关的信息。因此,我们应该使用我们所说的“语义”类使我们的元素可以用 CSS 选择。

function Essay({ title, content, author }) {
  return (
    <article className="essay">
      <h1>{title}</h1>
      <p>{content}</p>
      <div>
        <img src={author.image} />
        <div>— {author.name}</div>
      </div>
    </article>
  )
}

在类的上下文中,语义意味着它应该解释元素内部内容的含义。在上面的例子中,我们使用 essay,因为这是我们的组件正在渲染的内容。

然后,我们可以使用类作为锚点来选择组件内的不同元素。

.essay {
  border: 1px solid black;
  border-radius: 5px;
  padding: 10px 20px;
  margin-bottom: 5px;
}

.essay h1 {
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 15px;
}

.essay p {
  font-size: 16px;
  margin-bottom: 10px;
}

.essay div {
  display: flex;
  align-items: center;
}

.essay div img {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  margin-right: 10px;
}

.essay div div {
  font-size: 14px;
  font-style: italic;
}

为每个元素写出完整的选择器会使我们的 CSS 变得冗长,因此我们应该使用嵌套选择器。最新的浏览器已经采用了它们,并且我们有预处理器供旧版使用。

.essay {
  border: 1px solid black;
  border-radius: 5px;
  padding: 10px 20px;
  margin-bottom: 5px;

  h1 {
    font-size: 24px;
    font-weight: bold;
    margin-bottom: 15px;
  }

  p {
    font-size: 16px;
    margin-bottom: 10px;
  }

  div {
    display: flex;
    align-items: center;

    img {
      width: 50px;
      height: 50px;
      border-radius: 50%;
      margin-right: 10px;
    }

    div {
      font-size: 14px;
      font-style: italic;
    }
  }
}

遵循关注点分离的基本原则,我们的标记应该处理内容,CSS 应该处理样式,并且他们应该被分开。采用这种方法,我们将这一理念实施得相当不错。

但是,让我们来看看组件和它的 CSS 放在一起的情况。它们的结构几乎完全相同。

特别是当样式被嵌套时,我们可以看到我们的 CSS 与 HTML 有多紧密地耦合在一起。如果我们需要用 div 可视化另一块内容,它可能会继承一些根本不相关的样式。

所以,虽然这种按照规则的做法是遵循了原则,但是耦合得太紧了。我们应该给它应用更多的类,以使选择元素变得更容易。

function Essay({ title, content, author }) {
  return (
    <article className="essay">
      <h1 className="title">{title}</h1>
      <p className="content">{content}</p>
      <div>
        <img className="author-image" src={author.image} />
        <div className="author-name">— {author.name}</div>
      </div>
    </article>
  )
}

但是按照语义化类的想法,这些看起来太泛化了。.title.content 类可能会引用我们应用程序内多个元素,并且由于它们与一个核心产品实体相关,作者相关的类也可以在各种地方使用。

function Essay({ title, content, author }) {
  return (
    <article className="essay">
      <h1 className="essay-title">{title}</h1>
      <p className="essay-content">{content}</p>
      <div className="essay-author">
        <img className="essay-author-image" src={author.image} />
        <div className="essay-author-name">— {author.name}</div>
      </div>
    </article>
  )
}

这些好多了。

现在它们与一个实体的特定部分相关联,我们不再需要在 CSS 中引用元素类型。同时我们避免了可能发生的类名冲突,这些冲突可能会搞乱我们的样式。

.essay {
  border: 1px solid black;
  border-radius: 5px;
  padding: 10px 20px;
  margin-bottom: 5px;

  .essay-title {
    font-size: 24px;
    font-weight: bold;
    margin-bottom: 15px;
  }

  .essay-content {
    font-size: 16px;
    margin-bottom: 10px;
  }

  .essay-author {
    display: flex;
    align-items: center;

    .essay-author-image {
      width: 50px;
      height: 50px;
      border-radius: 50%;
      margin-right: 10px;
    }

    .essay-author-name {
      font-size: 14px;
      font-style: italic;
    }
  }
}

我们的 CSS 仍然与标记很相似,但至少我们已经在一定程度上减少了它对确切组件结构的了解,将其责任限制在样式上

创建相似的组件

大多数 Web 应用程序都试图拥有一致的外观和感觉,组件相似也是正常的。实际上,我们经常会发现,如果设计得当,组件在应用程序的其他部分使用时几乎不需要或不需要进行调整。

我们的应用程序需要一个组件来显示引用,我们将在用户等待数据加载时显示此引用。

它需要被包裹在一个盒子里,它应该渲染引用、其作者的图片和他们的名字。它与我们已经有的 Essay 组件非常相似,其样式也应该是相同的。因此,本着不重复自己的精神,我们复制了标记和它的类,瞧!一切看起来都很好。

function Quote({ content, author }) {
  return (
    <article className="essay">
      <p className="essay-content">{content}</p>
      <div className="essay-author">
        <img className="essay-author-image" src={author.image} />
        <div className="essay-author-name">— {author.name}</div>
      </div>
    </article>
  )
}

不过,这种方法有一个问题。

我们新组件中的类不再反映其内容。遵循 HTML 规范,我们知道我们应该编写赋予标记意义的语义类。但由于我们想要重用我们的 CSS,我们唯一的选择就是将类重命名为更通用的名字,以覆盖两种用例。

.text-box {
  border: 1px solid black;
  border-radius: 5px;
  padding: 10px 20px;
  margin-bottom: 5px;

  .text-box-title {
    font-size: 24px;
    font-weight: bold;
    margin-bottom: 15px;
  }

  .text-box-content {
    font-size: 16px;
    margin-bottom: 10px;
  }

  .text-box-author {
    display: flex;
    align-items: center;

    .text-box-author-image {
      width: 50px;
      height: 50px;
      border-radius: 50%;
      margin-right: 10px;
    }

    .text-box-author-name {
      font-size: 14px;
      font-style: italic;
    }
  }
}

我们重用了样式,并且在两种情况下类名依然反映了内容的本质。我们的 CSS 工作开始显现成效。

但是,通过这种方法,CSS 不仅仅与某一个标记区域关联。例如,Quote 组件没有标题,但是因为 Essay 组件的原因,有一个选择器在对它应用样式。

CSS 变得与所有使用它的区域耦合。处理边缘情况变得与管理一个广泛使用的编程抽象化相似。

一个具体的变化

现在,在我们的应用投入生产几周后,我们决定让一些文章突出显示。特定的文章应该有一个黑色背景和白色文字,以吸引注意力。我们现在不关注突出显示的标准或功能,只关注样式。

.highlighted {
  background-color: black;
  color: white;
}

我们可以将类命名为 highlighted,并通过一个简单的条件检查将其添加到我们的组件中。

function Essay({ title, content, author, highlighted }) {
  return (
    <article
      className={`text-box ${highlighted ? 'highlighted' : ''}`}
    >
      ...
    </article>
  )
}

但是,再过了几周,我们决定将突出显示的文章更改为淡紫色,文字应该再次变成黑色。

.highlighted {
  background-color: #d5b8ff;
}

我们测试了我们的更改,它们看起来不错,我们部署了更改。

但是,在我们每次重用类名时,都小心确保它们反映了内容的本质的同时,我们可能有一个同事在他们的工作中不那么勤奋。

他们也知道从代码库中去除重复的重要性,所以,当他们有任务添加一个带有文本和黑色背景的组件时,即使他们的组件的目的与我们的不匹配,他们还是重用了我们的 .text-box.highlighted 类。

我们的类变得太通用了,因为它没有反映它正在样式化的内容的本质,人们决定重用它。

.text-box.text-box-highlited {
  // ...
}

通过这个改变,我们明确了这个类应该在哪里使用。因此,他们可以创建他们自己的组件,并实现用于它的特定语义类,因为它服务于不同的目的。这损害了我们的可重用性,但是用例太不同了。

不过,另一个问题就在拐角处。

更具体的改动

我们需要开发的下一个功能是,我们的 Quote 组件的文本的第一个字母应该大写,类似于书籍章节的第一个字母。

.text-box {
  // ...

  .text-box-content {
    // ...
    &::first-letter {
      text-transform: uppercase;
      font-size: 200%;
      font-weight: bold;
    }
  }
}

但这也会使 Essay 组件的第一个字母大写,这是我们不希望发生的。所以,我们应该将这实现为只在 Quote 组件中添加的一个额外类。

但等等,组件的默认样式应该是什么?

仅仅因为 Essay 组件是第一个使用它的,就应该有正常的首字母大写吗?我的哲学是最简单的风格应该是基础,所以我会留下大写作为一个额外的类,但我们应该如何命名它?

再次回到 HTML 规范,类应该反映元素中的内容,所以我们可以命名它像是 .capitalized-first-letter,但然后我们将遇到我们之前遇到的与 .highlighted 相同的问题。有人可能会将一个特定的类当作一个通用的类来使用,而在不应该使用的地方使用它。

我们可以添加一个像 .quote-content 这样的具体类,但将它与一个通用类绑定打破了关注点分离。

重复

让我们回到我们之前的问题 – 两个组件有许多样式在它们之间重复,但有足够的特殊性来防止我们重用一个类。

.author-image {
  display: flex;
  justify-content: center;
  align-items: center;

  border: 1px solid black;
  border-radius: 50%;
}

.company-image {
  display: flex;
  justify-content: center;
  align-items: center;

  border: 1px solid #d5b8ff;
  border-radius: 5px;
}

我们可以抽取一个类,仅描述重复的行为,因为我们预计会有很多元素需要使用 flexbox 并居中。

??? {
  display: flex;
  justify-content: center;
  align-items: center;
}

.author-image {
  border: 1px solid black;
  border-radius: 50%;
}

.company-image {
  border: 1px solid #d5b8ff;
  border-radius: 5px;
}

但现在,我们的 HTML 和 CSS 之间的耦合程度又增加了。我们的标记将不得不意识到我们的样式决策。关注点的分离再次被打破。

过去,我们使用 SCSS 混入来重用较小的逻辑片段,但请记住,我们做出的每一个决策都是在过度工程和欠工程之间的平衡上。有了如今可用的前端工具,这是不必要的复杂性。

经过多年面对类似问题的处理,我得出结论,可重用的 CSS 有点像是一种误导。

屏幕上有许多元素在具体情况下相似而又不同。是的,关于按钮、输入和低级组件的粒度化类是可重用的,但事物越是具体,重用任何样式就越困难。

样式不足

如果一个元素的类未设计为可重用,您会发现它的一些样式可能来自其父元素,如间距、字体或颜色。这意味着我们可以重用 “一些” CSS,但然后我们必须在我们自己的类中复制其余的。

硬编码的值

所以我接受重复是一种必要的恶。考虑合适的 CSS 架构会给我们没有合适工具来解决的项目增加许多不必要的复杂性。我们实质上是在实现一种继承形式,但没有编写代码时获得的智能提示。

所以我一次又一次地写出了样式 – 边距、字体、颜色等等。

在第一千次做这件事时,我在思考关注点分离的想法时,注意到我已经打破了另一个我们在代码库中已经建立的重要原则。

避免使用神奇的硬编码值。

抽象化样式值

当我看我的样式时,它们不仅重复,而且充满了神奇的值。各种颜色、边距和从 1048 像素的每一个可想象的字体大小,都支撑着 UI。

就像在我们的代码库中,这些数字并不描述它们的用途。

例如,你不知道 font-size: 24px 与当前应用的关系。文本到底有多大?在仪表板中,这可能是一个标题,但在一个粗犷主义着陆页中,这可能是页面上文本的正常大小。

但认知负荷并不是我们唯一的问题。屏幕的一致性和对称性是让产品看起来好的关键,而且拥有一个好看的最终结果对于任何前端应用来说都和它的状态管理同样重要。

通过设计令牌实现一致性

没有一致的间距的 UI 即使人们无法指出问题,也会感觉“不对”。

我们想要提取公共值,不仅是因为我们想要重用它们,还因为我们想要限制首先应该使用哪些。使用适合于一个尺度的字体大小、边距和填充,给 UI 带来了一种对称性和一致性的感觉。

其他所有值也是如此。

颜色是应用的身份。即使你选择了一个极简的色彩调色板,拥有多种灰色变体仍然可能让未经训练的眼睛看起来不好。

例如,一个按钮可能需要其正常、悬停、按下和禁用状态的多种颜色。现在,如果所有这些颜色都是同一基础主色的不同阴影,这个组件看起来会好很多。

设计令牌

我们不能可靠地重用类,但我们可以重用 CSS 值。复用一个复杂元素的 CSS 很难,但它们都可以由同一套“设计令牌”支撑。

设计令牌是代表设计系统最小单位的原子值 – 颜色、字体大小、间距、动画和我们需要重用的其他一切。与同时保留视觉和功能(事物)的组件库相反,设计令牌只携带样式。

他们的目标是在实现组件时,抽象化选择正确值的决策,并帮助我们保持一致性。在现代浏览器中,我们可以使用 CSS 变量来定义这些值。

:root {
  /* 颜色变量 */
  /* HSL 让我们更容易修改亮度 */
  --color-primary: hsl(221, 44%, 41%);
  --color-primary-light: hsl(221, 32%, 65%);
  --color-primary-dark: hsl(211, 50%, 29%);
  /* 但硬编码的十六进制值也很好 */
  --color-grey: #65737e;
  --color-grey-light: #c0c5ce;
  --color-grey-dark: #343d46;
  --color-black: #1a1a1a;
  /* RGBA 也可以 */
  --color-white: rgba(255, 255, 255, 0.9);

  /* 字体家族变量 */
  --font-serif: 'Merriweather', serif;
  --font-sans-serif: 'Montserrat', sans-serif;

  /* 字体大小变量 */
  --font-size-2xs: 0.75rem;
  --font-size-xs: 0.875rem;
  --font-size-sm: 1rem;
  --font-size-md: 1.125rem;
  --font-size-ml: 1.5rem;
  --font-size-lg: 2.25rem;
  --font-size-xl: 3rem;
  --font-size-2xl: 3.75rem;

  /* 间距变量 */
  --space-2xs: 0.25rem;
  --space-xs: 0.5rem;
  --space-sm: 0.75rem;
  --space-md: 1rem;
  --space-ml: 1.25rem;
  --space-lg: 2rem;
  --space-xl: 3.25rem;
  --space-2xl: 5rem;
}

通过使用 CSS 变量,我们消除了应用程序中许多决策制定和争论的燃料。每个人都在使用相同的创意约束集,并且需要一个很好的理由才能打破它。

重用组件而非样式

我注意到,每当我需要重用一个类时,实际上我是在尝试重用一个组件。当我在重用一个按钮的 CSS 时,我并不是将它放在任何其他元素上,我是将它放在一个按钮上。输入字段、布局以及我构建的任何自定义组件也是如此。

我在重用组件,而不是类。

但这有何不同呢?组件是一个完整的、内聚的单元,带有其样式和功能。它还解决了类层次结构的智能感知问题——我们可以通过其属性来沟通可以在组件中调整什么。

以组件的方式思考

拿我们一开始那个费思量的 .highlighted 类来说,它的存在反映了一个需要传递给组件的 prop 的需求。这又是一个样式与标记紧密耦合的例子。

但如果我们错误使用了组件的 API,我们就会得到一个错误。

当我开始用组件而不是标记和样式来思考时,我开始觉得它们的分离是不必要的摩擦。它们描述了同一个实体,这在耦合中是可见的。我需要的 CSS 解决方案是一个不依赖于语义类的方案。

CSS-in-JS

屏幕上的大多数元素都没有事件处理程序或与之相关的领域逻辑,它们是被样式化的传递器,React 组件的唯一责任是接受子元素并渲染它们。

一些前端开发者意识到了样式和标记之间的紧密耦合,并决定创建完全倾向于此的工具。CSS-in-JS 库为我们提供了一种简写 API,同时创建一个组件并对其进行样式设置。

现在,我们将有一组代表上面的 .text-box 类的每个部分的组件。

const TextBox = styled.div`
  border: 1px solid black;
  border-radius: 5px;
  padding: 10px 20px;
  margin-bottom: 5px;
`

const TextBoxTitle = styled.div`
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 15px;
`

const TextBoxContent = styled.div`
  font-size: 16px;
  margin-bottom: 10px;
`

const TextBoxAuthor = styled.div`
  display: flex;
  align-items: center;
`

const TextBoxAuthorImage = styled.img`
  width: 50px;
  height: 50px;
  border-radius: 50%;
  margin-right: 10px;
`

const TextBoxAuthorName = styled.div`
  font-size: 14px;
  font-style: italic;
`

export function TextComponent() {
  return (
    <TextBox>
      <TextBoxTitle>生命的意义是什么?</TextBoxTitle>
      <TextBoxContent>42</TextBoxContent>
      <TextBoxAuthor>
        <TextBoxAuthorImage
          src="/path/to/image.jpg"
          alt="Alex Kondov"
        />
        <TextBoxAuthorName>Alex Kondov</TextBoxAuthorName>
      </TextBoxAuthor>
    </TextBox>
  )
}

我们导出整个 TextComponent,将其呈现为一个整体单元,对其构建块进行抽象封装。这表明组件只能作为一个整体被重用。

我们不必考虑语义类,因为组件的名称描述了其用途。注意,我们没有使用嵌套样式,而是独立描述每个组件,将它们从组件中的位置解耦。

实用类

CSS-in-JS 是一种将标记与样式结合在一起的方法,因此我们可以处理组件。另一种似乎初看起来有些违反直觉的是实用类。

function TextComponent() {
  return (
    <div className="border border-black rounded p-2.5 mb-1.5">
      <div className="text-2xl font-bold mb-4">标题框的标题</div>
      <div className="text-lg mb-2.5">
        框内内容在此。
      </div>
      <div className="flex items-center">
        <img
          src="/path/to/image.jpg"
          alt="Author"
          className="w-12 h-12 rounded-full mr-2.5"
        />
        <div className="text-base italic">作者姓名</div>
      </div>
    </div>
  )
}

此示例使用 Tailwind 类来实现与我们之前示例相同的结果,其实现迄今为止最为简洁。我们再次使用了类,但这一次它们不是语义化的。

我们使用简写样式为每个元素设置样式。如果将传统的样式方法类比为编程中的继承,那么这种方法就相当于组合,我发现后者在 CSS 的上下文中效果要好得多。

结合标记、样式和设计令牌

我们依赖同事审查的事项越多,某些事情被忽视的机会就越高。这里最大的问题是设计令牌。

一个团队同意使用预选的颜色调色板、间距和字体,只是最后发现他们的代码库充斥着魔法值,因为人们匆忙中没有找到合适的值来使用。设计令牌增加了一点摩擦,因为你必须寻找合适的令牌来使用。

在 Tailwind 的实用类中,使用设计令牌是样式本身的一部分。决定字体大小、边距、填充和颜色都内置在实用类中,尽可能减少了摩擦。

同时,编写样式也容易得多,因为你不需要在多个文件之间跳转——你一次性完成标记和 CSS 的编写。

什么是可扩展性?

我与 Tailwind 没有任何关联,但我发现实用类方法对于样式来说是最可扩展的。在 CSS 的上下文中,可扩展性意味着能够在不成比例增加样式工作量的情况下向页面添加更多内容。

使用实用类的样式工作量始终是固定的。开发人员更新一个类时不会无意中改变另一个组件的外观的机会。

不需要考虑可重用性,不需要在代码审查中强制使用设计令牌。使用语义类的理由是它们描述了它们标记的内容,但我们看到这并非那么简单。

将类的语义与内容的性质绑定已经影响了可扩展性。

唯一可重用的组件是那些名称与其内容无关的组件。这并不意味着这些类名称不是语义化的。这只意味着它们的语义与内容无关。

什么是语义化?

语义化关乎某物的共同意义,一种共享的理解。

语义化的 HTML 意味着我们按照预期的方式编写标记,以便其他工程师和工具能够理解它。我们已经约定,标题元素标记为 h1h6,并且 button 应该用来提交表单,而不是 div

但当涉及到 CSS 时就没有语义化了。类背后没有共同的意义。没有人描述过 .text-box 是什么,.card.card-title 是什么。这些都是我们给类赋予的意义。

从这个意义上说,类不能是非语义化的。你不可能编写一个缺乏意义的类,因为赋予意义的人是你。

即使 HTML 规范鼓励开发者使用描述内容的类名,但并没有具体的理由说明为什么这个建议应该适用于已经偏离旧做法的现代前端开发。

语义化的类名更好

.text-box 是不是比 border border-black rounded p-2.5 mb-1.5 这样的类名好?当然是。

但我写 CSS 的目标并不是写出好类名。我的目标是以一种能让我将来更容易与产品协同工作的方式来设计产品。

在我写下 .card-header 类一天之后,我只看代码就没有办法知道它的确切样式了。边距、填充、字体大小——除非我跳到 CSS 文件,否则我对它的作用毫无头绪。

现在,当我看到另一个类时,我可以理解它有哪些样式。但更重要的是,当我未来某个时刻需要快速更改项目时,无论是一个月后还是一年后,我都能够理解。

进行 CSS 更改

我们编写 CSS 的方式和我们更改它的方式大相径庭。

在我们用传统方式编写 CSS 时,我们会思考用类名最好地描述内容。当我们需要更改某些东西时,我们很少考虑更新类设计,如果它不再足够好的话。

通常,我们会借助控制台来精确找到需要修改的位置,并应用一个精确的样式更改。如果我们在编写之后不利用复杂的类层次结构,那么拥有它们就没有意义。

我还没有看到有人能够通过语义化的类正确地追踪到样式。另一方面,使用工具类,我们需要做的更改总是在组件内部。

无类样式理念

我的样式理念是把类作为构建复杂 UI 的不再需要的工具留在过去。关注点分离很重要,但在现代前端开发中,关注点是组件。

要理解我为何如此欣赏这种方法,我们需要回顾在基于组件的库存在之前 CSS 是如何编写的。

在 React、Angular 以及其他让我们的生活变得更加轻松的技术出现之前,我们有着长达数英里的 HTML 文件,这些文件描述了网站的整个页面。

标签、新行和缩进在你需要弄清楚一个元素开始和结束的位置时很有帮助。但这仍然留下了几个问题 – 基于标签选择元素的 CSS 是一场噩梦,而且弄清楚长 HTML 文件中每个元素的含义是不可能的。

因此,类成了我们的解决方案。

如果没有类,深层嵌套的 HTML 几乎是不可能破译的。它们通过提供一种基于其类选择特定元素的简单方式,并为每个元素提供一个目的,解决了这两个问题。

当每个标签都附有一个名称,你可以弄清楚它用于什么时,浏览标记变得容易多了。

但由于这里描述的原因过于冗长,我们现在正在与组件而不是页面一起工作。

我们的开发流程变得简单多了,因为我们可以一次专注于一个元素,思考它需要的数据以及需要应用于它的样式。

但这比大多数开发者想象的要大的一个范式转变。突然间,支撑我们整个样式理念的两个问题不复存在了。当我们一次只处理界面的一小部分时,选择元素和理解它们变得容易多了。

不再需要语义类来解决这些问题。

我们经常谈论限制复杂性,并且在组件内部拥有实用类比我们拥有的任何其他替代方案都要简单得多。

处理复杂性与实用类

我们上面的组件相当简单,因为它没有接受任何 props,但是一旦它变得更加复杂,实用类作为解决方案是不是太不成熟了?

我们有几种方式可以使用它们来处理更高的复杂性。

影响组件样式的 props 会反映为组件实用类的变化。因此,在 className props 中内联一个条件是做到这一点的最简单方式。

function TextComponent({ highlighted }) {
  return (
    <div
      className={`border border-black rounded p-2.5 mb-1.5 ${
        highlighted ? 'bg-black' : ''
      }`}
    >
      // ...
    </div>
  )
}

但这很快就会变得难以处理。我们的开放标签现在因为一个条件而占用了 5 行,可以想象如果我们要添加另外两个条件会发生什么。一个替代方案是使用像 classnames 这样的库来构造类名。

function TextComponent({ highlighted }) {
  const classes = classNames(
    'border',
    'border-black',
    'rounded',
    'p-2.5',
    'mb-1.5',
    { highlighted }
  )
  return <div className={classes}>...</div>
}

但这不是更长了吗?是的,但我们需要记住一件事:需要动态类名的元素只占整个应用程序的一小部分。而且,我们正在处理组件,所以我们只需要管理一次这种复杂性。

如果组件内的其他元素需要条件逻辑,我会做我对待任何其他长函数所做的事情——提取出另一个函数。

样式是复杂的,提取一个组件以便我们可以管理其中的一些内容是绝对可以的。

复杂的类名

有些组件不仅仅会改变一两个类。

function TextComponent({ highlighted, large, disabled, error }) {
  const classes = classNames(
    'border',
    'border-black',
    'rounded',
    'p-2.5',
    'mb-1.5',
    {
      highlighted: highlighted,
      'bg-yellow-200': highlighted && !error,
      'bg-red-200': highlighted && error,
      'text-lg': large,
      'opacity-50': disabled,
      'cursor-not-allowed': disabled,
      'font-bold': highlighted || large,
      'border-red-500': error,
      'hover:bg-gray-100': !disabled,
    }
  )

  return <div className={classes}>...</div>
}

当我们发现自己处于这种情况时,另一个值得考虑的技术是将其拆分为独立的组件。一个函数应该有单一职责,如果一个组件过于灵活,这意味着它做了太多的事情。

一个常见的做法是将基础部分拆分到另一个组件中,只在原始组件中保留可配置的部分。

function StyledTextComponent({
  highlighted,
  large,
  disabled,
  error,
  children,
}) {
  const styledClasses = classNames({
    'bg-yellow-200': highlighted && !error,
    'bg-red-200': highlighted && error,
    'text-lg': large,
    'opacity-50': disabled,
    'cursor-not-allowed': disabled,
    'font-bold': highlighted || large,
    'border-red-500': error,
    'hover:bg-gray-100': !disabled,
  })

  return (
    <BaseTextComponent className={styledClasses}>
      {children}
    </BaseTextComponent>
  )
}

组件架构,而非 CSS 架构

流行的 CSS 范式让我们专注于构建我们的 CSS,但我宁愿样式是我们组件设计努力的一部分,而不是单独考虑的事情。

我们应该有组件架构,而不是样式架构。

我不希望本章听起来像是样式是世界上最难的事情。糟糕的 CSS 会让你的页面看起来有点破碎,可能会激怒一个客户,或在最坏的情况下让你损失一些钱。然而,数据库错误可能会毁掉你的整个公司。

是的,复杂的 UI、动画和布局始终是一个挑战,但绝大多数的网络应用并不需要这些。我们需要的是一种写 CSS 的常识方法,这种方法会与我们正在使用的现有工具很好地结合起来。

在状态管理上,我们已经在以组件为思考方式了。将风格也以这种方式思考是很自然的。

我不想考虑 CSS 架构。我希望能够以一种能让我高效工作并专注于应用程序的关键方面的方式来设置我的组件样式。