08-进阶篇:变量计算
juejin_logo copyCreated with Sketch.

前言

在重构个人官网时,不知为何突然喜欢用上CSS变量,可能其自身隐藏的魅力,让我对其刮目相看,所以也将其纳入本课程的使用范围,也是唯一一章需集合JS操作。

变量

CSS变量又称CSS自定义属性,指可在整个文档中重复使用的值。它由自定义属性--var与函数var()组成,var()用于引用自定义属性。谈到为何会在CSS中使用变量,以下使用一个示例讲述。

/* 不使用变量 */
.title {
	background-color: red;
}
.desc {
	background-color: red;
}

/* 使用变量 */
:root {
	--bg-color: red;
}
.title {
	background-color: var(--bg-color);
}
.desc {
	background-color: var(--bg-color);
}

看完可能觉得使用变量的代码会多了一点,但有无想到突然某天万恶的策划小哥哥与设计小姐姐说要做一个换肤功能。根据平时思路,估计有些同学就会根据默认颜色主题增加一份对照的新颜色主题样式文件。这样每次增加需求都同时维护几套颜色主题,多麻烦啊!

此时变量就派上用场,提前跟设计小姐姐规范好各种需变换的颜色并使用变量定义,通过JS批量操作这些定义好的变量即可,这也是颜色主题切换的一种解决方案,好处在于只需维护一套CSS代码

<button id="red-theme-bt">切换红色主题</button>
<button id="blue-theme-bt">切换蓝色主题</button>
<button id="green-theme-bt">切换绿色主题</button>
["red", "blue", "green"].forEach(v => {
	const btn = document.getElementById(`${v}-theme-btn`);
	btn.addEventListener("click", () => document.body.style.setProperty("--bg-color", v));
});

CSS使用变量有如下好处。

  • 减少样式代码的重复性
  • 增加样式代码的扩展性
  • 提高样式代码的灵活性
  • 增加一种CSSJS的通讯方式
  • 无需深层遍历DOM改变某个样式

可能有些同学会问,ScssLess早就实现变量该特性,何必再多此一举?可是细想下,变量对比Scss变量Less变量又有它的过人之处。

  • 浏览器原生特性,无需经过任何转译可直接运行
  • DOM对象一员,极大便利CSSJS间的联系

认识

本来打算用一半篇幅讲述变量的规范与用法,网上一搜一大把就感觉无必要在此详细讲述了,贴上阮一峰老师的教程《CSS变量教程》。同时我也对变量的细节做一个整理,方便记忆。

  • 声明:--变量名
  • 读取:var(--变量名, 默认值)
  • 类型
    • 普通:只能用作属性值不能用作属性名
    • 字符:与字符串拼接,"Hello, "var(--name)
    • 数值:使用calc()与数值单位连用,var(--width) * 10px
  • 作用域
    • 范围:在当前节点块作用域及其子节点块作用域中有效
    • 优先级别:内联样式 > ID选择器 > 类选择器 = 伪类选择器 = 属性选择器 > 标签选择器 = 伪元素选择器 > 通配选择器 = 兄弟选择器 = 后代选择器

接着使用几个特别的场景展示变量的魅力。还是那句话,一样东西有使用的场景,那自然就会有它的价值,那用的人也会越来越多。

场景

其实变量有一个特别好用的场景,就是结合List集合使用。若不明白这是什么,请继续往下看。

条形加载条

一个条形加载条通常由几条线条组成,每条线条相应一个存在不同时延的相同动画,通过时间差运行相同动画,以产生加载效果。估计很多同学可能会把代码编写为以下形式。

条形加载条

<ul class="strip-loading">
	<li></li>
	<li></li>
	<li></li>
	<li></li>
	<li></li>
	<li></li>
</ul>
.strip-loading {
	display: flex;
	justify-content: center;
	align-items: center;
	width: 200px;
	height: 200px;
	li {
		border-radius: 3px;
		width: 6px;
		height: 30px;
		background-color: #f66;
		animation: beat 1s ease-in-out infinite;
		& + li {
			margin-left: 5px;
		}
		&:nth-child(2) {
			animation-delay: 200ms;
		}
		&:nth-child(3) {
			animation-delay: 400ms;
		}
		&:nth-child(4) {
			animation-delay: 600ms;
		}
		&:nth-child(5) {
			animation-delay: 800ms;
		}
		&:nth-child(6) {
			animation-delay: 1s;
		}
	}
}
@keyframes beat {
	0%,
	100% {
		transform: scaleY(1);
	}
	50% {
		transform: scaleY(.5);
	}
}

分析代码发现,每个<li>只是animation-delay不同,其余代码则完全相同,换成其他类似的List集合,那岂不是有十个<li>就写十个:nth-child(n)。显然这种方式不灵活也不易于封装成组件,若能像JS那样封装为一个函数并根据参数输出不同样式效果,那就更棒了。说到这里,很明显就是为了铺垫变量的技巧了。

对于HTML部分的修改,让每个<li>拥有一个自己作用域中的变量。对于CSS部分的修改,需分析哪些属性是随着index递增而发生规律变化的,对规律变化的部分使用变量表达式代替。

<ul class="strip-loading">
	<li style="--line-index: 1;"></li>
	<li style="--line-index: 2;"></li>
	<li style="--line-index: 3;"></li>
	<li style="--line-index: 4;"></li>
	<li style="--line-index: 5;"></li>
	<li style="--line-index: 6;"></li>
</ul>
.strip-loading {
	display: flex;
	justify-content: center;
	align-items: center;
	width: 200px;
	height: 200px;
	li {
		--time: calc((var(--line-index) - 1) * 200ms);
		border-radius: 3px;
		width: 6px;
		height: 30px;
		background-color: #f66;
		animation: beat 1.5s ease-in-out var(--time) infinite;
		& + li {
			margin-left: 5px;
		}
	}
}
@keyframes beat {
	0%,
	100% {
		transform: scaleY(1);
	}
	50% {
		transform: scaleY(.5);
	}
}

变量--line-index--time使每个<li>拥有一个属于自己的作用域。例如第二个<li>--line-index的值为2--time的计算值为200ms,换成第三个<li>后这两个值又会不同。

这就是变量的作用范围所致(在当前节点块作用域及其子节点块作用域中有效),因此在.strip-loading的块作用域中调用--line-index是无效的。

/* flex属性无效 */
.strip-loading {
	display: flex;
	align-items: center;
	flex: var(--line-index);
}

通过妙用变量,把代码从41行缩减到27行,对于那些含有List集合越多的场景,效果就越明显。这样处理也更美观更易维护,某天说加载效果的时间差不明显,直接将calc((var(--line-index) - 1) * 200ms)中的200ms调整为400ms,就无需修改每个:nth-child(n)了。

心形加载条

前段时间刷掘金看到陈大鱼头兄的心形加载条,觉得很漂亮,很带感觉。

鱼兄心形加载条

通过动图分析,发现每条线条的背景颜色与动画时延不同,另外动画运行时的高度也不同。细心的你可能还会发现,第一条与第九条的高度一样,第二条与第八条的高度一样,依次类推,得到高度变换相同类的公式:对称index = 总数 + 1 - index

背景颜色使用滤镜的色相旋转hue-rotate(),目的是为了使颜色过渡得更自然;动画时延与上述条形加载条一样。以下就用变量根据看到的动图实现一番。

<div class="heart-loading">
	<ul style="--line-count: 9;">
		<li class="line-1" style="--line-index: 1;"></li>
		<li class="line-2" style="--line-index: 2;"></li>
		<li class="line-3" style="--line-index: 3;"></li>
		<li class="line-4" style="--line-index: 4;"></li>
		<li class="line-5" style="--line-index: 5;"></li>
		<li class="line-6" style="--line-index: 6;"></li>
		<li class="line-7" style="--line-index: 7;"></li>
		<li class="line-8" style="--line-index: 8;"></li>
		<li class="line-9" style="--line-index: 9;"></li>
	</ul>
</div>
.heart-loading {
	display: flex;
	justify-content: center;
	align-items: center;
	width: 200px;
	height: 200px;
	ul {
		display: flex;
		justify-content: space-between;
		width: 150px;
		height: 10px;
	}
	li {
		--angle: calc(var(--line-index) / var(--line-count) * .5turn);
		--time: calc((var(--line-index) - 1) * 40ms);
		border-radius: 5px;
		width: 10px;
		height: 10px;
		background-color: #3c9;
		filter: hue-rotate(var(--angle));
		animation-duration: 1s;
		animation-delay: var(--time);
		animation-iteration-count: infinite;
		&.line-1,
		&.line-9 {
			animation-name: beat-1;
		}
		&.line-2,
		&.line-8 {
			animation-name: beat-2;
		}
		&.line-3,
		&.line-7 {
			animation-name: beat-3;
		}
		&.line-4,
		&.line-6 {
			animation-name: beat-4;
		}
		&.line-5 {
			animation-name: beat-5;
		}
	}
}
@keyframes beat-1 {
	0%,
	10%,
	90%,
	100% {
		height: 10px;
	}
	45%,
	55% {
		height: 30px;
		transform: translate3d(0, -15px, 0);
	}
}
@keyframes beat-2 {
	0%,
	10%,
	90%,
	100% {
		height: 10px;
	}
	45%,
	55% {
		height: 60px;
		transform: translate3d(0, -30px, 0);
	}
}
@keyframes beat-3 {
	0%,
	10%,
	90%,
	100% {
		height: 10px;
	}
	45%,
	55% {
		height: 80px;
		transform: translate3d(0, -40px, 0);
	}
}
@keyframes beat-4 {
	0%,
	10%,
	90%,
	100% {
		height: 10px;
	}
	45%,
	55% {
		height: 90px;
		transform: translate3d(0, -30px, 0);
	}
}
@keyframes beat-5 {
	0%,
	10%,
	90%,
	100% {
		height: 10px;
	}
	45%,
	55% {
		height: 90px;
		transform: translate3d(0, -20px, 0);
	}
}

一波操作后就有了以下效果。与陈大鱼头兄的心形加载条对比,颜色、波动曲线和跳动频率有点不同,在暖色调的蔓延与肾上腺素的飙升下,这是一种心动的感觉。

心形加载条

标签导航

上述通过两个加载条演示了变量在CSS中的应用以及一些妙用技巧,现在通过标签导航演示变量在JS中的应用。

JS中有三个操作变量的API

  • 读取变量:elem.style.getPropertyValue()
  • 设置变量:elem.style.setProperty()
  • 删除变量:elem.style.removeProperty()

先上效果图,效果中主要是使用变量标记每个标签的背景颜色切换标签的显示状态

标签导航

<div class="tab-navbar">
	<nav>
		<label class="active">标题1</label>
		<label>标题2</label>
		<label>标题3</label>
		<label>标题4</label>
	</nav>
	<main>
		<ul style="--tab-count: 4;">
			<li style="--bg-color: #f66;">内容1</li>
			<li style="--bg-color: #66f;">内容2</li>
			<li style="--bg-color: #f90;">内容3</li>
			<li style="--bg-color: #09f;">内容4</li>
		</ul>
	</main>
</div>
.tab-navbar {
	display: flex;
	overflow: hidden;
	flex-direction: column-reverse;
	border-radius: 10px;
	width: 300px;
	height: 400px;
	nav {
		display: flex;
		height: 40px;
		background-color: #f0f0f0;
		line-height: 40px;
		text-align: center;
		label {
			flex: 1;
			cursor: pointer;
			transition: all 300ms;
			&.active {
				background-color: #3c9;
				color: #fff;
			}
		}
	}
	main {
		flex: 1;
		ul {
			--tab-index: 0;
			--tab-width: calc(var(--tab-count) * 100%);
			--tab-move: calc(var(--tab-index) / var(--tab-count) * -100%);
			display: flex;
			flex-wrap: nowrap;
			width: var(--tab-width);
			height: 100%;
			transform: translate3d(var(--tab-move), 0, 0);
			transition: all 300ms;
		}
		li {
			display: flex;
			justify-content: center;
			align-items: center;
			flex: 1;
			background-color: var(--bg-color);
			font-weight: bold;
			font-size: 20px;
			color: #fff;
		}
	}
}
const navs = document.querySelectorAll(".tab-navbar nav label");
const tabs = document.querySelector(".tab-navbar main ul");

navs.forEach((v, i) => v.addEventListener("click", e => {
	navs.forEach(v => v.classList[v === e.target ? "add" : "remove"]("active"));
	tabs.style.setProperty("--tab-index", i);
}));

<ul>中定义--tab-index表示标签当前的索引,当点击按钮时重置--tab-index的值,可实现移动<ul>的位置显示指定标签。可移动<ul>是因为定义了--tab-move,通过calc()计算--tab-index--tab-move的关系,以操控transform:translate3d()移动<ul>

另外在<li>中定义--bg-color表示标签的背景颜色,也是一种较简洁的模板赋值方式,总比写<li :style="backgroundColor:${color}">要好看。若多个属性依赖一个变量赋值,那使用变量赋值到style中就更方便了,那些属性可在css文件中计算与赋值,这样可帮助JS分担一些属性计算工作。

悬浮跟踪按钮

通过几个示例实践了变量在CSSJS中的应用,相信大家已掌握了其用法与技巧。曾经在某个网站看过一个酷炫的鼠标悬浮效果,好像也是使用变量实现的。我凭着记忆也使用变量实现一番。

其实思路也很简单,先对按钮布局与着色,然后使用伪元素标记鼠标的位置,定义--x--y表示伪元素在按钮中的坐标,通过JS获取鼠标在按钮中的offsetXoffsetY分别赋值给--x--y,再对伪元素添加径向渐变的背景颜色,大功告成,一个酷炫的鼠标悬浮跟踪效果就这样诞生了。

悬浮跟踪按钮

<a class="track-btn">
	<span>妙用CSS变量,让你的CSS变得更心动</span>
</a>
.track-btn {
	overflow: hidden;
	position: relative;
	border-radius: 25px;
	width: 400px;
	height: 50px;
	background-color: #66f;
	cursor: pointer;
	line-height: 50px;
	text-align: center;
	font-weight: bold;
	font-size: 18px;
	color: #fff;
	span {
		position: relative;
		pointer-events: none; // 不加会卡顿
	}
	&::before {
		--size: 0;
		position: absolute;
		left: var(--x);
		top: var(--y);
		width: var(--size);
		height: var(--size);
		background-image: radial-gradient(circle closest-side, #09f, transparent);
		content: "";
		transform: translate3d(-50%, -50%, 0);
		transition: width 200ms ease, height 200ms ease;
	}
	&:hover::before {
		--size: 400px;
	}
}
const btn = document.getElementsByClassName("track-btn")[0];
const btnStyle = btn.style;
btn.addEventListener("mousemove", e => {
	btnStyle.setProperty("--x", `${e.offsetX}px`);
	btnStyle.setProperty("--y", `${e.offsetY}px`);
});

其实可结合鼠标事件完成更多的酷炫效果,例如动画关联、事件响应等操作。无做不到只有想不到,尽情发挥你的想象力啦。

悬浮视差按钮

曾经在CodePen中还看到一个很不错的示例,一个悬浮视差按钮,具体代码涉及一些3D变换的知识。看完源码,根据其思路自己也实现一番,顺便对代码稍加改良。

悬浮视差按钮

兼容

对于现代浏览器来说,变量的兼容性其实还是挺好的,所以可放心使用。毕竟现在都是各大浏览器厂商快速迭代的时候,产品对于用户体验来说是占了很大比重,因此在条件允许的情况下还是大胆尝新,不要被一些过去的所谓的规范约束着。

变量兼容性

试问现在还有多少人愿意去维护IExplorer兼容性,若一个产品的用户体验受限于远古浏览器的压制,相信该产品也不会走得很远。

在完成一个产品时,不仅是为了完成工作任务,若在保证进度的同时能花点心思点缀,可能会有意外的收获。

通过循序渐进的方式探讨变量的应用与技巧,对于一个这么好用的特性,当然是不能放过啦。其实多多思考,就能把变量用在很多场景中。最后送给大家一个大大的彩蛋,一个暖心彩虹色调搭配的爱心点赞按钮。

点赞按钮

留言
Ctrl + Enter
全部评论(26)
晨ss的头像
删除
开发
calc(var(--tab-index) / var(--tab-count) * -100%)能解释下这句吗?当前点击第二个,2/4* -100%这个是怎么计算移动逻辑的呀
点赞
1
删除
我理解是,100%为整个内容区宽度,n * -100%为向左移动整个内容区宽度的 n 倍。点击标题三,--tab-index = 2,向左移动内容区的 2/4 倍,展示的自然为第三个内容区了。可以将tab-navbar的overflow: hidden;属性暂时关闭就能看出移动逻辑了
点赞
回复
ng_kp的头像
删除
前端开发 @ ONES
悬浮跟踪按钮 span {
position: relative;
pointer-events: none;
} , 如果span定位不是relative,hover产生的渐变效果会遮住文字,定位必须是realative才能正确显示,这是啥原理有大佬解答一下吗。
点赞
2
删除
只要在上面的层叠上下文中,span元素的层级比before伪元素高就可以了。上面能正确的显示原因是,before伪元素可以看成a元素的子元素,此时span元素和before元素同级,是兄弟元素,span元素在before元素后面,before元素为绝对定位,此时before元素脱离文档流,z-index为auto,也可将z-index的值看成是0,span元素为相对定位,没有脱离文档流,z-index为auto,也可将z-index的值看成是0。此时before元素和span元素的z-index相同都为0,但是before元素在前,span元素在后,那么span元素就会覆盖before元素了,此时hover产生的渐变效果会遮住文字了。这里span元素将它设置成absolute定位也可以,总之只要z-index比before元素高或相同即可,即 span元素的 z-index >= before元素的z-index。
展开
1
回复
删除
感谢解答🙏🙏
只要在上面的层叠上下文中,span元素的层级比before伪元素高就可以了。上面能正确的显示原因是,before伪元素可以看成a元素的子元素,此时span元素和before元素同级,是兄弟元素,span元素在before元素后面,before元素为绝对定位,此时before元素脱离文档流,z-index为auto,也可将z-index的值看成是0,span元素为相对定位,没有脱离文档流,z-index为auto,也可将z-index的值看成是0。此时before元素和span元素的z-index相同都为0,但是before元素在前,span元素在后,那么span元素就会覆盖before元素了,此时hover产生的渐变效果会遮住文字了。这里span元素将它设置成absolute定位也可以,总之只要z-index比before元素高或相同即可,即 span元素的 z-index >= before元素的z-index。
点赞
回复
prism的头像
删除
vue里面条形加载条直接写成这样:animation-delay: calc((var(--line-index)-1) * 200ms); 会报错
点赞
回复
佛系小玩家的头像
删除
我早就想问,把雅君娶回家了吗?
2
1
删除
居然还有熟人哈哈哈
点赞
回复
用户9576771680335的头像
删除
请问这两种js操作style的方法会触发重排吗? elem.style.setProperty() elem.style.removeProperty()
点赞
1
删除
(作者)
这两种可能会引发重绘,若处理的属性是外观属性就不会回流只会重绘
1
回复
种花家的进阶的头像
删除
coder
请问优先级别那边,为什么 内联样式 = 外联样式,通配选择器 = 后代选择器 = 兄弟选择器 呢?
点赞
2
删除
(作者)
这个是浏览器里规定的,若等级一样,就看出现顺序了
点赞
回复
删除
噢噢,谢谢
这个是浏览器里规定的,若等级一样,就看出现顺序了
点赞
回复
胖鹅黄彪的头像
删除
前端开发工程师
受教了😃
点赞
回复
ArthurCheng的头像
删除
好多骚操作
1
回复
Big0range的头像
删除
前端开发 @ 保密
悬浮跟踪按钮 a元素的class track-btn 少一个display:block属性
1
1
删除
(作者)
父元素已经flex化了
点赞
回复
saltine的头像
删除
前端打杂
学到了学到了😊
点赞
回复
Kiroro的头像
删除
web前端拷贝师
受益匪浅
1
回复
大地dadi的头像
删除
Web前端开发
这章干货满满啊
2
回复
console_man的头像
删除
Web前端
赞赞赞
点赞
回复
__漫长的头像
删除
前端 @ 字节跳动
厉害。
点赞
回复
欢乐症患者的头像
删除
前端中黑 @ 派大星的蟹黄堡
动画beat没有写在文章好像 去github上才看到
点赞
1
删除
(作者)
是的,考虑到这章的代码有点多就没写上去了,不过现在已加上了
点赞
回复
superJeff的头像
删除
Developer
很棒,又学到很多,感谢作者大大
1
1
删除
(作者)
谢谢支持
点赞
回复