头号作家解读 UI 框架样式修改:颜色主题配置与自定义变量

留学推荐2024-05-29 21:17:53佚名

专栏介绍

“顶级写手”是OPPO工程师解读最新热点技术的专栏,在这里你不仅可以看到最新最热的动态,还能和OPPO优秀的工程师一起学习技术知识。

顶级作家

■鲍勃

■一位在前端领域耕耘9年多的前端“新生代IT民工”,立志分享各种前端酷知识。

01

背景

相信用过 UI 框架的同学应该或多或少都修改过它的样式,最常见需要修改的样式应该就是:颜色。大多数情况下,官方框架可能会提供主题配置,比如主题颜色配置(使用 Sass 变量)或者 Ant 主题颜色配置(使用 Less 变量)。你可以定义 @-color(Ant) 或者 $--color-() 这样的变量来定制想要的主题颜色,比如 OPPO 绿色。

但相信大家都经历过这种情况,因为是借助了 CSS 预处理器的能力,而非原生的 CSS 支持,这不仅意味着需要打包工具的支持,也意味着这些变量并不是真正的“变量”。它们在打包时会被替换成 CSS 能够识别的颜色值,无法再通过改变其中一种 - 来改变页面上所有相关的颜色。对于只使用一套 OPPO 绿色的场景来说这还好,但如果要改变动态主题色怎么办?或者像夜间模式这种需要改变颜色的场景怎么办?这对于 CSS 来说其实并不容易。

使用 CSS 不是不可以,只是开发体验不好。比如可以为不同的主题设置不同的 class,并根据这些不同的 class 将所有使用颜色的地方重写。试想一下,业务 CSS 中有 20 个地方用到了主题颜色custom什么意思,每增加一套主题,就需要重写这 20 个地方的颜色定义。这其实也是我们之前常用的方法。

当然,也有办法将浏览器版的CSS预处理器嵌入到页面中,如果你已经在网上使用过这种方法,或者有这样的想法,请务必看完本文,思考是否还有其他方法。

遇到需要动态更换主题的同学肯定都搜索过并了解过CSS自定义变量的概念,如果你还没有用过,不用担心,看完你就懂了。

不知道大家有没有注意到,标题里有一个奇怪的---,和变量定义方式$--color-很像。那么这是什么呢?CSS自定义变量就是这么定义的。

02

基本定义

我们先来看一下基本的定义。CSS自定义变量由两部分组成 - 加一个名称。名称和变量名规则差不多,比如区分大小写(这个和普通的CSS属性名不同,CSS属性名一般不区分大小写),但不同之处在于CSS自定义变量名可以以数字开头,包括纯数字,例如-1就是合法的CSS自定义变量,甚至允许使用汉字,甚至允许使用表情符号。

custom什么意思_意思相近的两字词_意思相近的字组成词语

正如那句老话所说:你能做某件事并不意味着你应该做它。想想当你看到有人在别人的代码中写了 --1: 5px, --2: red,然后毫无理由地到处使用 --1 和 --2 时,试着表达你的感受。

其实 CSS 规范和 MDN 文档中都使用了“自定义属性”这个术语,但是“自定义属性”看起来不如“自定义变量”那么清晰吸引人,所以本文使用了“自定义变量”这个术语。请记住它对应的是“自定义属性”或者规范中提到的与变量相关的术语:“自定义变量”。

这里之所以可以是数字开头,也可以是纯数字,准确的说是因为前面有一个--,这个--不能算是一个变量名,因为实际的名字应该包含整个--。

但是,有了名字并不代表变量就可以用,需要赋值才有用。而且,CSS自定义变量必须存在于CSS的一定的元素规则定义中,不能像全局变量那样写在最外层。当然,全局变量也有相应的定义方式,后面会提到。所以CSS自定义变量的完整定义应该是:

:root {  --custom-variable: ;}
/* 举几个栗子 */html { --color-primary: green; --color-disabled: gray; --wide-border: 3px;}

除了在CSS文件、样式属性、对应方法中定义外,请注意,这也是在“某一元素规则的定义”中,因为它只对该属性所在的元素及其子元素有效。

03

使用自定义变量

用法

好的,现在我们有了一个有效的 CSS 自定义变量,如何使用它?CSS 定义了 var() 方法,例如,您可以使用 var(--color-) 来读取它。因此,可以像这样设置具有浅绿色背景的 div 的样式:

div {  background-color: var(--color-primary);}

var() 方法也支持默认值,当对应变量 或者 value 为 时,会读取默认值。定义很简单,用 隔开,后面的值就是默认值。例如 var(--color-,blue),当 --color- 或者 value 等于 时,会返回 blue。

但请注意,如果有多个逗号,则第一个逗号后面的整个值将作为默认值,而不是用逗号分隔。所以 var(--color-,blue,cyan) 的默认值是 blue, cyan。

在定义自定义变量的时候,还可以使用其他自定义变量。同时,var() 支持多层嵌套,因此默认值也可以是另一个自定义变量。例如以下形式:

div.bordered {  --color-border: var(--color-secondary, green);  /* 注意 --color-secondary 并未定义 */  border: var(--wide-border) var(--color-border) solid;  color: var(--color-text, var(--color-disabled, black));}

看到这里,大家应该知道怎么用了,但是为什么说了这么久,好像跟 CSS 预处理器的变量定义没什么区别。好啦,别着急,我先问大家,还记得 CSS 缩写里的 C 代表什么吗?没错,就是“”:“”。这个词也是 CSS 的独特魅力所在,也是最容易让人混淆的部分。不过,相信看文章的大家应该对 的优先级很熟悉了,如果不熟悉的话赶紧复习一下吧。为什么突然提到它呢?因为 CSS 自定义变量也遵循了 的概念。相同的变量定义会默认继承,优先级高的定义会覆盖优先级低的定义。这也是变量能“动态”改变其值的重要原因。

那么思考一下,如果前面所有的代码块都是在同一个页面中依次定义,而页面中有一个div和一个div,那么它们应该分别如何显示呢?

意思相近的两字词_意思相近的字组成词语_custom什么意思

了解了用法之后,假设你需要为这个页面添加多个主题样式,结合上面提到的级联,你想到怎么添加了吗?没错,我们可以用新样式覆盖变量:

.pink-theme {    --color-primary: pink;    --color-border: deeppink;}.gold-theme {    --color-primary: gold;    --color-border: goldenrod;}/* 还可以加更多 */

当然如果你不想添加class的话,也可以直接将样式写在对应的标签上来覆盖。

正如文章开头所说,这种动态改变 CSS 中变量的值的功能在 CSS 预处理器中是无法轻易做到的,因为它们的变量定义就像它们的名字一样,都是经过预先处理并放置在 CSS 中的。这也是 CSS 自定义变量相较于预处理器的一大优势。

全局变量

看完前面的使用部分,大家有没有搞清楚如何定义一个全局的 CSS 变量呢?因为自定义变量默认是被继承的,所以简单来说就是把样式放到覆盖范围最广的根元素上,也就是 HTML 中的元素上(其实一般情况下放在那里应该就可以了)。

上例中写了一个 :root 伪类,这个伪类也引用了根元素,在 HTML 中也引用了 html 元素,这有什么区别呢?比如下面的定义中,div 的背景应该是什么颜色呢?

:root {  --color-bg: red;}html {  --color-bg: blue;}div {  background-color: var(--color-bg);}

如果你看实际的页面,会发现是红色的。为什么呢?:root 是伪类,所以是类的优先级。回想一下优先级的定义,类的优先级要高于元素类型选择器的 html。

除了这种通过继承来增加覆盖率的形式之外,还有一种定义全局变量的方式,后面会提到。首先我们来看几个问题。

无效值

从上面的用法来看,一般情况下,CSS 自定义变量可以简单理解为在调用 var() 的地方将其值替换为文本,但在实现上还是有些区别的。抛开 CSS 解析计算过程的细节不谈,可以看作是 CSS 解析器忽略了使用自定义变量的属性值的语法检查,即不管该值本身是否可以在对应属性中使用。所以即使给自定义变量传递了非法值,这个属性还是会被正常解析,只是在计算值的时候会出错;这不同于直接写入非法值,CSS 解析器会提前检测到语法错误并忽略这条规则。

这样可能有点让人困惑,我们以MDN上的例子为例,做一个简单的扩展:

:root { --text-color: 16px; }p { color: blue; }p { color: var(--text-color); }
div { color: blue; }div { color: 16px; }

如果你打开 demo 页面,会发现第一行〈p〉是黑色的,而第二行〈div〉是蓝色的。这是因为在使用 CSS 自定义变量的〈p〉定义中,color:var(--text-color)被正常解析,覆盖了之前定义的color:blue(CSS 解析器一般会直接丢弃这个没用的规则定义)。然后在替换-text-color 的时候发现它不是一个值,不是合法的颜色,导致〈p〉元素的颜色定义被非法重置。因为color是继承的属性,所以〈p〉会首先尝试取继承的值。由于 demo 中上没有定义color,所以使用了继承的浏览器默认样式color:black。这里因为color:16px在解析过程中被浏览器认为是非法的而被忽略,所以选择了之前有效的定义color:blue。

custom什么意思_意思相近的两字词_意思相近的字组成词语

无效值重置和 CSS 全局关键字中 unset 的效果一致custom什么意思,即对继承的属性启用 unset 时相当于继承父级,对继承的属性禁用 unset 时相当于初始值。这三个字也是 CSS 的三个全局关键字。另外请注意,这三个关键字在使用自定义变量时也是有效的,将自定义变量值设置给它们时,就意味着对这个自定义变量执行 unset,以及操作:

:root {  --text-color: green;  color: red;}div {  --text-color: inherit;  color: var(--text-color);}

在上面的例子中,div的文字颜色会是绿色而不是红色,因为这意味着自定义变量是从父级继承下来的,也就是--text-color的值被计算为绿色,而不是用color:;替换后续color属性的文字。

另一个具有此功能的关键字可能是臭名昭著的 !。当用于自定义变量时,它还意味着自定义变量本身具有 ! 的优先级,而不是将其替换为文本。我不会为此写一个例子,我留给你自己去尝试。

因为不是文本替换,所以有些地方不能写自定义变量。比如不能写自定义变量来替换 CSS 属性名,也不能把值中的数字和单位分开留学之路,用自定义变量替换:

:root {  --property-name: padding;  --padding:10;}
p { var(--property-name):100px;/* 语法错误,忽略 */}p { padding:10px; padding:var(--padding)px; /* 正常解析但计算值失败,等同于 padding: unset,所以前一条规则会被覆盖 */}p.correct { padding: calc(var(--padding)*1px); /* 注意可以利用 calc() 来实现单位 */}

注意上面 p 的第二个定义 :10px; 是无效的,因为 var(--)px 在语法上不算无效,但实际计算时发现无法正确计算值,所以相当于 unset 了。不过如果你确实想只定义一个数字,并且在使用时加上单位,也可以使用例子中的 calc() 方法来实现。

虽然您不能像本例一样连接单元,但是您可以使用 CSS 自定义变量将属性值拆分为多个部分值,这将在后面的示例中提到。

循环引用

CSS 自定义变量允许你在 var() 内部嵌入另一个 var(),但不可避免地存在一个问题:

:root {  --margin:10px;}div {  --padding: calc(var(--margin)-10px);  margin:var(--margin);  padding:var(--padding);}div.cyclic {   --margin: calc(var(--padding)+10px);}

那么,div.的--和--该怎么解析呢?规范中定义,如果在同一个元素下发生循环引用,则本次循环内的所有自定义变量都等于该值,也就是值。

注意关键字“同一元素”,因为自定义变量默认是继承的,而继承行为发生在值计算之后,所以不同元素下的定义不一定构成循环引用,例如我们将最后一段定义改为div下的p:

:root {  --margin:20px;}div {  --padding: calc(var(--margin)-10px);  margin:var(--margin);  padding:var(--padding);  background:#f00;}div.cyclic p {   --margin: calc(var(--padding)+10px);   margin:var(--margin);   background:#0f0;}

在这个例子中,div.p的--可以正常计算得到20px,因为p在计算--时,--继承自div,而div已经被计算为10px了。

更多示例

前面的例子中提到,CSS 自定义变量在取值时基本相当于文本替换,没有类型的概念。这样的操作在某些场景下也能出乎意料的方便,比如 CSS-Trick 上提到的例子:

意思相近的字组成词语_custom什么意思_意思相近的两字词

button {  --h: 100;  --s: 50%;  --l: 50%;  --a: 1;    background: hsl(var(--h) var(--s) var(--l) / var(--a));}button:hover { /* Change the lightness on hover */  --l: 75%;}button:focus { /* Change the saturation on focus */  --s: 75%;}button[disabled] {  /* Make look disabled */  --s: 0%;  --a: 0.5;}

甚至有一种方法可以使用自定义变量作为开关,但请谨慎使用......

这里只是给出一些简单的使用示例,相信很多同学在实际工作中,包括我们的项目中,都有自定义变量的情况。

04

关于动画

自定义变量是 CSS 属性,因此可以在动画中使用。但是,由于它们没有类型,CSS 解析器不知道如何应用动画样式。效果不会像您预期的那样:

.color-div {  --angle: 0deg;  background: linear-gradient(var(--angle), red, yellow, blue, purple);  animation: rotate 5s ease-in-out both alternate infinite;}@keyframes rotate {  to {    --angle: 180deg;   }}

打开这个 demo 你会发现背景并没有“动”,只是颜色跳动了一下。其实这应该符合预期,毕竟渐变算是图片,本身不支持动画。

其实一眼就能看懂这段 CSS 代码想要表达什么,但是替换的方式让自定义变量支持放在过渡动画、动画样式中,其实并没有什么用。不过这么明显的问题,规范已经考虑到了,那么接下来我们看看他们是怎么解决这个问题的呢?

定义全局变量的另一种方法(注册)

鉴于CSS自定义变量的语法非常松散,无法定义其值类型、是否继承、及其初始值,这也导致无法很好实现动画等问题,CSS增加了@来定义,或者更准确的说,使用(注册)一个CSS自定义变量,可以设置其类型(或者其遵循的语法)、是否继承、及其初始值-value。结合上面的例子,我们来简单讲解一下@的用法以及一些需要注意的地方。前面的例子只要加上这个定义声明,就可以达到预期的效果了~

@property --angle { syntax: "";  inherits: false;  initial-value: ‘0deg’;}

同时@也有一个等价的接口可以调用,比如之前的声明,可以使用如下方式实现:

CSS.registerProperty({  name: '--angle',  syntax: '',  inherits: false,  initialValue: ‘0deg’;});

从上面的示例和解释来看,这些属性的含义可能非常简单。 但是,使用它们时需要注意以下几点:

●首先,与大多数@语句一样,@目前必须出现在CSS的最外层,不能嵌套在其他样式声明中,也不能位于最内层(但这种行为可能会改变,规范正在讨论中)。它可以嵌套在条件@语句中,例如@media:

:root {  @property --primary-color {    /*不会生效*/  }}
@media (min-width: 1200px) { @property --width { /* 有效 */ }}

●其次,所有属性都应设置,否则整个定义将被忽略。唯一的例外是,当属性为*时,-value可以留空。不过,带*的自定义变量的行为与普通定义基本相同。

●当需要填写 -value 时,其值必须符合定义的语法,并且必须是计算独立( )值,否则整个定义将被忽略。什么是“计算独立”?简单来说,就是不再依赖于其他 CSS 属性值。例如,1em 不符合条件,因为它依赖于 font-size 的定义,var(--other-) 也不起作用,但像 1px 和 #F00 这样的值是可以的。

@property --width {  syntax: "";  inherits: true;  initial-value: #f00; /* 不合定义语法的值,整个定义被忽略 */}@property --width {  syntax: "";  inherits: true;  initial-value: 1rem; /* 依赖根元素font-size,不符合条件,整个定义被忽略 */}

这里还要说一下-value,由于它的存在,var()的替换和之前略有不同,我们先来举一个上一篇文章中的例子:

:root { --text-color: 16px; }p { color: blue; }p { color: var(--text-color); }
div { color: blue; }div { color: 16px; }

在这个例子中,如前所述,由于计算值时出现错误,定义未设置,因此 〈p〉 的颜色变为黑色。但是如果我们添加定义:

@property --text-color {  syntax: "";  inherits: true;  initial-value: #f00;}

这种情况下,在计算值的时候, --text-color: 16px; 并不符合声明定义的语法,相当于 --text-color: unset;,所以结果为 --text-color: #f00; (即初始值),所以〈p〉 会显示为红色,而不是黑色。但请注意,验证是在计算 div 值时发生的,而不是在解析 CSS 时发生的,所以如果我们将 --text-color 的定义改为如下形式,效果还是一样:

:root {   --text-color: #0f0;   --text-color: 16px; /* 依旧是unset,而不是选择读取前一条,因为不是在解析时判断的语法错误 */}

由于种种原因,规范选择在解析时不做语法检查,所以即使计算时有语法检查,也和常规的解析错误不一致。如果这句话你还不太理解,回想一下前面错误检查的例子中,div文字是蓝色的。

另外要注意,对于有初始值的全局变量,当使用 var(--, ) 时,即使你手动将 --:; 替换为 var(--, ),也不会读取相应 -value 定义的初始值。

可以填写的类型有很多,也支持复合类型。前面的“”,或者“”和“>”等可以猜到的名字都是合法的值。详细列表请看这里。注意引号是必须的,因为这个值需要类型。

关于多个同名声明的优先级,如果CSS定义中存在同名的@声明,则最后一个生效;但如果存在多个同名声明,则其优先级最高,并且该方法不允许重复声明相同的变量名。

最后,目前没有办法取消注册变量,但规范提到可能会稍后添加此功能。

好了,本文到此结束。感谢您花时间阅读。希望您阅读完本文后对 CSS 自定义变量有更好的了解。

相关推荐

猜你喜欢

大家正在看

换一换