学习面向React开发者的CSS变量

1,061 阅读12分钟

简介

这是个有争议的观点,但我相当喜欢CSS-in-JS。😬🚨

但是!我也非常喜欢CSS。我不认为使用CSS-in-JS可以免除你对CSS的学习。无论怎样,你都是在写CSS!它只是被包装得有点不同。

无论你在哪里写CSS,你都有必要掌握这种语言。熟练掌握CSS将使你成为一个更有效的前端开发者。

在本教程中,我们将看到如何利用CSS中最令人兴奋的新发展之一。CSS变量,又称自定义属性。我们将看到如何在我们的React应用程序中使用它们来改善我们的工作流程,并做一些非常花哨的事情。

为什么?

作为一个React开发者,你可能会想,你不需要CSS中的变量。你有一个完整的JavaScript引擎供你使用!

在你的React应用中改用CSS变量有两个原因。

  1. 人机工程学很好。

  2. 它开启了新的可能性!你可以用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-sizecolor 。当我们在一个元素上定义一个变量时,该元素的所有子元素都可以使用。

许多开发者认为,CSS变量是全局性的,但这并不完全正确。请看这个。

Code Playground

<style>
  p {
    --highlight-color: yellow;
  }

  em {
    background: var(--highlight-color);
  }
</style>

<h1>
  This heading has some <em>emphasized text</em>!
</h1>

这个标题中的embackground 设置为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属性扔到墙上,希望其中有一个能达到我们想要的效果。