【回顾学习】::before与::after
发表于:2024-12-10 |

前言

在之前的文章当中,我提到过很多 js 可以实现的效果可以使用 CSS 的选择器来进行学习,对于 id 选择器,类选择器,标签选择器,子选择器,通配符选择器等相对而言都是比较熟悉的了,这里给大家介绍大家相对没有那么熟悉的伪类选择器。

::before 与::after 定义

我们简单先来了解一下这俩个伪类的定义

  1. CSS 中,::before 创建一个伪元素,其将成为匹配选中的元素的第一个子元素。常通过 content 属性来为一个元素添加修饰性的内容。此元素默认是行级的。

  2. ::after 会创建一个伪元素,作为所选元素的最后一个子元素。它通常用于为具有 content 属性的元素添加修饰内容。默认情况下,它是行向布局的。

  3. ::before 和 ::after 生成的伪元素是行级盒子,就好像它们是应用它们的元素或“源元素”的直接子元素,因此不能应用于替换元素(如 <img>),它们的内容在不受当前文档样式的影响的情况下被替换。

用法

在字符前后添加符号

我们给 p 标签的内容之前加上字,颜色设置为红色,后面加上:,颜色设置为蓝色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<style>
p::before {
content: "请";
color: red;
}
p::after {
content: ":";
color: blue;
}
</style>
<body>
<p>输入姓名</p>
<p>输入年龄</p>
</body>

可以看到,效果是这样的
效果图

添加 icon 字符

这个和上面的类似

1
2
3
4
5
6
7
8
9
10
11
12
<style>
p::before {
content: "😀";
}
p::after {
content: "😂";
}
</style>
<body>
<p>输入姓名</p>
<p>输入年龄</p>
</body>

效果图

添加图片

简单添加小图

不只是普通字符,我们还可以添加图片使用

1
2
3
p::before {
content: url(./assets/img.png);
}

效果图

我们可以进行位置的调整

1
2
3
4
5
p::before {
content: url(./assets/img.png);
position: relative;
top: 17px;
}

效果图

自定义大小图片

但是这样添加的图片,我们是没有办法调整大小的。因为伪类只是给了我们一个位置的信息。我们需要做的,就是将这个图片作为背景图片而不是普通的内容进行插入。一般建议直接一个 img 标签解决,别用这种方式,会被打。

1
2
3
4
5
6
7
8
9
10
11
p::before {
background-image: url(./assets/img.png);
background-size: 20px 20px; /* 设置背景图片大小 */
background-repeat: no-repeat; /* 避免重复平铺 */
position: relative;
display: inline-block;
top: 5px;
width: 20px; /* 这个宽度可以根据实际需求调整,和背景图尺寸配合 */
height: 20px; /* 高度同理 */
content: ""; /* 这里内容设置为空字符串 */
}

效果图

attr() 函数返回选择元素的属性值

这个可能用到的不多哈,比如我们想要显示标签中的属性,这里我把 a 标签的地址显示在百度两个字后面,将类名放在我是测试内容的前面

1
2
3
4
5
6
7
8
9
10
11
12
<style>
a::after {
content: "(" attr(href) ")";
}
.wrapper::before {
content: attr(class);
}
</style>
<body>
<a href="www.baidu.com">百度</a>
<div class="wrapper">我是测试的内容</div>
</body>

效果图

实现对话框效果

了解如何绘制三角形

实现这个效果之前,我们要先知道如何简单绘制三角形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<style>
#top-triangle {
width: 0px;
height: 0px;
border: 20px solid transparent;
border-bottom: 20px solid pink;
}
#right-triangle {
width: 0px;
height: 0px;
border: 20px solid transparent;
border-left: 20px solid pink;
}
#bottom-triangle {
width: 0px;
height: 0px;
border: 20px solid transparent;
border-top: 20px solid pink;
}
#left-triangle {
width: 0px;
height: 0px;
border: 20px solid transparent;
border-right: 20px solid pink;
}
</style>
<body>
<div id="top-triangle"></div>
<br />
<div id="bottom-triangle"></div>
<div id="right-triangle"></div>
<div id="left-triangle"></div>
</body>

效果图

我简单说一下这里的实现逻辑,首先是这三行代码

1
2
3
width: 0px;
height: 0px;
border: 20px solid transparent;

实际上他绘制了一个正方形的,如果我们把颜色加上,可以理解为这样的图形

1
2
3
4
5
6
7
8
9
10
11
12
13
<style>
#top-triangle {
width: 0px;
height: 0px;
border-left: 20px solid #333;
border-right: 20px solid pink;
border-top: 20px solid red;
border-bottom: 20px solid blue;
}
</style>
<body>
<div id="top-triangle"></div>
</body>

效果图

然后,我们来理解一下,为什么这样就会导致四个三角形。

边框绘制原理

当给一个元素设置 border(边框)属性时,从 CSS 的绘制机制来讲,每一条边框其实都是以一个梯形(或者特殊情况下是三角形)的形状来呈现的,四条边框组合起来围成了元素的矩形外框区域。

边框重叠与独立显示
每条边框的独立呈现:

每条边框(border-left、border-right、border-top、border-bottom)都是独立绘制的,它们会根据各自设置的宽度(这里都是 20px )和颜色来显示。比如 border-left 会沿着元素左边框所在直线绘制出一个三角形与梯形组合的形状(在宽度足够小等情况下接近三角形),其左边是一条斜边,右边是垂直于元素边界的直线,颜色为 #333 。同理,border-right 会沿着右边框绘制出相应形状,颜色为 pink;border-top 沿着上边框绘制,颜色为 red;border-bottom 沿着下边框绘制,颜色为 blue 。

视觉上形成四个三角形效果:

由于把元素的 width 和 height 都设置为 0px ,四条边框在汇聚到元素中心(也就是宽度和高度为 0 的这个点)时,各自的形状就独立地凸显出来了,视觉上就好像出现了四个独立的三角形,它们分别朝着不同的方向(上下左右),并且有着各自设置的颜色。

拓展-绘制直角三角形和矩形

在有了上面的知识作为底子之后,我们就可以实现绘制直角三角形和矩形了

直角三角形

首先来绘制直角三角形,这个很简单,我们把俩个三角形拼起来就行了

1
2
3
4
5
6
7
8
9
10
11
12
<style>
#top-triangle {
width: 0px;
height: 0px;
border: 20px solid transparent;
border-right: 20px solid pink;
border-bottom: 20px solid blue;
}
</style>
<body>
<div id="top-triangle"></div>
</body>

效果图

绘制梯形
1
2
3
4
5
6
7
8
9
10
11
<style>
#top-triangle {
width: 10px;
height: 0px;
border: 20px solid transparent;
border-bottom: 20px solid blue;
}
</style>
<body>
<div id="top-triangle"></div>
</body>

给图形加个宽度,汇聚的地方就会不一样了

效果图

在理解了这个原理之后,我们可以根据逻辑绘制更加奇形怪状的图形。这个就交给大家自由发挥了

实现对话框

ok,在了解了三角形绘制原理之后,那实现起来这个对话框就很简单了,一个矩形,拼一个三角形就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<style>
.left,
.right {
position: relative;
display: table;
min-height: 40px;
text-align: center;
background-color: #9eea6a;
margin: 0;
border-radius: 7px;
}
.left {
left: 10px;
}
.left::before,
.right::after {
position: absolute;
display: inline-block;
content: "";
width: 0px;
height: 0px;
border: 8px solid transparent;
top: 15px;
}
.left::before {
border-right-color: #9eea6a;
left: -16px;
}
.right::after {
border-left-color: #9eea6a;
right: -16px;
}
.left p,
.right p {
padding: 0px 10px;
}
.right {
right: -150px;
}
</style>
<body>
<div class="left">
<p>你好啊</p>
</div>
<div class="right">
<p>好久不见~</p>
</div>
</body>

效果图

绘制箭头

在有了上面三角形绘制的基础知识打底,我们还可以使用伪类来绘制箭头。逻辑很简单,绘制 border 的正方形,设置其中俩条边为透明,然后旋转一下,就实现了这样一个箭头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style>
.box {
width: 200px;
height: 50px;
position: relative;
background-color: pink;
}
.box::before {
content: "";
position: absolute;
width: 12px;
height: 12px;
border: 1px solid black;
border-bottom-color: transparent;
border-right-color: transparent;
transform: translate(-50%, -50%) rotate(-45deg);
left: 20%;
top: 50%;
}
</style>
<body>
<div class="box"></div>
</body>

效果图

清除浮动

这个以前面试可能会考,但是现在使用 float 的机会好像也不是很多了,就稍微提一嘴
原理:利用:after 和:before 在元素内部插入两个元素块,从而达到清除浮动的效果。

1
2
3
4
5
6
7
8
.outer:after {
clear: both; /*清除浮动*/
content: "";
display: block; /*显示伪元素*/
width: 0;
height: 0; /*不占位置*/
visibility: hidden; /*允许浏览器渲染它,但是不显示出来*/
}

进阶用法

伪类实现一个小组件

在明白了基础的::before 和::after 的用法之后,我们可以进阶玩一下。

比如最近我在搞大屏的时候,有个小组件,就是用到了伪类来进行实现的。先给大家看看效果

效果就是这样的:

这里除了那个中间的钱是图片,其他都是我用代码实现的,这个应该怎么实现呢,先别着急,我带着大家一步步实现这个小组件

搭建基本架子

代码基于 vue3 进行讲解,因为我现在公司技术栈用的就是 vue3 为主,个别项目用的 vue2(屎山老项目),小程序,h5 用的都是 uni-app。

父组件

简单调用一下子组件,把值传下去就好了

1
2
3
4
5
6
<template>
<BigScreenComponent title="本月充值金额" :number="20" unit="元" />
</template>
<script setup lang="ts">
import BigScreenComponent from "./components/index.vue";
</script>
子组件

我们先把架子搭一下,这里就是一个div,没啥好说的。

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="data-wrapper"></div>
</template>
<script setup lang="ts">
const props = defineProps<{
title: string;
number: number;
unit: string;
}>();
</script>
<style scoped lang="less"></style>

绘制蓝色的底色以及显示数字和单位

这里就是普通的 css,我就不多赘述了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<template>
<div class="data-wrapper-top">
<div class="data-wrapper-top-header">
<span class="number">
{{ number }}
</span>
<span class="unit">
{{ unit }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
title: string;
number: number;
unit: string;
}>();
</script>
<style scoped lang="less">
.data-wrapper-top {
height: 180px;
width: 172px;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 15px 15px 0 0;
background-image: linear-gradient(
to bottom right,
rgba(238, 248, 255, 0.01),
#d8eeff 100%
);
&-header {
padding-top: 30px;
color: #0064c4;
.number {
font-size: 24px;
}
.unit {
font-size: 14px;
}
}
}
</style>

效果图

绘制会动的金币效果

这个也是普通的 css3 动画而已,不多赘述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<template>
<div class="data-wrapper-top">
<div class="data-wrapper-top-header">
<span class="number">
{{ number }}
</span>
<span class="unit">
{{ unit }}
</span>
</div>
<div class="data-wrapper-top-icon">
<img class="img" :src="MONEY_IMG" />
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
title: string;
number: number;
unit: string;
}>();

const MONEY_IMG = new URL("@/assets/statics-money.png", import.meta.url).href;
</script>
<style scoped lang="less">
@keyframes skip {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
100% {
transform: translateY(0);
}
}

.data-wrapper-top {
height: 180px;
width: 172px;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 15px 15px 0 0;
background-image: linear-gradient(
to bottom right,
rgba(238, 248, 255, 0.01),
#d8eeff 100%
);
&-header {
padding-top: 30px;
color: #0064c4;
.number {
font-size: 24px;
}
.unit {
font-size: 14px;
}
}
&-icon {
margin-top: 50px;
.img {
animation: skip 1s infinite;
width: 60px;
}
}
}
</style>

效果图

伪类实现底部

先贴代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<template>
<div class="data-wrapper">
<div class="data-wrapper-top">
<div class="data-wrapper-top-header">
<span class="number">
{{ number }}
</span>
<span class="unit">
{{ unit }}
</span>
</div>
<div class="data-wrapper-top-icon">
<img class="img" :src="MONEY_IMG" />
</div>
</div>
<div class="data-wrapper-bottom">
<div class="weighted-cylinder">
<div class="weighted-title">
{{ title }}
</div>
<div class="weighted-cylinder-header"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
title: string;
number: number;
unit: string;
}>();

const MONEY_IMG = new URL("@/assets/statics-money.png", import.meta.url).href;
</script>
<style scoped lang="less">
@keyframes skip {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
100% {
transform: translateY(0);
}
}
.data-wrapper {
display: flex;
flex-direction: column;
&-top {
height: 180px;
width: 172px;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 15px 15px 0 0;
background-image: linear-gradient(
to bottom right,
rgba(238, 248, 255, 0.01),
#d8eeff 100%
);
&-header {
padding-top: 30px;
color: #0064c4;
.number {
font-size: 24px;
}
.unit {
font-size: 14px;
}
}
&-icon {
margin-top: 50px;
.img {
animation: skip 1s infinite;
width: 60px;
}
}
}
&-bottom {
width: 172px;
height: 30px;
.weighted-cylinder {
width: 100%;
height: 100%;
position: relative;
box-sizing: border-box;
padding: 0;
.weighted-title {
position: absolute;
left: 0;
right: 0;
transform: translateY(-50%);
text-align: center;
font-size: 14px;
bottom: -28px;
color: #fff;
z-index: 30;
}
.weighted-cylinder-header {
position: absolute;
left: 0;
right: 0;
top: 0;
z-index: 20;
height: 100%;
background: linear-gradient(to right, #00bbfd, #5279fe);
&::before,
&::after {
content: "";
position: absolute;
width: 100%;
height: 40px;
border-radius: 50%;
}
&::before {
z-index: 3;
top: -20px;
background: linear-gradient(to right, #dff8ff, #c6e6ff);
}
&::after {
z-index: 1;
bottom: -20px;
background: linear-gradient(to right, #00bbfd, #5279fe);
}
}
}
}
}
</style>
  1. 我们直接看 css 代码,首先我给底部设置了一个宽高,也就是
1
2
3
4
.data-wrappper-bottom {
width: 172px;
height: 30px;
}

然后我通过伪类,绘制了俩个圆,将内容拼了起来。
上面那个带一些阴影的浅色圆,就是通过::before绘制的。
效果图
而下面的圆部分,则是通过::after绘制的,这里我把::after代码去掉,效果就变成这样了
效果图

接下来我又通过定位的方式给 title 找到了适合它显示的位置。

拓展提升

此时,这个小组件的伪类部分就完成了,大屏一进来的数字是变化增长的,并且千分位显示的是带逗号的,这里我就给大家拓展一下如何实现。

我这里用的请求动画帧来完成这个变化,实现起来也很简单:

将显示数字的地方进行改变

先把显示数字的地方进行改变,后面陆续赋值

1
<span ref="numberRef" class="number"></span>
然后定义ref触发
1
2
3
4
5
6
7
8
9
10
11
12
const numberRef = ref<HTMLElement | null>(null);

watch(
() => props.number,
() => {
useNumberAnimation(0, props.number, numberRef.value);
},
);

onMounted(() => {
useNumberAnimation(0, props.number, numberRef.value);
});
定义方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export const useNumberAnimation=(startNumber: number, targetNumber: number, dom: HTMLElement)=> {
const number = targetNumber;
const numberDisplay = dom;
let currentNumber = startNumber || 0;
// 设置每次递增的步长, 递增的步长为当前数字的 1 / 3
const incrementStep = Math.max(number / 3, 1);
// 动画开始时间戳,用于计算经过时间
let startTime = 0;
let reqId: number;
const animate = (timestamp: number) => {
if (!startTime) {
startTime = timestamp;
}
// 计算经过的时间(单位:秒)
const elapsedTime = (timestamp - startTime) / 1000;
currentNumber += incrementStep * elapsedTime;
currentNumber = Math.round(currentNumber);
const showNumber = currentNumber.toLocaleString();
numberDisplay.innerText = showNumber.toString();

if (currentNumber < number) {
reqId = requestAnimationFrame(animate);
} else {
numberDisplay.innerText = number.toLocaleString();
cancelAnimationFrame(reqId);
}
};

reqId = requestAnimationFrame(animate);
}

简单讲解一下

  • toLocaleString() 这个就是js自带的千分位加逗号逻辑
  • requestAnimationFrame() 这个看我博客的小伙伴很熟悉,就是请求动画帧的逻辑
  • 核心逻辑:每次我加上一个数字,然后在超出的时候,赋值最终的数字,关闭请求动画帧逻辑

这样,就实现了这样一个大屏小组件的功能。

结语

本篇文章就到这里了,更多内容敬请期待,债见~

上一篇:
拖拽DOM和组件中的Popup拖拽
下一篇:
【可视化学习】96-从入门到放弃WebGL(二十三)