如何保持 Tailwind DRY

2025-05-25

如何保持 Tailwind DRY

我在网上看到很多关于 Tailwind 的抱怨:它是 WET 样式,而不是 DRY 样式,它和内联样式一样,无法进行全局更改,而且难以阅读。我理解你刚开始使用 Tailwind 时可能会有这种感觉。但需要记住的是,Tailwind 与传统 CSS 完全不同,你不应该把它当成传统 CSS

Tailwind 有很多好处,例如其极小的打包体积和超快的原型设计能力。我在之前的文章中对此进行了更详细的解释。但我们只有在正确的情况下才能获得这些好处;如果用在错误的环境中,Tailwind 只会给你带来麻烦。

什么时候不适合使用 Tailwind CSS?

我首先建议不要将 Tailwind 用于纯 HTML 静态网站。构建静态网站时,您不可避免地需要复制粘贴 HTML,因为同一个页面上的相同组件/部分可能会出现多次。

如果您使用的是传统的 CSS 方法论(例如BEM) ,那么完全没问题:您的 CSS 和 HTML 完全独立存在,因此您可以依赖 CSS 作为网站外观的唯一可信来源。如果您更改了 CSS 类,则更改将反映在所有使用该类的地方,而无需更新 HTML。这样,即使您复制粘贴了一些 HTML,也不会有太大影响。

// you can copy and paste these classes anywhere
<button class="button button--negative"></button>

<button class="button button--negative"></button>

// but you could also break rules like this
<div class="button"></div>

关注点分离传统上是通过语言来定义的

就 CSS 和纯 HTML 的关注点分离而言,这差不多就是你能做到的极限了。我个人仍然认为这种方法不太符合 DRY 原则,因为你需要在多个地方复制粘贴相同的代码,但这差不多是使用基础 HTML 所能做到的最好了——在我学习 CSS 的时候,我一直不太适应这一点。为了使这个系统真正符合 DRY 原则,你需要使用某种形式的模板或基于组件的框架,这样你就可以只为一个部分编写一次 HTML,然后在任何你喜欢的地方重复使用该组件。这让我想到了……

什么时候是使用 Tailwind CSS 的最佳时机?

很高兴你问这个问题!如果你不想在使用 Tailwind 构建网站时重复工作,你可能需要使用某种 JavaScript 框架。无论是 React、Vue 还是其他一些新潮的框架,重要的是你可以构建可重复使用的 JS 组件。你或许可以将其与 PHP 模板一起使用,但我认为这种方法最适合 JavaScript,因为你可以将 HTML、JS 和 CSS 保存在同一个文件中。

这才是 Tailwind 真正的使用方式:作为一种完全不同的范式,关注点分离并不意味着分离 HTML、CSS 和 JS,而是分离整个组件,并将与该组件相关的所有内容保存在一个文件或文件夹中。这是一种与我们习惯的工作方式截然不同的方式,它有其自身的挑战,但这种方法有一些很大的好处:

  • 组件可以彼此独立地运行,并且可以在不同的项目之间轻松使用
  • 组件可以单独测试,因此您不必担心以后会发生变化
  • 原型设计速度更快,因为你不需要为每个元素编写自定义类
  • 完全使用 JavaScript 实现比常规 HTML 更高级的条件样式
  • 鼓励组件组合——一旦你拥有一堆组件,就可以通过组合已有组件轻松构建页面,甚至构建组件的新变体

采用基于组件的架构

一旦将 HTML、JavaScript 和 CSS 整合到一起,您就会发现将组件放在各自的文件夹中比将资源分散到整个项目中的不同文件树中要容易得多。这种方式带来了新的机遇,例如能够使用 JavaScript 指定样式并为视图构建更复杂的逻辑。

基于组件的开发中的文件结构

以下是一些帮助您适应基于组件的开发的提示:

1. 将组件分解成可重复使用的小块

你是否注意到,在设计中,往往会出现大量重复的图案?你可以利用类组合来充分利用这一点。常见的布局是 50/50 的比例,一侧是文本,另一侧是某种媒体。我倾向于将它们称为SplitContent块。这种布局通常存在变体,例如某些文本的大小不同,或者媒体位置用轮播图而不是图片填充。

与其构建两个大部分样式完全相同的组件,不如创建一个带有 props 的容器组件,这些 props 是用于添加任意内容的 slot。您可以在其中设置样式逻辑——例如,您可以使用 props 来更改内容显示在哪一侧,或者在某一侧添加内边距。或者,您也可以直接添加一个 props,该 props 可以传递一串类名,从而根据容器在不同上下文中的使用情况进行自定义。

对于我想将 SplitContent 用作 CMS(例如 Wordpress)的动态内容块的区域,我可能会创建一个Handler组件,该组件分解 CMS 中定义的样式选项并传递相关的组件组合。

例如,您可能希望客户只能访问 CMS 中的一个 SplitContent 组件,但可以选择使用该组件创建多种不同的布局。一些选项可能包括:

  • 您希望每一侧都包含哪种类型的内容?
  • 每种内容类型应该位于哪一侧?
  • 这个组件是否需要不同的配色方案?

组件处理程序可以接受这些选项并返回正确的布局,同时将所有这些逻辑保留在其自身内,以便其他组件仍然可以在不同的组件中使用。

我通常将与 SplitContent 相关的所有内容放在一个文件夹下,并添加一个包含构成主要组件的较小部分的子文件夹:

这只是一个例子;本质上,您的组件都应该有一个单一的用途,这样就可以更轻松地使用您创建的部件来构建更大、更复杂的组件。

2. 使用 JS 构建类列表

如果你觉得 Tailwind 难以阅读,那你并不孤单。这是最常见的抱怨之一,我能理解为什么:你必须阅读每个类才能理解发生了什么,但这并不适用于所有人。

依赖 JavaScript 来构建类名可能会有所帮助。我通常更喜欢这种方法,而不是为了编写新的 CSS 类而编写,尤其是在它们可能只在一个地方使用的情况下。有些人可能会说这和使用@apply指令一样,但如果这个类不会在其他地方使用,就没有必要为它编写一个全新的类。像这样用 JavaScript 编写类有助于将与该组件相关的所有内容放在一个相似的位置,而不是将其放在 CSS 文件夹中很远的地方。

// components/Modal/View.jsx

export default function ModalView () {
  const modalContainerClass = "bg-white p-4 rounded shadow";
  const modalHeadingClass = "heading-1 text-darkgrey";

  return (
    <aside className={modalContainerClass}>
      <h1 className={modalHeadingClass}>...</h1>
    </aside>
  );
}

将类存储在 JavaScript 变量中可以更清楚地了解使用它要完成的任务,同时也提供了使用比 CSS 更高级的逻辑的机会。

3. 使用 props 扩展组件

与普通 CSS 相比,我们在使用 Tailwind 时遇到的一个问题是,我们失去了将组件的基本版本扩展为具有类的新修改版本的能力:

// _button.scss

.button {
  padding: 20px;
  border: 1px solid black;
}
.button--negative {
  border-colour: red;
}

// index.html

<button class="button">Accept</button>
<button class="button button--negative">Cancel</button>

当然,我们可以手动将border-redTailwind 类添加到任何想要设为负片的按钮上,但是如果有多个样式怎么办?如果背景和文本颜色也发生变化怎么办?

// this would be a nightmare if the negative styles ever changed

<button class="p-5 border-red bg-red text-white">Cancel</button>

解决方案:使用 JavaScript 扩展组件

当我们切换到基于组件的开发时,我们能够使用 JavaScript 代替 CSS 来创建组件。由于不再受单独样式表的束缚,您可以以基础组件为起点,将组件抽象到不同的文件中,从而创建组件的变体。

最灵活的方法之一是将类名作为 props 传递下去,并将其与组件上现有的类合并。这是一个将解构的 props 与其他值合并的示例,如精彩的资源reactpatterns.com所示

使用此方法后,我们的按钮变体可能看起来如下:

// components/Button/index.jsx

export default function Button = ({ classnames, handleOnClick, label }) {
  const buttonClass = [
    "p-5 border-1", // default button styles
    classnames      // any additional styles
  ].join(' ');
  
  return (
    <button className={buttonClass} onClick={handleOnClick}>
      {label}
    </button>
  )
}

// components/Button/Negative.jsx

export default function ButtonNegative = (props) {
  return (
    <Button
      classnames="border-red bg-red text-white"
      {...props}
    />
  )
}

现在,我们可以将index.jsx其用作按钮的底层,并将所有逻辑保留在该层级上,同时清晰地定义该按钮的变体,而无需更改任何功能。这样,如果以后样式发生变化,任何<ButtonNegative />使用的代码都会反映该文件中的更改。

4. 将视图逻辑和业务逻辑移至单独的文件

这对于使用 JavaScript 框架来说是一个相当通用的技巧,但在 Tailwind 中,它的作用更大,因为它将样式与业务逻辑分离,而无需将它们放在完全不同的文件夹中。您可以进入 Button 文件夹,并知道该文件夹中的所有内容都与按钮相关。

一旦你把所有内容都放在一处,你就可以开始进一步分解:在 React 中,你可以将组件的外观与其行为分开。以下是一个例子:

// components/Carousel/View.jsx (view logic only)
export default function CarouselView ({ slides }) {
  return (
    <SomeCarouselPlugin>
      {Array.isArray(slides) && slides.map(slide => (
        <CarouselSlide {...slide} />
      ))}
    </SomeCarouselPlugin>
  )
}

// components/Carousel/Jobs.jsx (business logic only)
export default function JobsCarousel () {
  const [jobs, setJobs] = useState(null);
  
  const fetchJobs = async () => {
    const res = await request({
      url: 'my-api-url.com/jobs?limit=16',
      method: 'GET'
    })
    setJobs(res.data)
  }
  
  useEffect(() => {
    fetchJobs();
  }, [])
  
  return !!jobs ? (
    <CarouselView slides={jobs.map(job => ({
      title: job.title,
      description: job.description,
      salary: 'Up to ' + job.salary.max
    }))} />
  ) : <>Loading...</>
}

如果我们想制作另一个使用相同样式的轮播,也许我们希望轮播中充满工作人员而不是工作,我们可以通过在中创建一个新的容器组件来实现Carousel/Staff.jsx

这对于分解数百甚至数千行代码的大型组件非常有帮助,而且这种方法意味着,如果您需要更多自定义功能,还可以添加额外的层。这种扩展系统可以更轻松地分解组件的功能,同时确保您不会重复编写代码。

5. 使用类组合来组合容器、文本样式以及组件之间使用的任何内容

没错:即使组件是你的数据源,自定义类仍然有其用武之地。例如,你可能会在许多不同的组件上使用一个容器类,该类具有最大宽度、margin: auto 和一些侧边距。由于这些不太可能改变,因此使用@apply指令编写一个新的自定义类是合理的。

我个人也喜欢添加一些排版类,比如标题、标准内容块等等。虽然为这些内容创建新的 JavaScript 组件意义不大,但它们可以在多个地方组合相同的样式。

.page-wrap {
  @apply max-w-page mx-auto px-4 tablet:px-5 laptop:px-6;
}

.paragraph {
  @apply text-16 font-body leading-loose;
}

// we can still create variants of .paragraph
<p class="paragraph text-white">Hello world!</p>

6. 编写类时,避免使用边距

您可以通过使类与位置无关来提高其可复用性。如果您省略掉诸如边距之类的仅影响元素位置的属性,则可以更频繁地复用它们。

// _typography.scss

.heading-2 {
  @apply text-black text-24 bold;
}
<h2 className="heading-2 mb-4">Hello world!</h2>

这并非在所有情况下都适用——也许你确实希望每个标题都有一定的边距。但在大多数情况下,这是一个值得记住的技巧,它可以让你的组件更加灵活,并且更少地依赖于它们在页面上的位置。

7. 将 tailwind.config.js 作为真实来源

在 SCSS 或 LESS 中,您可以为颜色、字体和最大宽度等常量创建变量。您可以在 CSS 的任何位置重复使用这些变量,并且如果您更改变量,此更改将反映在其使用的所有位置。

Tailwind 的工作方式大致相同,但所有内容都由变量定义。这意味着您不仅可以使用文本或背景颜色,还可以使用间距、大小、边框以及几乎任何您能想到的其他属性。您可以将这些功能与theme中的对象结合使用tailwind.config.js,也可以使用 对象扩展默认主题extend

此文件定义了整个应用程序的外观:如果您的设计师使用了通常使用数字等的设计系统,4, 8, 12, 16, 32您可以将该系统直接构建到您的 CSS 中:

spacing: {
  1: '4px',
  2: '8px',
  3: '12px',
  4: '16px',
}

然后就可以立即使用这些类了,并且在 的情况下,spacing属性将应用于paddingmarginrelative定位类leftright

别忘了,你也可以使用常规 JavaScript 来生成其中一些属性,这样可以节省一些时间并简化文件。我喜欢创建一个const包含类似上面数组的数组,并将其用于spacingwidth以及height任何其他类似的属性——甚至font size

我也考虑过使用黄金比例生成这种间距/字体系统的想法,这可能是快速原型设计的绝佳选择,同时还能保持良好的视觉流畅。

8. 使用工具发现重复模式

关于组成类的主题,有一些很棒的工具可以帮助您在类列表中找到重复的模式,以便您可以将它们重构为自己的通用类。

最实用的工具之一是 Refactor CSS,这是一个 VS Code 扩展,它可以自动查找并显示非常相似的类字符串,这有助于找到可以抽象成新类的常见模式。如果类字符串包含超过 3 个类,并且这 3 个类在当前文档中重复超过 3 次,则会突出显示这些类字符串。类的顺序会被忽略,因此您无需担心维护属性排序顺序即可确保该工具正常工作。

如果您担心属性排序顺序(如果您担心的话,Tailwind 的可读性要高得多),那么您可以使用另一个工具来解决这个问题:Headwind。这个 VS Code 扩展会在保存时格式化您的 Tailwind 类,并根据其功能对其进行分组,确保所有内容都符合您的预期。

// before saving
<div class="bg-red container mb-6 text-white"></div>

// after saving
<div class="container mb-6 text-white bg-red"></div>

您还可以更改 Headwind 的正则表达式,这样您就可以按照自己喜欢的方式自定义排序顺序。

结论

我不会假装转向这种工作方式特别容易,还有很多问题需要解决。我们仍然处于实用程序优先框架和基于组件的开发的早期阶段,所以一切都还没有弄清楚。

尽管如此,我相信我们将会开始看到新的工具、软件包和方法,旨在解决我们可能面临的任何问题。这种方法能够让我们获得真正遵循DRY原则的轻量级、快速应用程序。独立、隔离的组件非常适合跨平台构建,因此我认为我们会看到许多构建无头系统的公司采用这种工作方式。

我在我的博客npm run dev上撰写了更多类似这样的关于无头系统和基于组件的开发的文章。如果您感兴趣,可以去看看,我很乐意听取您对我的想法和写作风格的反馈。感谢您的阅读!

文章来源:https://dev.to/charliejoel/how-to-keep-tailwind-dry-2jfe
PREV
ChartDB:3 天内从零到 1.5K GitHub Stars - 方法如下🚀⭐️
NEXT
使用 Tailwind 而非传统 CSS 的 6 个理由