简介
这是个有争议的观点,但我相当喜欢CSS-in-JS。😬🚨
但是!我也非常喜欢CSS。我不认为使用CSS-in-JS可以免除你对CSS的学习。无论怎样,你都是在写CSS!它只是被包装得有点不同。
无论你在哪里写CSS,你都有必要掌握这种语言。熟练掌握CSS将使你成为一个更有效的前端开发者。
在本教程中,我们将看到如何利用CSS中最令人兴奋的新发展之一。CSS变量,又称自定义属性。我们将看到如何在我们的React应用程序中使用它们来改善我们的工作流程,并做一些非常花哨的事情。
为什么?
作为一个React开发者,你可能会想,你不需要CSS中的变量。你有一个完整的JavaScript引擎供你使用!
在你的React应用中改用CSS变量有两个原因。
-
人机工程学很好。
-
它开启了新的可能性!你可以用CSS变量做一些JS无法做到的事情。
让我们来看看如何使用它们,然后我们会看到有哪些门被打开了!
快速介绍
让我们来看看CSS变量的作用。
编码游戏场
<style>
p {
--highlight-color: yellow;
}
em {
background: var(--highlight-color);
}
</style>
<p>
This paragraph has some <em>emphasized text</em>!
</p>
在这个例子中,我们在段落选择器上定义了一个新变量,--highlight-color
。我们用这个颜色来给子<em>
元素应用一个背景色。每当在一个段落中使用<em>
,它就会有一个黄色的背景。
注意我们是如何以定义典型的CSS属性的方式来定义我们的CSS变量的。这是因为CSS变量就是属性。它们被正式称为 "CSS自定义属性",这是有原因的。
CSS变量默认是可继承的,就像font-size
或color
。当我们在一个元素上定义一个变量时,该元素的所有子元素都可以使用。
许多开发者认为,CSS变量是全局性的,但这并不完全正确。请看这个。
Code Playground
<style>
p {
--highlight-color: yellow;
}
em {
background: var(--highlight-color);
}
</style>
<h1>
This heading has some <em>emphasized text</em>!
</h1>
这个标题中的em
将background
设置为var(--highlight-color)
,但它没有效果,因为--highlight-color
还没有为这个元素定义。它只被定义为段落,而这个<em>
标签并不在一个段落内。
有时,我们希望我们的CSS变量是全局的。例如,CSS变量通常用于颜色主题,在这种情况下,我们希望这些颜色在整个应用程序中都可用。
我们可以通过将我们的变量挂在顶层元素上来做到这一点,<html>
。
/*
This variable will be available everywhere,
because every element is a descendant of
the HTML tag:
*/
html {
--color-red: hsl(0deg 80% 50%);
--color-blue: hsl(270deg 75% 60%);
}
:root?
教程中经常显示CSS变量被附加到一个神秘的:root
选择器上。
:root {
--color-red: hsl(0deg 80% 50%);
--color-blue: hsl(270deg 75% 60%);
}
:root
是对顶层元素的引用。它等同于 选择器。为了简单起见,我在这篇博文中会使用 。html
html
使得CSS变量与典型的CSS属性不同的是,我们可以访问它们的值,使用var()
函数。这就是允许我们使用CSS自定义属性作为变量的原因。
关于CSS变量的其他一些简单事实。
-
自定义属性需要以两个破折号开始。这也是它们与传统的CSS属性的区别所在。
-
它们可以保存任何类型的值,而不仅仅是颜色和像素。
-
如果没有定义CSS变量,你可以指定一个默认值:
var(--primary-color, pink)
,必要时可以返回到pink
。
在一个React应用程序中
让我们看看这在React中是什么样子的。本教程使用的是styled-components,但无论使用哪种CSS-in-JS库,其说明都应该比较相似。
首先,我假设你有一个保存所有设计标记的文件,就像这样。
const COLORS = {
text: 'black',
background: 'white',
primary: 'rebeccapurple',
};
const SIZES = [
8,
16,
24,
32,
/* And so on */
];
在React应用中,你可以直接将它们导入需要它们的组件中。
import { COLORS } from '../constants';
const Button = styled.button`
background: ${COLORS.primary};
`;
或者,你可以使用一个主题。
// components/App.js
import { ThemeProvider } from 'styled-components';
import { COLORS } from '../constants';
// This element wraps our entire application,
// to make the theme available via context.
const App = ({ children }) => {
return (
<ThemeProvider theme={{ colors: COLORS }}>
{children}
</ThemeProvider>
);
};
// Elsewhere…
const Button = styled.button`
background: ${(props) => props.theme.colors.primary};
`;
下面是同样的代码,但使用CSS变量进行设置。
import { createGlobalStyle } from 'styled-components';
const GlobalStyles = createGlobalStyle`
html {
--color-text: black;
--color-background: white;
--color-primary: rebeccapurple;
}
`;
const App = ({ children }) => {
return (
<>
<GlobalStyles />
{children}
</>
);
};
(如果你不熟悉createGlobalStyle
,它允许我们编写非范围性的CSS,就像我们在一个styles.css
文件中编写一样。)
我们已经创建了一些变量,把它们挂在根节点上,现在我们可以在我们的组件中访问它们。
const Button = styled.button`
background: var(--color-primary);
`;
在我看来,这是一个不错的小胜利。能够在没有导入或内联函数的情况下访问主题值是一种新鲜的空气。你确实失去了一些静态类型的好处--稍后再谈这个--但在我看来,这是一个非常愉快的权衡。
不过,这只是一个相对较小的差别。让我们来看看更有趣的东西...
改变数值,而不是改变变量
让我们假设我们有一个Button组件。
代码游戏场
const Button = styled.button`
min-height: 32px;
padding: 0 32px;
border-radius: 16px;
border: none;
color: white;
font-size: 1rem;
font-weight: 600;
text-shadow: 1px 1px 0px #3a00df;
background: linear-gradient(170deg, #359eff 5%, #3a00df 95%);
`;
render(
<Button width={60}>
Hello World
</Button>
);
它看起来还不错,但我们得到的反馈是点击目标在移动设备上太小了:行业准则是互动元素的高度应该在44px到48px之间。我们需要提高尺寸,使其在手机上更容易点击。
让我们走过一个可能的解决方案,不使用CSS变量。
const Button = styled.button`
/* Omitted other styles for brevity */
min-height: 32px;
@media (pointer: coarse) {
min-height: 48px;
}
`;
关于这段代码,有两个简单的说明。
-
我使用
min-height
,而不是height
,这样按钮就可以在需要时增长。如果用户调大了他们的默认字体大小,或者文本必须换行,就会发生这种情况。 -
我没有使用基于宽度的媒体查询,而是使用
pointer: coarse
。这个媒体查询跟踪用户的主要输入机制是精细的还是粗糙的。鼠标或触控板被认为是 "精细 "的,因为你可以控制它的位置到像素。而手指或Wii遥控器则不那么精确。我们实际上并不关心屏幕是什么尺寸,我们关心的是他们是否能精确地点击或敲击。
我们将这一变化装船,我们知道我们已经改善了我们的应用程序的可用性,就会睡得好一点。
然而,我们很快就知道,我们的工作还没有完成。在我们的应用程序中,按钮并不是唯一可触摸的元素。还有文本输入,以及其他。
让我们也来更新我们的TextInput
组件。为了保持干燥,我们将在我们的主题上存储我们的尺寸。
const App = ({ children }) => {
return (
<ThemeProvider
theme={{
colors: COLORS,
coarseTapHeight: 48,
fineTapHeight: 32,
}}
>
{children}
</ThemeProvider>
);
};
我们在我们的两个组件中都使用这些值。
const Button = styled.button`
min-height: ${(props) => props.theme.fineTapHeight}px;
@media (pointer: coarse) {
min-height: ${(props) => props.theme.coarseTapHeight}px;
}
`;
const TextInput = styled.input`
min-height: ${(props) => props.theme.fineTapHeight}px;
@media (pointer: coarse) {
min-height: ${(props) => props.theme.coarseTapHeight}px;
}
`;
这对任何可触摸的元素来说都是很大的一块CSS的拖累。
现在,我们可以用很多方法来解决这个问题,比如使用样式化组件混合器或CSS类。但我认为解决这个问题的最好方法是使用CSS变量。
与其硬性规定每个组件在不同的断点上应该如何响应,不如我们给它传递一个反应式变量,为我们跟踪它,这样会怎么样?
const GlobalStyles = createGlobalStyle`
html {
--min-tap-target-height: 32px;
@media (pointer: coarse) {
--min-tap-target-height: 48px;
}
}
`;
有了这个神奇的CSS变量,我们的响应式组件就变得非常简单了。
const Button = styled.button`
min-height: var(--min-tap-target-height);
`;
const TextInput = styled.input`
min-height: var(--min-tap-target-height);
`;
我首先要承认:你第一次看到这种模式时,它看起来有点古怪。它需要一个心理模式的转变。但在我看来,它是非常有说服力的。
我们都习惯于使用CSS媒体查询来在不同的情况下应用不同的CSS块。但是,如果我们保持CSS属性不变,而改变其值呢?
在我们的组件里面,min-height
总是指向同一个值,--min-tap-target-height
,但这个值是动态的。
让我们看看其中的一些好处。
-
通过将断点的东西整合到一个地方,我们现在有了一个单一的真理来源。以前,不听话的开发人员可能会不小心删除其中一个断点,从而导致不一致的行为。现在它被打包成了一个有弹性的变量。
-
它让我们更清楚地了解我们为什么要这样做。我们给它起了一个名字--
min-tap-target-height
--它传达了我们为什么需要首先设置一个min-height
。 -
这是更明确的声明!我们不是指定每个组件应该如何基于指针类型而改变,而是告诉它值应该是什么。
最小知识原则 "是指代码应该只访问与其直接相邻的东西,而不是 "伸手 "到代码库中完全不同的部分。我觉得如果我们稍稍眯起眼睛,这个想法也适用于此。
另一个简单的例子:我们可以为我们的主题变量做同样的技巧,这样每个视口都有自己的比例。
const GlobalStyles = createGlobalStyle`
html {
--space-sm: 8px;
--space-md: 16px;
@media (min-width: 1024px) {
--space-sm: 16px;
--space-md: 32px;
}
}
`;
// Elsewhere...
const Paragraph = styled.p`
padding: var(--space-sm);
`;
而不是每个窗口大小都有相同的比例,我们可以为每个断点设置自定义比例。这将导致更一致的用户界面,并减少在每个单独组件中的麻烦。
其他新的可能性
到目前为止,我们所谈的都是关于开发者的体验。我们已经研究了解决问题的其他方法。
现在让我们来看看CSS变量所能解决的一些问题,从而改善用户体验。
将任何属性制成动画
有一些CSS属性根本就不能被动画化。例如,如果你曾经试图对线性或径向渐变进行动画处理,你会很快意识到这是行不通的。
使用CSS变量,你可以对任何属性进行动画处理,因为你不是对属性应用过渡,而是对值应用过渡。
例如,这里有一个有趣的渐变动画,通过CSS变量可以实现。
Swirly!
在撰写本文时,这个动画在Firefox和Safari中不工作。CSS变量已被广泛支持,但各浏览器仍在实现对其应用过渡的能力。这是CSS Houdini的一部分,这是一项长达数年的大规模努力,旨在将CSS引擎暴露给开发者。
"黑暗模式 "闪光修复
如果你试图实现一个 "黑暗模式 "的变体,你可能已经被这种棘手的情况咬过了:在短暂的瞬间,错误的颜色会闪现。
当页面加载时,"光模式的闪光"。
"黑暗模式 "出乎意料地棘手,尤其是在服务器渲染的情况下(如使用Next.js或Gatsby)。问题是,HTML在到达用户的设备之前就已经生成了,所以没有办法知道用户喜欢哪种颜色的主题。
获取和设置
在上面的例子中,我们在一个GlobalStyles
组件中硬编码我们的主题值。
const GlobalStyles = createGlobalStyle`
html {
--color-text: black;
--color-background: white;
--color-primary: rebeccapurple;
}
`;
可能有的时候,你需要在JavaScript中访问这些值。
如果你愿意,你可以继续将它们存储在一个constants.js
文件中。它们将被用于实例化CSS变量,但也可以在你需要原始值的地方导入JS。
const GlobalStyles = createGlobalStyle`
html {
--color-text: ${COLORS.text};
--color-background: ${COLORS.background};
--color-primary: ${COLORS.primary};
}
`;
另一个想法是使用CSS作为真理的来源。你可以用一点JS来访问这些值。
getComputedStyle(document.documentElement)
.getPropertyValue('--color-primary');
你也可以从JS中设置这些值。
// 🚨 This is an editable code snippet!! 🚨
// Try changing the value of `color-primary`,
// and check out the site's logo in the top left!
document.documentElement.style.setProperty(
'--color-primary',
'hsl(245deg, 100%, 60%)'
);
从JS中获取和设置CSS变量是一个逃生通道。你可能会惊讶于你很少需要它!你甚至可以在JS中使用CSS变量。你甚至可以在嵌入式SVG中使用CSS变量😮。
缺点
没有类型
为主题使用CSS变量的最大缺点可能是没有办法静态地输入它们(通过Typescript或Flow)。
在我看来,这并不是什么大问题;我在这条河的两岸都呆过。有一个类型化的主题对象很好,但我不能说它为我节省了大量的时间。一般来说,当你输入错误的CSS变量的名称时,是很明显的,而且是快速修复。
我认为在你的网站上运行编译时检查很重要,但我认为像Chromatic这样的工具是更可靠的检查。它们在CI中运行,并捕获渲染的视觉输出中的任何差异。
也就是说,如果类型安全是必须的,你不需要放弃它!你只需要保持你的类型安全。你只需要把你的样式保存在JS对象中,然后把它们插进去。
浏览器支持
CSS变量在4个主要浏览器中享有健康的浏览器支持,但缺少IE支持。
不太宽松
在使用样式化组件时,你可以把变量放在任何你想放的地方,包括媒体查询中。
const Ymca = styled.abbr`
font-size: 1rem;
@media (max-width: ${(p) => p.bp.desktop}) {
font-size: 1.25rem;
}
`;
CSS变量不能在媒体查询中的任何地方使用。有一种说法是让用户用env()
来描述他们自己的环境变量,这样就可以做到这一点......但这还没有实现。
尽管有这些缺点,但CSS变量还是打开了很多非常有趣的大门。我已经使用它们好几年了(包括在这个博客上!),我喜欢它们。它们是我管理动态样式的首选方式。
圣洁的三位一体
作为网络开发人员,我们使用3种主要技术。HTML、CSS和JS。为了有效地工作,我们需要对这三种技术都感到满意。
我认识很多开发人员,他们对HTML和JS有相当扎实的掌握,但对CSS却很吃力。我自己就属于这个群体,有好几年了。
CSS是一种具有欺骗性的复杂语言。基础知识可以快速和(相对)容易地学会,但它是一种非常难以掌握的语言。
当我们写CSS时,我们只看到了冰山一角。它是一种非常隐蔽的语言。各种各样的秘密机制跃然纸上,以令人惊讶、难以捉摸的方式调整我们的CSS属性的效果。
CSS没有错误信息或控制台日志。我们写CSS,结果要么是我们所期望的,要么不是。当事情出错时,会让我们感到无能为力。我们把随机的CSS属性扔到墙上,希望其中有一个能达到我们想要的效果。