照片由Unsplash上的Khamkéo Vilaysing拍摄
令人惊讶的是,当这些组件和使用它们的应用程序都使用 Tailwind 进行样式设计时,关于如何将 Tailwind 与设计系统或共享组件一起使用的文章却很少。 Tailwind 的prefix
选项是专门为实现这一点而设计的,但这是一个有点丑陋的解决方案,直到最近我们才在 Culture Amp 中尽力避免。这是我们学到的一切,以及我们最终采用prefix
的原因。
Tailwind 的简单介绍
TailwindCSS (简称Tailwind)是一个颇具争议的工具选择,适用于手工制作适用于具有语义意义的HTML 元素模式的CSS 选择器没有好处的项目。我们决定在 Culture Amp 中使用它,其原因值得专门写一篇文章。存在过度简化的风险:如果您的 UI 结构与代码库的结构相匹配(即视觉块往往对应于软件组件而不是文档的各个部分),您可以通过直接将样式应用于元素来节省时间和精力这些组件,而不是通过编写与这些组件的实现紧密耦合的 CSS 选择器。再说一遍,这里还有很多东西要说,我会尽快写下来。如果您想阅读它,请告诉我。
从根本上讲,Tailwind 会扫描应用程序的源代码以查找类名,如下所示:
< div class = "m-0" > </ div >
…并生成一个样式表,其中仅包含与您使用的类相匹配的必要样式:
.m-0 { margin : 0px ;}
虽然乍一看这似乎具有内联样式( style
属性)的所有缺点,但 Tailwind 设计了一种非常强大的类名语言,可以涵盖您需要的几乎任何选择器、属性或值。内联样式无法执行伪类选择器或媒体查询,但 Tailwind 可以:
< div class = "dark:hover:bg-sky-500/25" > </ div >
@media ( prefers-color-scheme : dark) { .dark \ :hover \:bg-sky- 500 \/ 25 :hover { background-color : rgb ( 14 165 233 / 0.25 ); }}
在一个非常适合 Tailwind 的项目中(见上文),这实际上消除了编写主要包含与单个元素匹配的选择器的 CSS 代码的需要——这是一种不必要的抽象。
设置共享组件的样式
就本文而言,共享组件是在多个 Web 应用程序中使用的用户界面元素。在 Culture Amp 的案例中,这包括我们Kaizen 设计系统中的 React 组件。
当您想要使用 Tailwind 来设置应用程序的非共享组件和应用程序使用的共享组件的样式时,您需要做出决定:是否使用已编译的 CSS 发布共享组件,或者您希望应用程序的构建运行Tailwind 编译器也可以通过共享组件的源代码生成这些样式吗?
多年来,Culture Amp 采取了第二种选择,即在不编译 CSS 的情况下分发共享组件。这意味着每个使用共享组件的应用程序都需要包含必要的 CSS 构建工具(当时是 CSS 模块和node-sass )以及兼容的版本和配置。这相对容易设置,但随着时间的推移证明很难维护。当 node-sass 被弃用(速度更快但稍微不兼容)的Dart Sass时,这需要在所有这些代码库之间进行困难的锁步迁移,而我们尚未实现这一点。随着新应用程序为了自己的样式而转向 Tailwind,他们必须继续并行维护那些旧的构建工具以实现共享组件的样式。
为了避免共享组件的源代码与使用它们的应用程序的构建工具之间的这种耦合,我们现在想要采取另一种方式:让我们的共享组件在自己的构建管道中构建其样式,并使用纯 CSS 发布组件。这样,我们应用程序的 CSS 构建工具就可以与组件库的 CSS 构建工具保持解耦:Sass 风格的应用程序可以使用 Tailwind 风格的组件而无需运行 Tailwind,而 Tailwind 风格的应用程序可以使用 Tailwind 风格的组件。 Sass 风格的组件,无需运行 Sass。
但是,当 Tailwind 风格的应用程序使用 Tailwind 风格的组件时会发生什么? Tailwind 生成的共享组件样式如何与 Tailwind 生成的应用程序样式共存?
天真的方法
乍一看,将两个 Tailwind 构建的样式表合并为一个似乎是安全的,尽管效率稍低。假设您的组件使用m-0
:
< div class = "m-0" > This is a shared component </ div >
.m-0 { margin : 0px ;}
然后您的应用程序还使用m-0
:
< div class = "m-0" > This is my application. </ div >
.m-0 { margin : 0px ;}
当您在应用程序中使用共享组件时,您会得到如下结果:
< div class = "m-0" > This is my application. </ div > < div class = "m-0" > This is a shared component </ div >
/* shared component styles */ .m-0 { margin : 0px ;} /* application styles */ .m-0 { margin : 0px ;}
.m-0
在样式表中定义了两次,这是有点浪费的部分,但是这两个规则应用了完全相同的样式,因此一切仍然有效。
虽然重复的 CSS 输出很不幸,但与手工编码的 CSS 相比,Tailwind 生成的样式表往往小得令人难以置信,因此这种膨胀可能可以忽略不计。文档中几乎每个需要将边距设置为零的元素都是通过这一(重复的)CSS 规则完成的,而不是传统的 CSS,其中许多不同的规则可能分别指定margin: 0px
。如果您确实想避免这种重复输出,像postcss-discard-duplicates这样的 PostCSS 插件可以为您做到这一点。
小问题:配置耦合
然而,比输出重复更大的缺点是 Tailwind 配置之间产生的耦合。如果有一天您在任一项目(您的共享组件或应用程序)中升级或更改 Tailwind 的配置,并且两个输出不匹配,则一组样式将覆盖另一组样式。
/* shared component styles */ .bg-sky-500 { background-color : #0ea5e9 ; /* new value */ } /* application styles */ .bg-sky-500 { background-color : #87cefa ; /* old value */ }
在此示例中,因为我们的组合 CSS 输出首先包括共享组件的 CSS,其次是应用程序的 CSS,所以应用程序样式“获胜”(因为 CSS 级联按源代码顺序应用相同特异性的规则)。
更令人困惑的是,请记住,Tailwind 只为您使用的类名生成样式,因此,如果您的应用程序使用共享组件使用的某些类而不是其他类,那么您最终可能会得到应用于您的应用程序的两种配置:
/* shared component styles */ .bg-red-500 { background-color : #ef4444 ; /* new value */ } .bg-sky-500 { background-color : #0ea5e9 ; /* new value */ } /* application styles */ .bg-sky-500 { background-color : #87cefa ; /* old value */ }
在上面的示例中,您的共享组件将显示bg-sky-500
的旧值,但显示bg-red-500
的新值,因为应用程序的源代码中未使用第二种背景颜色!
当然,您可以翻转两个样式表的组合顺序,将应用程序的样式放在前面:
/* application styles */ .bg-sky-500 { background-color : #87cefa ; /* old value */ } /* shared component styles */ .bg-red-500 { background-color : #ef4444 ; /* new value */ } .bg-sky-500 { background-color : #0ea5e9 ; /* new value */ }
…但这只是扭转了问题:现在您的应用程序意外地被用于构建共享组件样式的 Tailwind 配置重新设计了样式,但仅在您的应用程序中使用的任何类除外,这些类将保留旧配置的样式。
简而言之,如果您希望一致地应用样式,则需要避免对 Tailwind 配置进行重大更改,或者同步更新两个包。如果您希望 Tailwind 配置相对稳定,那么这本身可能不会成为问题。 Tailwind 本身对于发布重大更改非常谨慎,因此共享组件和应用程序中稍微不同的 Tailwind 版本在大多数情况下不太可能导致问题。
但很难忽视这样一个事实:我们最终又回到了这样一种情况:我们的应用程序被迫将其构建配置与我们的共享组件所假定的配置相匹配。
大问题:Tailwind 取决于源顺序
实际上,这里潜伏着一个更微妙(也是致命)的问题:Tailwind 的设计假设是它控制其生成的规则的源顺序。
让我们再次考虑我们的零边距div
:
< div class = "m-0" > </ div >
.m-0 { margin : 0px ;}
margin
是一个简写属性,这意味着它具有设置margin-block-start
(上)、 margin-inline-end
(右)、 margin-block-end
(下)和margin-inline-start
(左)的效果。
在传统 CSS 中,您可以在这样的简写属性后面加上您想要覆盖的任何特定属性,例如添加左边距:
.bottom-margin-only { margin : 0px ; margin-inline-start : 1rem ;}
上面属性声明的顺序很重要:如果交换它们以便首先设置margin-block-end
,则margin
声明将覆盖它。
在 Tailwind 中,您同样可以使用特定类(如ms-4
)覆盖速记类(如m-0
:
< div class = "m-0 ms-4" > </ div >
.m-0 { margin : 0px ;} .ms-4 { margin-inline-start : 1rem ;}
但这里要注意的关键是HTML 中类名的顺序并不重要:只有生成的 CSS 规则的顺序才重要。
如果我们交换 HTML 中类名的顺序:
< div class = "m-0 ms-4" > </ div >
…左边距仍然会覆盖m-0
,因为 Tailwind 仍然按顺序生成两个 CSS 规则,以确保在更粗粒度的样式(所有内容上的边距)之后应用更细粒度的样式(左边距)。四边):
.m-0 { margin : 0px ;} .ms-4 { margin-inline-start : 1rem ;}
现在,观察 Tailwind 生成的规则的源顺序是其设计的一个关键特征,并将其与我们上面确定的共享组件样式破坏应用程序样式的问题结合起来(反之亦然):
< div class = "m-0" > This is my application. </ div > < div class = "m-0 ms-4" > This is a shared component </ div >
/* shared component styles */ .m-0 { margin : 0px ;} .ms-4 { margin-inline-start : 1rem ;} /* application styles */ .m-0 { margin : 0px ; /* ❌ overides .ms-4 */ }
在此示例中,我们的共享组件使用m-0
和ms-4
,但我们的应用程序仅使用m-0
。应用程序的m-0
规则将覆盖ms-4
规则设置的margin-inline-start
的值,并破坏共享组件的左边距!
再次,您可以通过交换两个生成的样式表的顺序来解决此特定实例,但您最终会遇到相反的问题:共享组件样式干扰应用程序样式,在此示例中,当边距位于应用程序元素上时。
< div class = "m-0 ms-4" > This is my application. </ div > < div class = "m-0" > This is a shared component </ div >
/* application styles */ .m-0 { margin : 0px ;} .ms-4 { margin-inline-start : 1rem ;} /* shared component styles */ .m-0 { margin : 0px ; /* ❌ overides .ms-4 */ }
如果您很想尝试不使用速记样式(例如,避免m-0
而使用mt-0 me-0 mb-0 ms-0
),Tailwind 还取决于媒体查询等修饰符的源顺序:
< div class = "m-0 md:m-4" > This is my application </ div > < div class = "m-0" > This is a shared component </ div >
/* application styles */ .m-0 { margin : 0px ;} @media ( min-width : 768px ) { .md \:m- 4 { margin : 1rem ; }} /* shared component styles */ .m-0 { margin : 0px ; /* ❌ overides .md:m-4 */ }
在这里,我们说第一个div
默认情况下应具有零边距,但在中型或更大屏幕上,它应具有1rem
边距。这两个规则具有相同的特异性(一个类选择器),因此媒体查询样式覆盖默认样式的事实取决于源顺序。共享组件的 CSS 末尾的第二个.m-0
规则破坏了这个边距。
修饰符( dark:
、 hover:
等)是 Tailwind 样式语言的核心功能;它们是不可避免的,正如我们所看到的,它们通过在单个 CSS 样式表中组合多个 Tailwind 构建而被破坏。
我们该如何解决这个问题?
诱人的非解决方案
有几种方法可以解决这个问题,在您仔细考虑之前,它们似乎可以很好地发挥作用。我们将简要介绍每一个,并解释为什么它们不起作用。
!important
Tailwind 开箱即用,可让您使用!important
生成样式,覆盖级联中的竞争样式。例如, class="!ms-4"
将输出:
.\!ms- 4 { margin-inline-start : 1rem !important ;}
首先, !important
是一把非常锋利的刀,最好避免使用。例如,它会干扰 JavaScript 应用于元素的内联样式。但即使我们忽略了!important
所有不好之处,它实际上并不能解决问题!
< div class = "m-0" > This is my application. </ div > < div class = "m-0 !ms-4" > This is a shared component </ div >
/* shared component styles */ .m-0 { margin : 0px ;}.\!ms- 4 { margin-inline-start : 1rem !important ;} /* application styles */ .m-0 { margin : 0px ;}
是的,这可以防止应用程序的m-0
规则覆盖组件的ms-4
规则。但要做到这一点,我们必须在了解将使用它的应用程序的情况下修改我们的共享组件,这是与内部实现细节的不健康耦合,在共享组件和应用程序的现实世界生态系统中维护它是不切实际的。
如果我们让所有组件样式变得重要怎么办? Tailwind 甚至为此提供了一个配置选项:
< div class = "m-0" > This is my application. </ div > < div class = "m-0 ms-4" > This is a shared component </ div >
/* shared component styles */ .m-0 { margin : 0px !important ;} .ms-4 { margin-inline-start : 1rem !important ;} /* application styles */ .m-0 { margin : 0px ;}
好吧,这相当于将我们的共享组件样式放在样式表的末尾:如上所述,共享组件样式最终会在不同情况下破坏应用程序样式:
< div class = "m-0 ms-4" > This is my application. </ div > < div class = "m-0" > This is a shared component </ div >
/* shared component styles */ .m-0 { margin : 0px !important ; /* ❌ overrides .ms-4 */ } /* application styles */ .m-0 { margin : 0px ;} .ms-4 { margin-inline-start : 1rem ;}
提高important
的特异性
Tailwind 的important
配置选项还允许您为顶级容器元素(如body
或#app
)指定选择器,以提高其生成的选择器的特异性,使其超出通常的一类特异性。
< div class = "m-0" > This is my application. </ div > < div class = "m-0 ms-4" > This is a shared component </ div >
/* shared component styles */ body :is ( .m-0 ) { margin : 0px ;} body :is ( .ms-4 ) { margin-inline-start : 1rem ;} /* application styles */ .m-0 { margin : 0px ;}
在这里,再次将important
设置为'body'
似乎可以解决问题,因为共享组件样式现在一致地覆盖应用程序样式。但就像上面的!important
一样,这与将组件样式放在样式表末尾具有相同的效果:共享组件样式会破坏应用程序样式。
< div class = "m-0 ms-4" > This is my application. </ div > < div class = "m-0" > This is a shared component </ div >
/* shared component styles */ body :is ( .m-0 ) { margin : 0px ; /* ❌ overrides .ms-4 */ } /* application styles */ .m-0 { margin : 0px ;} .ms-4 { margin-inline-start : 1rem ;}
CSS 级联层
@layer
CSS at-rule 是一个相对较新但现已得到广泛支持的浏览器附加功能,可让您创建 CSS 规则组并控制它们应用于页面的顺序。因此,您可以指定在应用程序样式之后应用共享组件样式:
< div class = "m-0" > This is my application. </ div > < div class = "m-0 ms-4" > This is a shared component </ div >
@layer application-styles, component-styles; @layer component-styles { .m-0 { margin : 0px ; } .ms-4 { margin-inline-start : 1rem ; }} @layer application-styles { .m-0 { margin : 0px ; }}
但再一次 – 您猜对了 – 这实际上就像将组件样式移动到样式表的底部一样:它会产生组件样式干扰应用程序样式的相反问题。
丢弃重复项
还记得在本文开头我们注意到我们的共享组件和应用程序可以生成相同的 CSS 规则,并且这会不必要地夸大我们的 CSS 输出吗?我提到postcss-discard-duplicates可以摆脱那些重复的规则。
< div class = "m-0" > This is my application. </ div > < div class = "m-0 ms-4" > This is a shared component </ div >
/* shared component styles */ .m-0 { margin : 0px ;} .ms-4 { margin-inline-start : 1rem ;} /* application styles */
这看起来很有希望,因为有问题的风格已经完全消失了。但这里仍然存在一个源顺序问题,因为保留下来的样式是在源顺序中首先出现的样式,但这并不总是正确的。
通过稍微调整的示例,其中重复样式是细粒度样式( ms-4
),我们遇到了同样的问题:
< div class = "m-2 ms-4" > This is my application. </ div > < div class = "m-0 ms-4" > This is a shared component </ div >
/* shared component styles */ .m-0 { margin : 0px ;} .ms-4 { margin-inline-start : 1rem ;} /* application styles */ .m-2 { margin : 0.5rem ; /* ❌ overrides .ms-4 */ }
机会:智能 Tailwind 输出合并
我们面临的根本问题是,两个使用相同全局命名空间作为类名的 Tailwind 构建将不可避免地相互干扰,因为 Tailwind 无法将生成的 CSS 规则按照它们所需的源顺序放置,以便以正确的方式相互覆盖顺序。
但如果我们可以呢?
规则的正确顺序并不神秘。它在 Tailwind 编译器中实现。像prettier-plugin-tailwindcss这样的工具会自动对 HTML 代码中的类名进行排序,以匹配 Tailwind 在 CSS 输出中生成它们的顺序,请使用 Tailwind 中的公共 API来获取此顺序。
那么,如果我们编写一个 PostCSS 插件来获取两个 Tailwind 构建的输出并将它们合并在一起,删除重复的样式并将剩余的样式按正确的顺序排序呢?
这似乎可行,甚至可能是一个相对简单的项目,甚至值得 Tailwind 考虑将其作为核心功能。我很快就会在 Tailwind 问题跟踪器上打开此建议。
即使我们实现了这一点,我们仍然需要接受我们的两个 Tailwind 版本需要具有兼容的版本和配置,以便它们的合并输出能够可靠地工作。
实际解决方案: prefix
正如我在上一节中提到的,我们在这里面临的根本问题是 Tailwind 在全局命名空间中生成样式,因此两个 Tailwind 构建在该命名空间中发生冲突。
如果我们可以给我们的 Tailwind 构建单独的命名空间怎么办?我们的共享组件将仅从其生成的 CSS 输出接收样式,而我们的应用程序元素将仅从应用程序的 Tailwind 构建输出接收样式。
这就是 Tailwind prefix
选项的用途。它允许您指定添加到所有 Tailwind 类名称开头的短字符串,以将它们与需要共存的样式区分开来(在本例中是另一个 Tailwind 生成的样式表)。
例如,我们可以使用 Tailwind 前缀kz-
配置 Kaizen 组件库,并得到:
< div class = "m-0" > This is my application. </ div > < div class = "kz-m-0 kz-ms-4" > This is a shared component </ div >
/* shared component styles */ .kz-m-0 { margin : 0px ;} .kz-ms-4 { margin-inline-start : 1rem ;} /* application styles */ .m-0 { margin : 0px ;}
这两个元素的样式在 CSS 中是完全独立的,因为它们使用两个完全不同的命名空间!您可以交换两个样式表的顺序,这没有什么区别。 (我们将应用程序样式放在最后,因为我们的设计系统组件允许您传入类名来覆盖其内置样式,因此这些应用程序类名需要放在样式表的最后。)
这是解决这个问题的一个干净而完整的解决方案,您可能想知道为什么这不是一篇短得多的文章。
问题是,对于那些已经习惯了 Tailwind 的人来说,其类名的极度简洁是其最好的特性之一。它大大减少了大多数样式任务所需的击键次数。像m-0
这样的字符串会被刻入你的肌肉记忆中,你几乎可以不假思索地输入这些字符串。
因此,如果您告诉喜欢 Tailwind 的工程师,当他们在库中工作时,他们需要记住在所有类名的开头添加三个额外的字符 ( kz-
),并且不同的库将具有不同的前缀,那么您可能会毁了他们的一天。有人将不得不做出非常不受欢迎的决定,决定谁拥有默认(无前缀)名称空间:您的应用程序代码库或设计系统的组件库。为了避免这种痛苦,我们首先尝试了一切可以做的事情。
但这确实是解决使不同 Tailwind 版本/版本/配置在单个网页中共存的问题的最干净的解决方案,而在 Culture Amp,我们刚刚学会接受这一点。
原文: https://kevinyank.com/posts/use-tailwindcss-prefixes-for-shared-design-system-components/