构建 TailwindCSS 类名的简单策略

2025-06-10

构建 TailwindCSS 类名的简单策略

这是我关于 TailwindCSS 的系列文章的第三篇。如果你还没看过,可以看看我的其他文章。

任何提议在项目中使用 TailwindCSS 的人可能都听过这样的话:

“呃,这看起来像内联样式!”

“我的组件中有这么多类名,我该如何保持概览?”

“这似乎很难维持……”

是的,我理解这些担忧。Tailwind 的“实用程序优先”方法默认会将任何实用程序类名直接写入组件的标记中。对于更复杂的组件,这种情况很快就会失控。

在今天的帖子中,我们将讨论一个可能更好的解决方案,我已经在我的项目中使用了一段时间了。

一个简单的例子

我们以这个Navigation组件为例:

const Navigation = ({ links }) => {
  const router = useRouter()
  return (
    <nav className="container">
      <ul className="flex flex-col justify-end list-none sm:flex-row">
        {links.map((link, index) => {
          return (
            <li
              key={index}
              className="mb-3 sm:ml-3 sm:mb-0 even:bg-gray-50 odd:bg-white"
            >
              <a
                className={`text-black font-bold inline-block rounded-full bg-yellow-400 py-1 px-3 ${
                  router.pathname === link.path
                    ? 'text-white'
                    : 'hover:bg-yellow-500'
                }`}
                href={link.path}
              >
                {link.name}
              </a>
            </li>
          )
        })}
      </ul>
    </nav>
  )
}
Enter fullscreen mode Exit fullscreen mode

我们该怎么做才能让组件看起来不那么混乱?

我的第一条经验法则是:所有计算都应该在渲染/返回函数之前进行,并且只在渲染过程中使用这些计算出来的标志。这同样适用于router.pathname === link.path条件——我们把它移到 a 中const,并命名为isActive

既然我们已经这样做了,让我们将className定义也移动到consts 中——只需根据它们相应的 HTML 元素来命名它们(使用语义元素而不是一堆divs 的另一个原因 ;)):

const Navigation = ({ links }) => {
  const router = useRouter()
  const navClassNames = 'container'
  const listClassNames = 'flex flex-col justify-end list-none sm:flex-row'
  return (
    <nav className={navClassNames}>
      <ul className={listClassNames}>
        {links.map((link, index) => {
          const isActive = router.pathname === link.path
          const listItemClassNames =
            'mb-3 sm:ml-3 sm:mb-0 even:bg-gray-50 odd:bg-white'
          const anchorClassNames = `text-black font-bold inline-block rounded-full bg-yellow-400 py-1 px-3 ${
            isActive ? 'text-white' : 'hover:bg-yellow-500'
          }`
          return (
            <li key={index} className={listItemClassNames}>
              <a className={anchorClassNames} href={link.path}>
                {link.name}
              </a>
            </li>
          )
        })}
      </ul>
    </nav>
  )
}
Enter fullscreen mode Exit fullscreen mode

看起来已经好多了,但仍有改进的空间。

使用.join(" ")

我们不用写一长串的 className,而是用数组来自动连接它们。数组的好处在于,你可以有条件地添加条目,这样就不用再用模板字面量来判断了:

const Navigation = ({ links }) => {
  const router = useRouter()
  const navClassNames = 'container'
  const listClassNames = [
    'flex',
    'flex-col',
    'justify-end',
    'list-none',
    'sm:flex-row',
  ].join(' ')
  return (
    <nav className={navClassNames}>
      <ul className={listClassNames}>
        {links.map((link, index) => {
          const isActive = router.pathname === link.path
          const listItemClassNames = [
            'mb-3',
            'sm:ml-3',
            'sm:mb-0',
            'even:bg-gray-50',
            'odd:bg-white',
          ].join(' ')
          const anchorClassNames = [
            'text-black',
            'font-bold',
            'inline-block',
            'rounded-full',
            'bg-yellow-400',
            'py-1',
            'px-3',
            isActive ? 'text-white' : 'hover:bg-yellow-500',
          ].join(' ')
          return (
            <li key={index} className={listItemClassNames}>
              <a className={anchorClassNames} href={link.path}>
                {link.name}
              </a>
            </li>
          )
        })}
      </ul>
    </nav>
  )
}
Enter fullscreen mode Exit fullscreen mode

(关于有条件地添加 className 的三元运算符的一个注意事项:如果您没有非此即彼的操作,只需在 else 情况中添加一个空字符串(例如isCondition ? 'myClass' : ''),而不要依赖像 这样的简写isCondition && 'myClass'。后者适用于值,但在条件为假的情况下向数组undefined添加一个字符串。)"false"

将所有组件样式抽象成一个styles对象

让我们进一步研究这种方法:在这个例子中,一个组件中有多个元素,特别是在组件return功能之外创建样式对象可能是有意义的。

但有一个问题:在我们的锚链接样式定义中,我们依赖于对标志的访问isActive。我们可以通过将其定义从字符串转换为返回字符串的箭头函数来轻松解决这个问题。使用这样的函数,您可以在元素样式数组的范围内提供所需的任何条件:

const styles = {
  nav: 'container',
  ul: [
    'flex',
    'flex-col',
    'justify-end',
    'list-none',
    'sm:flex-row',
  ].join(' '),
  li: [
    'mb-3',
    'sm:ml-3',
    'sm:mb-0',
    'even:bg-gray-50',
    'odd:bg-white',
  ].join(' '),
  a: ({ isActive }) =>
    [
      'text-black',
      'font-bold',
      'inline-block',
      'rounded-full',
      'bg-yellow-400',
      'py-1',
      'px-3',
      isActive ? 'text-white' : 'hover:bg-yellow-500',
    ].join(' '),
}

const Navigation = ({ links }) => {
  const router = useRouter()
  return (
    <nav className={styles.nav}>
      <ul className={styles.ul}>
        {links.map((link, index) => {
          const isActive = router.pathname === link.path
          return (
            <li key={index} className={styles.li}>
              <a className={styles.a({ isActive })} href={link.path}>
                {link.name}
              </a>
            </li>
          )
        })}
      </ul>
    </nav>
  )
}
Enter fullscreen mode Exit fullscreen mode

这里还有一点需要注意:我把标志放入了一个对象中,而不是直接放入参数列表中(({ isActive })而不是(isActive))。这样做更合理,因为这样更容易维护:否则,您必须在函数调用和样式对象中的定义中都考虑标志的特定顺序。使用对象的解构语法,您可以解决这个问题,而不必担心对象条目的位置——只需再添加两个字符即可。

将样式放入单独的文件中

如果您想更进一步,您可以使用相同的方法将您的样式外包到单独的文件中:

// Navigation.styles.js
export default {
  nav: 'container',
  ul: [
    'flex',
    'flex-col',
    'justify-end',
    'list-none',
    'sm:flex-row',
  ].join(' '),
  li: [
    'mb-3',
    'sm:ml-3',
    'sm:mb-0',
    'even:bg-gray-50',
    'odd:bg-white',
  ].join(' '),
  a: ({ isActive }) =>
    [
      'text-black',
      'font-bold',
      'inline-block',
      'rounded-full',
      'bg-yellow-400',
      'py-1',
      'px-3',
      isActive ? 'text-white' : 'hover:bg-yellow-500',
    ].join(' '),
}
Enter fullscreen mode Exit fullscreen mode
// Navigation.jsx
import styles from "./Navigation.styles";

const Navigation = ({ links }) => {
  const router = useRouter()
  return (
    <nav className={styles.nav}>
      <ul className={styles.ul}>
        {links.map((link, index) => {
          const isActive = router.pathname === link.path
          return (
            <li key={index} className={styles.li}>
              <a className={styles.a({ isActive })} href={link.path}>
                {link.name}
              </a>
            </li>
          )
        })}
      </ul>
    </nav>
  )
}
Enter fullscreen mode Exit fullscreen mode

我使用这种方法已经有一段时间了,我非常喜欢它。它简洁明了,让我在编写 TailwindCSS 时不会因为一堆类名而弄乱组件。

其他方法

您还可以使用其他一些方法来代替或结合上述方法:

使用classnames()(或clsx()

classnames()库是一个简单的实用程序,用于将您的类名连接成字符串。它内置了一些可能有用的附加函数。

clsx()具有相同的 API,但捆绑包大小较小:

这些库非常有意义,特别是在处理许多条件(如isActive上述示例中的条件)或需要展平的嵌套数组时。

对于大多数情况,我会说像上面那样加入一个数组就可以完成工作,并且你不需要任何额外的包 - 但对于更大的项目来说,采用这些库的 API 可能是有意义的。

布里斯

另一个有趣的方法是 pago 的 brise:

它使用模板字面量来处理 Tailwind 样式。它甚至允许你使用 Emotion 的css实用程序添加自定义 CSS。

它也绝对值得一试。

希望这篇文章能启发您使用 TailwindCSS 编写更简洁的组件。如果您还有其他建议,欢迎在评论区留言!

鏂囩珷鏉ユ簮锛�https://dev.to/wheelmaker24/a-simple-strategy-for-structuring-tailwindcss-classnames-1ba9
PREV
GraphQL 简介以及如何使用 GraphQL API
NEXT
使用 Python 创建 WhatsApp 机器人:开发人员分步指南