项目稳定性治理思考:十五个防御性CSS技能

分享十五个实用的、防御性 CSS 技能。

image-20220703113540863

文章转载自前端进阶指南

一、概念解释

防御性 CSS,防的是谁?我把他总结为:一切使表现和行为偏离预期效果的情景。出现这些场景的原因是因为终端环境的多样化,开发及测试用例只能覆盖大多数使用场景,在其他环境下,解析机制差异、内容动态变化等,都是导致非预期效果的原因。

二、防御的必要性

防御性 CSS 不仅仅是为了兼容其他少数场景,避免边界情况,更大的价值在于提升团队协作的可能性。防御性 CSS 的意义类似 JS 中的 try…catch, 他可能无法缩短需求开发的时间,但却是你程序正确运行和稳定运行的最后一道防线,更何况 JS 的错误只有在用户交互后才有感知,而 CSS 一旦出错,直接赤裸裸的展现在用户面前,直接影响用户的使用率和留存率。

都说编程风格分为三种:能跑就行风格、中规中矩风格、锦上添花风格。能跑就行风格代表的是:每一个设定和判断都和当次需求贴合的严丝合缝,如同山羊走钢丝,摇摇欲坠,但就是不倒,不得不令人称奇,但这种风格,不仅对编程人员要求极高,而且十分不利于团队协作,一旦意料意外的情景发生或者需求变更,带来的是雪崩式的改动;中规中矩风格概述为,该写注释的地方写注释,该写思路的地方写自己这么做的理由,该兜底处理的地方做拦截处理,程序的鲁棒性和可维护性直接拉满;万无一失风格更多的像是处女座,追求极致和完美,在中规中矩风格上再增一抹亮色,年轻时候的“雷布斯”就是典型代表。防御性 CSS 的目的就是从技术上尽可能的改变编程者能跑就行的侥幸心理,提升项目的可用性和可维护性。其目的也可以归纳为让你的项目做到:跑起来不出错,改起来不骂人。

防御性 CSS 的作用是对常规 CSS 的兜底,是实现项目稳定性建设重要但极其容易被忽视的一环。

三、防御技能

技能一:flex-wrap

属性背景:flex-wrap 是 flex 布局中的属性,其作用是控制 flex 容器内元素所占空间超出 flex 容器空间时是否折行。

防御原因:flex-wrap 属性默认是不折行,容易忽略多元素溢出兜底;为兜底,请设置 flex-wrap: wrap;

意外后果:内部元素被裁剪,或 flex 容器出现滚动条;

应用场景

1)开发中 flex 容器空间够用,但小尺寸屏幕会溢出;

2)内容由服务端下发,元素个数无法提前预支,超于预期时导致 flex 容器出现滚动条或内部元素被裁剪;

代码:

1
2
3
4
.options-list {
display: flex;
flex-wrap: wrap;
}

场景示例:

image-20220703112307377

技能二:margin 间距

属性背景:margin 作用是调整元素的外边距。用于指定元素与周围空间的距离关系。

防御原因:防止元素与元素之间挤压空间,造成重叠等情况;

意外后果:元素重叠或被挤压;

应用场景

1)内容所占空间无法保证与其他元素不存在挤压的场景;

代码:

1
2
3
.section__title {
margin-right: 1rem;
}

场景示例:

image-20220703112924176

技能三:长文本处理

背景:当文本长度超出容器时,该如何显示。

意外后果:文本折行,样式不统一;

应用场景

1)要求列表表现一致但文本长度不可控;

此处假设与设计最终商定,超出部分以省略显示,那么,意外兜底的样式代码为:

1
2
3
4
5
.username {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

场景示例:

image-20220703112957839

技能四:防止图像被拉伸或压缩

背景:通常,服务器下发的图片尺寸以及用户自定义上传的图片,显示在页面时,不可能百分百与容器尺寸贴合,不可避免的会遇到图片的放缩处理。

意外后果:图像被拉伸或压缩;

应用场景

1)服务端下发多种不确定尺寸的图片;

2)用户自定义上传图片且需要预览和编辑;

1
2
3
.card__thumb {
object-fit: cover;
}

demo 链接:https://monageju.github.io/Blog/object_fit.html

场景示例:

image-20220703113024894

技能五:锁定滚动链接

背景:overscroll-behavior 是 overscroll-behavior-x 和 overscroll-behavior-y 的简写属性,它控制的是元素滚动到边界时的表现。换个能听得懂的说法:在 JS 世界里,有事件冒泡机制,你可以通过 event 的 stopPropagation 方法去阻止冒泡的发生,同样,在 CSS 世界里,滚动也有冒泡机制,当内部元素滚动到边界时,如果继续滚动,会带动外层祖先元素发生滚动,这种现象被称为滚动链,为了方便记忆,你也可以把他形象的记忆为滚动冒泡。而 overscroll-behavior 这个属性,就是类似 event 的 stopPropagation 方法阻止冒泡事件一样,提供给开发者去控制内层元素是否可以发生”冒泡“带动外层元素滚动的属性。

意外后果:”滚动冒泡“ 或 ”滚动穿透“;

应用场景

1)页面存在多层滚动元素,需要单独控制每层滚动是否引起外层滚动;

1
2
3
4
.child {
overscroll-behavior-y: contain;
overflow-y: auto;
}

demo 链接:https://monageju.github.io/Blog/overscroll_behavior.html

场景示例:如 demo 链接示例

拓展

理解了 overscroll-behavior 属性的作用,现在我们来看点拓展的东西:

首先来看下 overscroll-behavior 的属性值有哪些:

overscroll-behavior 属性有 3 个值

auto - 默认。元素的滚动会传播给祖先元素。

contain - 阻止滚动链接。滚动不会传播给祖先,但会显示元素内的原生效果。例如,Android 上的炫光效果或 iOS 上的回弹效果,当用户触摸滚动边界时会通知用户。注意:overscroll-behavior: contain 在 html 元素上使用可防止滚动导航操作。

none - 和 contain 一样,但它也可以防止节点本身的滚动效果(例如 Android 炫光或 iOS 回弹)。

这里有两个效果:一是下拉刷新,二是炫光回弹,这里有个 demo 可以看到具体效果:链接传送门

下拉刷新是原生支持的功能,如果项目要求自定义下拉刷新效果,除了要考虑如何实现自定义,还要考虑如何去掉默认原生下拉刷新,否则就会出现两个并存的下拉刷新,而去掉原生的下拉刷新也很简单,只需要在 body 或 html 元素添加如下代码:

1
2
3
4
body {
/* 禁用滚动冒泡,但是依然可以进行下拉刷新和炫光和回弹效果以及滑动导航 */
overscroll-behavior-y: contain;
}

至于禁用炫光和回弹效果,其实是应用 overscroll-behavior 属性的 none 属性值,具体代码如下:

1
2
3
4
body {
/* 禁用默认的下拉刷新和炫光和回弹效果,但是依然可以进行滑动导航 */
overscroll-behavior-y: none;
}

除了上述描述的两个效果,其实还有一个效果:手势导航,如左滑退出及右滑前进功能;而如果要禁用手势导航,可以使用如下代码:

1
2
3
4
body {
/* 禁用滑动导航 */
overscroll-behavior-x: none;
}

技能六:CSS 变量默认值

背景:CSS 变量可以实现动态控制元素属性,但是当 CSS 变量未定义或无效时,造成变量值异常,此时,元素的样式将会脱离预期,而变量默认值可以实现异常兜底,保证变量值异常时页面依然能运行。需要额外说明的是,备用值并不是用于实现浏览器兼容性的。如果浏览器不支持 CSS 自定义属性,备用值也没什么用。它仅对支持 CSS 自定义属性的浏览器提供了一个备份机制,该机制仅当给定值未定义或是无效值的时候生效,函数的第一个参数是自定义属性的名称。如果提供了第二个参数,则表示备用值,当自定义属性值无效时生效。

意外后果:因失去宽高等变量值而不显示或变形;

应用场景

1
2
3
.item {
color: var(--my-var, red); /* Red if --my-var is not defined */
}

技能七:弹性元素尺寸 min-height / min-width

背景:当需求要求完整展示某个列表数据,但列表数据所占空间无法固定时,为避免部分内容过宽、过高突破固定空间破坏布局,可以使用弹性尺寸 min-_ 或者 max-_ , 这样能自动适应部分内容所占空间过大或过小带来的样式美观问题;

意外后果:占用空间过大或过小,破坏布局或不美观;

应用场景

1
2
3
.hero {
min-height: 350px;
}

场景示例:

image-20220703113103762

max-width 的使用场景:

如果对每一个元素使用固定的 width,则当内容空间大于容器尺寸时,将发生溢出,此时,需要使用 min-width 限制最小宽度,当超出尺寸时,能够实现自动适配。

技能八:被遗忘的 background-repeat

背景:使用图片作为容器的背景图,当容器的尺寸大于图片尺寸时,默认背景图会重复,如果你在开发中忽略了上述问题,则会出现背景图重复的问题;

场景示例:

image-20220703113126092

解决办法

代码如下 :

1
2
background-image: url('..');
background-repeat: no-repeat;

解决后效果:

image-20220703113153598

技能九:媒体查询 @media

背景:媒体查询的使用更像是 CSS 中的条件判断,它会根据你定义的条件,当条件满足时,条件内的样式生效;

举例:当屏幕的宽度小于 600px 时,body 背景色为红色;当屏幕宽度介于 600-800px 之间时,body 背景色为黄色;当屏幕宽度大于 800px 时,body 的背景色为蓝色;

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 将 body 的背景色设置为蓝色 */
body {
background-color: blue;
}

/* 在小于或等于 800 像素的屏幕上,将背景色设置为黄色 */
@media screen and (max-width: 800px) {
body {
background-color: yellow;
}
}

/* 在 600 像素或更小的屏幕上,将背景色设置为红色 */
@media screen and (max-width: 600px) {
body {
background-color: red;
}
}

demo 链接如下:https://monageju.github.io/Blog/media.html

技能十:图片上的文字

背景:当需要在图片上层展示文字时,如果图片加载失败,而外层容器的背景色和文字颜色接近,那么文字的展示效果就不理想;

举例:容器背景设置为黑色,图片为橙色,文字颜色为近黑色,当图片加载失败时,文字的背景色直接变为容器的背景色,文字与容器背景色重合,示例如下;

解决后效果:

image-20220703113215229

解决代码:

1
2
3
.card__img {
background-color: #fff;
}

至此,即使图片加载失败,图片上的问题依然可以正常显示;至于图片加载失败时左上角的“破图”标记,可以使用伪类进行遮挡美化;

技能十一:合理使用滚动条属性

背景:当容器的空间固定时,如果内容超出容器,为正常显示完所有内容,同时不扩展所占空间,会使用 overflow 属性控制超出部分自动滚动展示,同时给与滚动条样式提示有剩余内容,但如果该属性使用不当,会造成样式很难看;

举例:overflow 属性有两个作用很相近的属性值,一个是 scroll, 另一个是 auto; 这两个属性值都能实现当内容大于所占空间时滚动展示,不同点在于使用 scroll 属性无论内容是否超出容器空间,都会展示滚动条,而 auto 属性会分辩条件,内容超出时才会展示滚动条,为超出时则会自动隐藏,样式上较为美观;

解决代码:

1
2
3
.box {
overflow-y: auto;
}

场景示例:

image-20220703113233112

技能十二:预留滚动条空间,避免重排

背景:接技能十一,当我们正确使用了 overflow:auto 就万事大吉了吗?也不尽然。

设想这样一个场景:有一个宽度 100vw,高度为 100vh 的容器盒子,容器内展示商品卡片,滑动到页面底部时,触发滑动加载,当触发懒加载时,容器内商品卡片占用的高度已经超出 100vh,依据外层容器设置的 overflow:auto,内容超出时会展示滚动条,滚动条的出现,使得页面不得不给滚动条让出一定的宽度,这个切换的场景中,由于不得不给滚动条让位置,最外层的元素发生了元素宽度变化,产生了重排的效果,有没有可能避免这一次不必要的重排呢?答案是有的。

大家一定还记得 vue 的指令中有两个很相像的指令 v-if 和 v-show, 他们俩的原理和区别是什么?分别用在什么情景下?提醒到这,是不是有思路了?如果还没有,那也没关系,再提示一点点,既然要避免多余的一次重排,而滑动加载又不可避免,如果我一开始就预留好滚动条的位置,只是你看不见,到了滚动条应该出场的时候再让你看见,是不是就能避免不必要的重排了呢?现在再想想,这是不是就是 v-show 指令的设计原理?

CSS 中有一个 scrollbar-gutter 属性,当它的值设置为 stable 时,就能够实现上述的这种功能,代码如下:

1
2
3
.box {
scrollbar-gutter: stable;
}

举例

内容较短时预留滚动条空间,内容超出时显示滚动条;

image-20220703113258300

技能十三:图片最大宽度

背景:当给固定宽高容器设置背景图时,如果背景图尺寸超过容器宽高,图片会溢出,因此,最好在项目的 resetCss 中按照以下属性属性初始化:

1
2
3
4
img {
max-width: 100%;
object-fit: cover;
}

实例:

image-20220703113319483

技能十四:粘性定位

说明:position 的粘性定位指的是通过用户的滚动,元素的 position 属性在 position:relative 与 position:fixed 定位之间切换;这对于需要使用滚动吸顶的场景非常方便;是典型的依据业务场景推动 CSS 技术发展的典例;

技能十五:浏览器兼容性 CSS 请勿批量处理

说明:根据 W3C 标准,批量分组选择选择器,如果分组中,其中一个无效,那么整个选择器都将会失效。因此,在遇到浏览器兼容属性时,切勿批量组合书写;

实例:

如果是如下书写方式,则该选择器没有任何问题,因为该分组选择器的所有选择器都有效:

1
2
3
4
5
h1,
h2,
h3 {
font-family: sans-serif;
}

此时,它的作用等同于:

1
2
3
4
5
6
7
8
9
h1 {
font-family: sans-serif;
}
h2 {
font-family: sans-serif;
}
h3 {
font-family: sans-serif;
}

但如果,是下面这种情况就不同了:

1
2
3
4
input::-webkit-input-placeholder,
input:-moz-placeholder {
color: #222;
}

该选择器使用了分组选择器,在确定的某一个浏览器中,该分组中只有一个选择器有效,而其他选择都是失效状态,根据规则,整个分组选择器都将会失效,因此,正确的做法应该是分开写,代码如下:

1
2
3
4
5
6
7
input::-webkit-input-placeholder {
color: #222;
}

input:-moz-placeholder {
color: #222;
}

此时,其效果才是符合预期的。

四、结语

通常一个项目的稳定性指的都是逻辑层稳定和服务层稳定,CSS 是极其容易被忽视的一层;当项目发生线上故障时,逻辑层和服务器可以通过日志查询、抓包等手段定位,而 CSS 问题则只能凭借经验和项目所运行环境进行大致推断,极难快速准确定位问题。在稳定性建设时,CSS 的书写应该遵循“瞻前顾后”的防御性写法,尽可能的避免意外的边界情况,这才是防御性 CSS 的真实价值。