这是一个奇妙的工具。在许多方面,它改变了我对CSS架构的看法,并帮助我保持代码库的整洁和模块化,就像React一样
它与React还有一个共同点:开发人员一开始往往不喜欢这个想法😅。"每个样式都是一个组件 "可能是一粒难以吞咽的药丸,就像 "你的视图现在是用XML/JS混合语言编写的"。
也许是这个原因,我发现很多开发者从来没有真正完全接受过风格化组件。他们在项目中加入了风格化组件,却没有更新他们关于风格化的心理模型。一脚进去,一脚出来。结果,他们错过了这个工具最好的一些部分。
如果你使用风格化组件,或者类似Emotion这样的工具,我希望这篇文章能帮助你从它身上获得最大利益。我把多年的实验和实践提炼成一些实用的技巧和方法。如果你应用这些想法,我真的相信你会成为一个更快乐的CSS开发者✨
预期的读者
这篇文章主要是为已经在使用styled-components或其他CSS-in-JS解决方案(如Emotion)的各种经验水平的React开发者而写的。
这篇文章不是为了介绍styled-components,也不是为了将其与其他工具进行比较或对比。
CSS变量
让我们从一个有趣的小提示开始。
假设我们有一个Backdrop 组件,它需要不透明度和颜色的道具。
function Backdrop({ opacity, color, children }) {
return (
<Wrapper>
{children}
</Wrapper>
)
}
const Wrapper = styled.div`
/* ?? */
`;
你如何将这些属性应用到Wrapper ?
一种方法是使用一个插值函数。
function Backdrop({ opacity, color, children }) {
return (
<Wrapper opacity={opacity} color={color}>
{children}
</Wrapper>
)
}
const Wrapper = styled.div`
opacity: ${p => p.opacity};
background-color: ${p => p.color};
`;
这个方法很好用,但它的摩擦力很大。这也意味着每当这些值发生变化时,风格化组件将需要重新生成类,并将其重新注入到文档的<head> ,这在某些情况下可能是一个性能问题(例如,做JS动画)。
这里有另一种方法来解决这个问题,即使用CSS变量。
function Backdrop({ opacity, color, children }) {
return (
<Wrapper
style={{
'--color': color,
'--opacity': opacity,
}}
>
{children}
</Wrapper>
)
}
const Wrapper = styled.div`
opacity: var(--opacity);
background-color: var(--color);
`;
我们还可以使用CSS变量来指定默认值。
function Backdrop({ opacity, color, children }) {
return (
<Wrapper
style={{
'--color': color,
'--opacity': opacity,
}}
>
{children}
</Wrapper>
)
}
const Wrapper = styled.div`
opacity: var(--opacity, 0.75);
background-color: var(--color, var(--color-gray-900));
`;
如果我们调用<Backdrop> ,而不指定不透明度或颜色,我们将默认为75%的不透明,以及我们的颜色主题的深灰色。
这只是感觉不错。它并不改变游戏规则,但它给我带来了一点快乐。
但这仅仅是个开始。让我们来看看更多的东西。
样式的单一来源
如果你从这篇博文中只带走一样东西,那就是这个提示。这就是母矿。
在这个博客上,我有一个TextLink 的组件。它看起来像这样。
const TextLink = styled.a`
color: var(--color-primary);
font-weight: var(--font-weight-medium);
`;
在这个Aside ,"包含的链接 "这句话是用一个TextLink ,也就是同一个组件来呈现的。不过,我想应用一些不同的风格;我不喜欢在蓝色背景上有蓝色的文字。
这就是我所说的 "上下文风格"。同一个组件根据其上下文而改变外观。当你把一个TextLink 到一个Aside ,一些样式被添加/替换了。
你会如何解决这种情况呢?我经常看到这样的东西。
// Aside.js
const Aside = ({ children }) => {
return (
<Wrapper>
{children}
</Wrapper>
);
}
const Wrapper = styled.aside`
/* Base styles */
a {
color: var(--color-text);
font-weight: var(--font-weight-bold);
}
`;
export default Aside;
在我看来,这是一个五雷轰顶的情况。我们让推理我们的应用程序中的样式变得如此困难!你怎么能发现?
你怎么会发现TextLink 可以被赋予这些样式?你不可能在整个项目中搜索TextLink 。你必须用grep搜索a ,祝你好运。如果我们不知道Aside 应用这些样式,我们就永远无法预测它。
那么好吧,什么才是正确的方法?也许你已经想过用TextLink 而不是a 来指定这些样式。
// Aside.js
import TextLink from '../TextLink'
const Aside = ({ children }) => { /* Omitted for brevity */ }
const Wrapper = styled.aside`
/* Base styles */
${TextLink} {
color: var(--color-text);
font-weight: var(--font-weight-bold);
}
`;
export default Aside;
styled-components允许我们像这样将一个组件 "嵌入 "另一个组件中。当组件被渲染时,它会弹出适当的选择器,一个与TextLink styled-component匹配的类。
这无疑是更好的,但我还不是一个快乐的营员。我们还没有解决最大的问题,我们只是让它稍微容易解决。
让我们退一步,谈谈封装问题。
让我爱上React的原因是,它给了你一种将逻辑(状态、效果)和UI(JSX)打包到一个可重用的盒子里的方法。很多人关注 "可重用 "的方面,但在我看来,更酷的事情是它是一个盒子。
一个React组件沿其周边设置了一个严格的边界。当你在一个组件中编写JSX时,你可以相信HTML只会在该组件中被修改;你不必担心在应用程序的另一端有其他组件 "伸手 "来篡改HTML。
再看一下那个TextLink 的解决方案。 **Aside 正在伸手干预TextLink's styles!**如果任何组件都可以覆盖任何其他组件的样式,那么我们就没有真正的封装了。
想象一下,如果你完全有把握地知道,一个给定元素的所有样式都是在这里定义的,就在有样式的组件本身中,那该有多好啊?
嗯,事实证明,我们可以做到这一点。这就是方法。
// Aside.js
const Aside = ({ children }) => { /* Omitted for brevity */ }
// Export this wrapper
export const Wrapper = styled.aside`
/* styles */
`;
export default Aside;
// TextLink.js
import { Wrapper as AsideWrapper } from '../Aside'
const TextLink = styled.a`
color: var(--color-primary);
font-weight: var(--font-weight-medium);
${AsideWrapper} & {
color: var(--color-text);
font-weight: var(--font-weight-bold);
}
`;
如果你不熟悉& 字符,它是一个生成类名的占位符。当styled-components为这个组件创建一个.TextLink-abc123 类时,它也会用这个选择器替换任何& 字符。这是它生成的CSS。
.TextLink-abc123 {
color: var(--color-primary);
font-weight: var(--font-weight-medium);
}
.Aside-Wrapper-def789 .TextLink-abc123 {
color: var(--color-text);
font-weight: var(--font-weight-bold);
}
通过这个小把戏,我们颠倒了控制。我们在说:"这是我的基础TextLink 样式,这是我被包裹在AsideWrapper中时的TextLink 样式。所有这些都在一个地方。
我们强大的TextLink ,又一次掌握了自己的命运。我们有一个单一的样式来源。
这样做是非常好的。下次你遇到这种情况时,不妨试一试。
工作的正确工具
当我们有两个像Aside 和TextLink 这样的通用组件时,这种形式的 "倒置控制 "使事情变得如此美好和可预测。但它在所有情况下都有意义吗?
让我们想象一下,我们有一个组件,HalloweenSale.js 。这是一个营销页面,使用了我们的标准组件,但增加了一个诡异的字体和橙色的主题。
我们应该更新我们所有的可重复使用的组件,以包括这个变体吗?天哪,不。😅
显示更多
继承的属性
从某种程度上说,这有点像圆孔方钉的情况:组件的边界永远不会是完全密闭的,因为某些CSS属性是可以继承的。
不过,在我看来,这并不是什么大问题。
显示更多
孤立的CSS
好了,我还有一个大想法要分享。
比方说,我们希望Aside ,让它周围有一些空间,这样它就不会紧贴着它的同级段落和标题。
这里有一个方法可以做到。
// Aside.js
const Aside = ({ children }) => {
return (
<Wrapper>
{children}
</Wrapper>
);
}
const Wrapper = styled.aside`
margin-top: 32px;
margin-bottom: 48px;
`;
export default Aside;
这将解决我们的问题,但我觉得这也有点先发制人。我们已经锁定了自己;如果我们决定在另一种情况下使用这个组件,一个有不同间距要求的情况,会发生什么?

以这种方式使用页边距,与可重用的通用组件的理念背道而驰。
Heydon Pickering对此有一段很好的论述。
"页边距就像在你还没有决定要把它粘在什么地方,或者它是否应该被粘在什么地方之前,就把胶水粘在什么地方。"
还有一个事实是,保证金很奇怪。它以令人惊讶和反直觉的方式折叠,这种方式会破坏封装;例如,如果我们把我们的<Aside> 放在一个<MainContent> 里面,那顶部的边距会把整个组推下去,就像MainContent有边距一样。
越来越多的开发者选择完全不使用保证金。我还没有完全放弃这个习惯,但我认为避免像这样的 "漏网之鱼 "是一个很好的妥协,一个很好的开始。
我们如何在没有边距的情况下做间隔?有几个选择!
-
如果是在CSS网格内,你可以用
grid-gap,让每个元素都有空间。 -
如果它在一个Flex容器内,全新的
gap属性会有奇效(尽管你可能希望在[Safari增加支持]之前暂缓)。 -
你可以[使用
Spacer组件],这是一个有争议但令人惊喜的选择。 -
你可以使用一个[专门的布局组件,比如Stack],来自Braid设计系统。
归根结底,我们的目标是避免把我们自己画进一个角落。我相信,只要我们有意为之,并理解其中的利弊,偶尔使用保证金是没有问题的。
最后,我们需要谈谈堆叠上下文的问题。
仔细看一下这段代码。
// Flourish.js
const Flourish = styled.div`
position: relative;
z-index: 2;
/* Omitted decorative properties */
`;
export default Flourish;
看到问题所在了吗?与之前类似,我们先发制人地给了我们的组件一个z-index。我们最好希望2 ,在今后所有的使用情况下都是正确的层!
这个问题还有一个更坏的版本。看一下这段代码。
// Flourish.js
const Flourish = ({ children }) => {
return (
<Wrapper>
<DecorativeBit />
<DecorativeBackground />
</Wrapper>
);
}
const Wrapper = styled.div`
position: relative;
`;
const DecorativeBit = styled.div`
position: absolute;
z-index: 3;
`;
const DecorativeBackground = styled.div`
position: absolute;
z-index: 1;
`;
顶层的风格化组件,Wrapper ,没有设置z-index......当然,这肯定是好的?
我希望它是这样。事实上,这种情况会导致一个超级混乱的问题。
如果我们的Flourish 组件有一个具有中间z-index的兄弟姐妹,它就会在位和其背景之间 "交错"。
<div>
{/* Malcom will get stuck in the middle! */}
<Malcolm
style={{ position: 'relative', zIndex: 2}}
/>
<Flourish />
</div>
我们可以通过明确地创建一个堆叠上下文,使用isolation 属性来解决这个问题。
const Flourish = ({ children }) => {
return (
<Wrapper>
<DecorativeBit />
<DecorativeBackground />
</Wrapper>
);
}
const Wrapper = styled.div`
position: relative;
isolation: isolate;
`;
// The rest unchanged
这确保了任何同级元素都在这个元素的上方或下方。作为奖励,新的堆叠上下文没有z-index,所以我们可以纯粹依靠DOM顺序,或者在我们知道需要什么的时候传递一个特定的值。
杂项技巧和窍门
吁!我们已经涵盖了高层次的 "大 "字。我们已经涵盖了我想分享的高层次的 "大概念",但在我总结之前,我有一些小的花絮,我认为是值得的。让我们来看看这些。
作为 "道具
React开发者有一个名声,那就是对语义HTML一无所知,把<div> 作为一个万能的工具。
对styled-components的一个公平的批评是,它在JJSX和正在产生的HTML标签之间增加了一层间接性。我们需要意识到这一事实,以便我们能够对其进行解释
你创建的每一个样式化组件都接受一个as ,这个道具会改变使用的HTML元素。这对于标题来说非常方便,因为确切的标题级别将取决于具体情况。
// `level` is a number from 1 to 6, mapping to h1-h6
function Heading({ level, children }) {
const tag = `h${level}`;
return (
<Wrapper as={tag}>
{children}
</Wrapper>
);
}
// The `h2` down here doesn't really matter,
// since it'll always get overwritten!
const Wrapper = styled.h2`
/* Stuff */
`;
It can also come in handy f
对于那些可以呈现为按钮或链接的组件来说,它也可以派上用场,这取决于具体情况。
function LinkButton({ href, children, ...delegated }) {
const tag = typeof href === 'string'
? 'a'
: 'button';
return (
<Wrapper as={tag} href={href} {...delegated}>
{children}
</Wrapper>
);
}
语义HTML是非常重要的,而as ,对于所有使用风格化组件的开发人员来说,这是一个至关重要的知识点。
提高特异性
在大多数CSS方法学中,你偶尔会遇到这样的情况:你写的声明没有效果,因为另一个样式正在覆盖它。这被称为特异性问题,因为不受欢迎的样式 "更特异",因而获胜。
在大多数情况下,如果你遵循本文所阐述的技术,我保证你不会有特异性问题,除了在处理第三方CSS时可能出现。这个博客有大约1000个风格化的组件,我从来没有遇到过特定性问题。
我很犹豫要不要分享这个技巧,因为这是对一个真正应该避免的情况的逃避......但我也想现实一点。我们都在并不总是理想的代码库中工作,在你的工具箱中拥有一个额外的工具总是无害的。
这就是了。
const Wrapper = styled.div`
p {
color: blue;
}
`
const Paragraph = styled.p`
color: red;
&& {
color: green;
}
`;
// Somewhere:
<Wrapper>
<Paragraph>I'm green!</Paragraph>
</Wrapper>
在这种情况下,我们有三个独立的color 声明,目标是同一个段落。
在底层,我们的段落使用标准的风格化组件语法被赋予红色文本。不幸的是,Wrapper 使用了一个后裔选择器,用蓝色文本覆盖了红色文本。
为了解决这个问题,我们可以使用一个双击器将其翻转为绿色文本。
正如我们前面所看到的,& 字符是生成类名的占位符。把它放两次会重复该类。而不是.paragraph ,它将是.paragraph.paragraph 。
通过对类的 "加倍",它的特异性增加了。.paragraph.paragraph 比.wrapper p 更加特异。
这个技巧对于提高特异性很有用,而不需要使用核选择,即!important 。但是这里有一个潘多拉的盒子:一旦你开始沿着特异性技巧的道路前进,你就走上了相互保证的毁灭之路。
babel插件
在生产中,样式化组件将为你创建的每个样式化组件生成唯一的哈希值,如.hNN0ug 或.gAJJhs 。这些简洁的名字是有益的,因为它们不会在我们的服务器渲染的HTML中占用太多空间,但它们对我们开发人员来说是完全不透明的。
值得庆幸的是,有一个[babel插件]存在!在开发过程中,它使用了语义学的类。在开发中,它使用语义类名称,以帮助我们追踪一个元素/样式的来源。
如果你使用[create-react-app],你可以从这个插件中获益,而不需要通过改变你所有的导入来弹出。
import styled from 'styled-components/macro';
在你的项目中快速查找和替换,将极大地改善你的开发经验!
早些时候,我们谈到了使用下级选择器会使你的应用程序中的样式更难追踪。但是,如果我们可以使用babel插件来追踪样式,这个问题不就消失了吗?
遗憾的是,不是。完全不是。
心智模式
在这篇文章中,我们看了一些针对风格化组件的API,但实际上我希望传达的思想比任何特定的工具或库都要大。
当我们把组件的思维方式扩展到我们的CSS时,我们就会获得各种新的超级力量。
-
我们有能力自信地知道删除一个CSS声明是否安全(不可能影响到应用程序的某个完全不同的部分!)。
-
完全不存在特异性问题,不再试图寻找技巧来提高特异性。
-
一个整齐划一的心理模型,适合你的头脑,帮助你准确理解你的页面会是什么样子,而不需要做大量的手工测试。
styled-components相对来说没有什么主见,所以有很多不同的使用方法......但我必须承认,当我看到开发者把它当作一个花哨的类名生成器,或者 "Sass 2.0 "时,我有点难过。如果你认为风格化组件首先是组件,你就会从这个工具中得到更多。
当然,这些只是我的观点,但我很高兴了解到它们与推荐的做法是一致的。我把这篇文章的早期草稿发给了Max Stoiber,他是styled-components的创建者,下面是他的回复。
对我来说,很多东西已经淡出了我的视线,只有在经过几年的实验后才变得清晰起来。我希望这个帖子能为你节省一些时间和精力。