2.5 CSS新特性的渐进增强处理技巧

在CSS2时代,浏览器层出不穷的奇怪bug让CSS Hack技巧一度盛行,例如:

/* IE8+ */
display: table-cell;
/* IE7 */
*display: inline-block;
/* IE6 */
_display: inline;

这种利用语法错误实现浏览器判别的做法可以说是CSS历史上的一道奇观了,直到现在,还有很多开发者在区分更高版本的IE浏览器的时候,使用在CSS属性值后面加\0或者加\9的方法。现在的CSS世界已不同于过去,CSS特性的问题已经不在于渲染bug,更多的是浏览器支持与不支持的问题,所以上面这些做法已经过时,且没有意义,请不要再使用了。

如果你想渐进增强使用某些CSS新特性,可以看看本节介绍的几个技巧,它们足以应付各种各样的场景。在开始之前,为了让我的表述更简洁,有一些名词所表示的含义需要提前和大家说明。

IE浏览器:一直到IE11版本的所有IE浏览器。

Edge浏览器:专指Edge12~Edge18版本的浏览器。

Chromium Edge浏览器:使用Chromium作为核心的Edge浏览器,并且是Edge18之后的版本,版本号从76开始。

现代浏览器:使用Web标准渲染网站,不需要使用CSS Hack,拥有高性能,同时和CSS新特性与时俱进的浏览器。在本书中专指Chrome浏览器、Safari浏览器、Firefox浏览器、Opera浏览器和Chromium Edge浏览器。

IE9+浏览器:特指IE9及其以上版本的IE浏览器,以及所有Edge版本浏览器和所有现代浏览器。以此类推,IE10+浏览器、IE11+浏览器、Edge12+浏览器这些名词的含义也是类似的。

webkit浏览器:特指以webkit为渲染引擎,或者前身是webkit渲染引擎的浏览器。特指Chrome浏览器、Safari浏览器、Opera浏览器和Chromium Edge浏览器。

以上这些名词全书统一。

有很多CSS新特性是对现有Web特性的体验升级,我们直接使用这些CSS新特性就好了,不要担心兼容性问题。因为在支持的浏览器中体验更好,在不支持的浏览器中也就是保持原来的样子而已。例如很常见的border-radius、box-shadow、text-shadow、filter等与视觉表现相关的CSS属性,或者scroll-behavior、overscroll-behavior等交互体验增强的CSS属性,还有will-change等性能增强的CSS属性,都是可以直接使用的。

用常见的border-radius属性举例。我们经常会把用户头像设置成圆的,代码很简单:

img {
    border-radius: 50%;
}

对于不支持的浏览器怎么办呢?不需要做什么,放着就好了,矩形也挺好看的。

记住,做Web开发是没有必要让所有浏览器都显示得一模一样的,好的浏览器有更好的显示,糟糕的浏览器就只有普通的显示,这才是对用户更负责任的做法。

有时候我们想要渐进增强使用某些新特性,则可以在属性值语法上做文章,借助全新的属性值语法有效区分新旧浏览器。

举个例子,IE10+浏览器支持CSS动画属性animation,我们要实现加载效果就可以使用一个很小的PNG图片,再借助旋转动画。这个方法的优点是资源占用少,动画效果细腻。于是,我们的需求来了,IE9及其以下版本浏览器还是使用传统的GIF动图作为背景,IE10+浏览器则使用PNG背景图外加animation属性实现加载效果。这个需求的难点在于我们该如何区分IE9和IE10浏览器。大家千万不要再去找什么CSS Hack了,我们可以利用属性值的语法差异实现渐进增强效果。例如:

.icon-loading {
    display: inline-block;
    width: 30px; height: 30px;
    /* 所有浏览器识别 */
    background: url(./loading.gif);
    /* IE10+浏览器识别,覆盖上一行background声明 */
    background: url(./loading.png), linear-gradient(transparent, transparent);
    animation: spin 1s linear infinite;
}
@keyframes spin {
    from { transform: rotate(360deg); }
    to   { transform: rotate(0deg); }
}

关键的CSS代码就是上面加粗的部分。由于线性渐变函数linear-gradient()需要IE10+浏览器支持,因此,加粗的这行CSS声明在IE9浏览器中是无法识别的,IE9浏览器下的GIF背景图不会被PNG背景图覆盖。图2-6所示就是IE9模式下CSS样式的应用细节,可以看到background属性值和animation属性下方都有红色波浪线,这是无法识别的意思。

图2-6 IE9浏览器中CSS源码应用细节

图2-7所示则是IE9浏览器中的实时加载效果。

图2-7 IE9浏览器中GIF图像实现的加载效果示意

眼见为实,读者可以在浏览器中进入https://demo.cssworld.cn/new/2/5-1.php页面,或者扫描右侧的二维码查看效果。

类似的例子还有很多,例如,下拉浮层效果通过在IE9+浏览器中使用box-shadow盒阴影、在IE8等浏览器中使用border边框来实现:

.panel-x {
    /* 所有浏览器识别 */
    border: 1px solid #ddd;
    /* rgba() IE9+识别,覆盖上一行border声明 */
    border: 1px solid rgba(0,0,0,0);
    box-shadow: 2px 2px;
}

又如,下面这段代码既可以去除inline-block元素间的空白间隙,又能保持空格特性:

.space-size-zero {
    font-size: .1px;
    font-size: -webkit-calc(1px - 1px);
}

理论上讲,直接使用font-size:0就可以实现想要的效果,但是在IE浏览器中直接设置font-size:0会失去空格特性,如无法实现两端对齐效果等,因此只能设置成font-size:.1px,此时字号大小按照0px渲染,空格特性也保留了。但是,这种做法又带来另外一个问题,由于Chrome浏览器有一个12px的最小字号限制规则,因此font-size:.1px会按照font-size:12px渲染,怎么办呢?我们使用一个IE浏览器无法识别的语法就可以了,这里就使用了-webkit- calc(1px - 1px)。

又如,我们可以使用background-blend-mode属性让背景纹理更好看,但是IE/Edge浏览器均不支持这个CSS属性,那就退而求其次,使用资源开销较大的背景图片代替。技术方案有了,那如何区分IE/Edge浏览器呢?可以试试使用#RRGGBBAA色值(下面CSS代码中加粗的部分):

.background-pattern {
    background: url(./pattern.png);
    background: repeating-linear-gradient(...), repeating-linear-gradient(...), #00000000;
    background-blend-mode: multiply;
}

#00000000指透明度为0的黑色,也就是纯透明颜色。其不影响视觉表现,作用是让IE/Edge浏览器无法识别这行CSS声明,因为IE/Edge浏览器并不支持#RRGGBBAA色值语法。于是,IE/Edge浏览器会加载并渲染pattern.png,而其余浏览器则使用纯CSS绘制的带有混合模式效果的很美的纹理背景,效果如图2-8所示。

图2-8 带有混合模式效果的很美的纹理背景示意

眼见为实,读者可以在浏览器中进入https://demo.cssworld.cn/new/2/5-2.php页面,或者扫描右侧的二维码查看效果。

我们讲解的例子已经足够多了,但重要的不是例子本身,而是例子中的处理技巧和思维方式。大家在实际开发的时候,要是遇到类似的场景,可以想一想是不是可以借助属性值语法巧妙地解决浏览器的兼容性问题。

利用属性值的语法差异渐进增强使用CSS新特性固然精妙,但并不是所有CSS属性都可以这样使用。我们可以试试借助伪类或伪元素区分浏览器,其优点是可以一次性区分多个CSS属性,同时不会影响选择器的优先级。

1.IE浏览器、Edge浏览器和其他浏览器的区分

想要区分IE9+浏览器,可以使用IE9浏览器才开始支持的伪类或伪元素。例如,使用下面几个伪元素:

/* IE9+浏览器识别 */
_::before, .some-class {}
/* 或者 */
_::after, .some-class {}
/* 或者 */
_::selection, .some-class {}

或者下面几个伪类:

_:checked, .some-class {}
/* 或者 */
_:disabled, .some-class {}

之所以上面的写法可以有效地区分不同版本的浏览器,是因为CSS选择器语句中如果存在浏览器无法识别的伪类或伪元素,整个CSS规则集都会被忽略。

可能有开发者会对_::before或者_::selection前面的下划线的作用感到好奇。这个下划线是作为一个标签选择器用来占位的,本身不会产生任何匹配,因为我们的页面中没有标签名为下划线的元素。这里换成some-tag-hahaha::before或者some-tag-hahaha::selection,效果也是一样的,之所以使用下划线是因为可以节省字符数量,下划线只需要占用一个字符。

要想区分IE10+浏览器,可以使用从IE10才开始支持的与表单验证相关的伪类,比如:required、
:optional、:valid和:invalid。由于animation属性也是从IE10浏览器才开始支持的,因此,2.5.2节中出现的加载的例子的CSS代码也可以这么写:

.icon-loading {
    display: inline-block;
    width: 30px; height: 30px;
    background: url(./loading.gif);
}
/* IE10+浏览器识别 */
_:valid, .icon-loading {
    background: url(./loading.png);
    animation: spin 1s linear infinite;
}
@keyframes spin {
    from { transform: rotate(360deg); }
    to   { transform: rotate(0deg); }
}

区分IE11+浏览器可以使用::-ms-backdrop伪元素。::backdrop是一个从IE11开始支持的伪元素,可以控制全屏元素或者元素全屏时候的背景层的样式。在IE11浏览器中使用该元素时需要加-ms-私有前缀,在Edge等其他的浏览器中使用则不需要加私有前缀,加了反而无法识别。

因此,最终的CSS代码会有冗余,.some-class下的CSS样式需要写两遍:

/* IE11+浏览器识别 */
_::-ms-backdrop, .some-class {}
@supports (display: none) {
    .some-class {}
}

区分Edge12+浏览器可以使用@supports规则,这个在2.5.4节会详细介绍。区分Edge13+浏览器可以使用:in-range或者:out-of-range伪类,示例如下:

/* Edge13+浏览器识别 */
_:in-range, .some-class {}
/* 或者 */
_:out-of-range, .some-class {}

再往后的Edge版本区分就没什么意义了,也不会有这样的需求场景,我们无须关心。

2.浏览器类型的区分

若只想让Firefox浏览器识别,可以使用一个带有-moz-私有前缀的伪类或伪元素,示例如下:

/* Firefox only */
_::-moz-progress-bar, .some-class {}

若只想让现代浏览器识别,可以用如下语句:

/* 现代浏览器 */
_:default, .some-class {}

若只想让webkit浏览器识别,则只能使用带有-webkit-前缀的伪类,而不能使用带有-webkit-前缀的伪元素,因为Firefox浏览器会认为带有-webkit-前缀的伪元素语法是合法的:

/* webkit浏览器 */
:-webkit-any(_), .some-class {}

若只想让Chromium Edge浏览器识别,可以用如下语句:

/* Chromium Edge only */
_::-ms-any, .some-class {}

Chromium Edge浏览器会把任意带有-ms-前缀的伪元素都认为是合法的,这应该是借鉴了Firefox、Chrome、Safari等浏览器认为带有-webkit-前缀的伪元素是合法的这一做法。

当然,使用伪类或伪元素处理浏览器的兼容性也是有风险的,说不定哪一天浏览器就改变规则了。例如,浏览器突然不支持某个伪类了,-webkit-any()伪类就有不被浏览器支持的风险;或者哪天Firefox浏览器也支持使用带有-moz-和-webkit-前缀的伪类了,就像Chromium Edge浏览器一样。

因此,本节所提供的技巧,尤其是浏览器类型的区分,只能用在一些特殊场合,解决特殊问题,切不可当作金科玉律或者炫技的资本。

@supports是CSS中的常见的@规则,可以用来检测当前浏览器是否支持某个CSS新特性,这是最规范、最正统的CSS渐进增强处理方法,尤其适合多个CSS属性需要同时处理的场景。

@supports规则的设计初衷非常好,理论上应该很常用才对,毕竟IE浏览器的兼容性问题非常严重。但是在实际开发的时候,@supports规则并没有在IE浏览器的兼容性问题上做出什么大的贡献。原因很简单,@supports规则的支持是从Edge12浏览器开始的,根本就没有IE浏览器什么事情。

如果非要强制使用@supports规则,则要牺牲IE浏览器的部分体验。用上面加载效果实现来举例,如果我们用@supports规则书写代码则是下面这样的:

.icon-loading {
    display: inline-block;
    width: 30px; height: 30px;
    background: url(./loading.gif);
}
/* Edge12+浏览器 */
@supports (animation: none) {
    .icon-loading {
        background: url(./loading.png);
        animation: spin 1s linear infinite;
    }
}
@keyframes spin {
    from { transform: rotate(360deg); }
    to   { transform: rotate(0deg); }
}

此时,明明IE10和IE11浏览器都支持animation属性,却使用了GIF动图作为背景,这是因为IE10和IE11浏览器不支持@supports规则。

对追求极致用户体验的开发者而言,这种做法显然是无法容忍的,于是他们就会放弃使用@supports规则,转而使用其他的技巧。不过,随着浏览器的不断发展,IE10和IE11浏览器用户的占比一定会越来越小,我相信这个比例很快就会小于1%。这个时候,牺牲小部分IE10和IE11浏览器用户的体验,换来代码层面的稳健,权衡来看,也是可以接受的。

因此,在我看来,@supports规则的应用前景一定会越来越好,对于这个CSS规则,我是极力推荐大家学习的。当你在实际项目中使用@supports规则应用了一个很帅气的CSS新特性的时候,那种愉悦的感觉会让你终生难忘。

1.从@supports规则常用的语法说起

所有开发者都能轻易掌握@supports规则最基本的用法,例如:

@supports (display: flex) {
    .item { flex: 1; }
}

这段代码的意思很明了,如果浏览器支持display:flex,则匹配.item类名的元素就设置flex:1。

@supports规则还支持使用操作符进行判断,这些操作符是not、and和or,分别表示“否定”“并且”“或者”。利用这些操作符实现简单的逻辑判断也没什么问题,例如:

/* 支持弹性布局 */
@supports (display: flex) {}
/* 不支持弹性布局 */
@supports not (display: flex) {}
/* 同时支持弹性布局和网格布局 */
@supports (display: flex) and (display: grid) {}
/* 支持弹性布局或者支持网格布局 */
@supports (display: flex) or (display: grid)  {}

甚至连续判断3个以上的CSS声明也没问题:

/* 合法 */
@supports (display: flex) and (display: grid) and (gap: 0) {}
@supports (display: flex) or (display: grid) or (gap: 0) {}

但是,一旦遇到复杂逻辑判断,运行会出现问题,语法怎么写都写不对。

例如,写一个判断当前浏览器支持弹性布局,但不支持网格布局的@support语句,很多人按照自己的想法就会写成下面这样,结果语法错误,最后只能找别人已经写好的复杂语法例子去套用,这哪是学习呢?这是应付工作!实际上,稍微多花一点点功夫,就能完全学会@supports的条件判断语法,级联、嵌套,都完全不在话下。

/* 不合法 */
@supports (display: flex) and not (display: grid) {}
@supports not (display: grid) and (display: flex) {}

接下来的内容会用到CSS属性值定义语法,我现在就认定你已经掌握了这方面的知识。

我们先随便定义一个数据类型,将其命名为<var>,用于表示括号里面的东西,然后我们依葫芦画瓢:

(display: flex)
not (display: flex)
(display: flex) and (display: grid) and (gap: 0)
(display: flex) or (display: grid) or (gap: 0)

上面这些条件判断语句可以抽象成下面这样的正式语法:

<supports-condition> = ( <var> ) | not ( <var> ) | ( <var> ) [ and (<var>) ]+ | ( <var> ) [ or (<var>) ]+

最重点的部分来了!这个自定义的<var>的语法很神奇、很有趣:

<var> = <declaration> | <supports-condition>

居然在CSS语法中看到了递归——<supports-condition>嵌套<supports-condition>数据类型。原来@supports规则的复杂条件判断就是把合法的逻辑语句放在括号里不断嵌套!

此刻才发现,“判断当前浏览器支持弹性布局,但不支持网格布局”这样的问题实在是太简单了,先把基础语法写好:

@supports (display: flex) and (不支持网格布局)  {}

然后“不支持网格布局”的基础语法是not (display: grid),将语法嵌套一下,就可以得到正确的写法:

@supports (display: flex) and (not (display: grid))  {}

Edge12~Edge15浏览器正好是符合上面的条件判断的,我们不妨验证一下:

<span class="supports-match">如果有背景色,则是匹配</span>
.supports-match {
    padding: 5px;
    border: 1px solid;
}
@supports (display: flex) and (not (display: grid))  {
    .supports-match {
        background-color: #333;
        color: #fff;
    }
}

在Edge14浏览器中的效果如图2-9所示,但是在Chrome浏览器中则只有边框。

图2-9 Edge14浏览器中的文字样式示意

眼见为实,读者可以在浏览器中进入https://demo.cssworld.cn/new/2/5-3.php页面,或者扫描右侧的二维码查看效果。

2.@supports规则完整语法和细节

至此,是时候看一下@supports规则的正式语法了,如下所示:

@supports <supports-condition> {
    /* CSS规则集 */
}

其中,<supports-condition>就是前面不断出现的<supports-condition>,之前对它的常规用法已经讲得很详细了,这里再说说它的其他用法,也就是@supports规则支持CSS自定义属性的检测和CSS选择器语法的检测。例如:

@supports (--var: blue) {}
@supports selector(:default) {}

其中,CSS自定义属性的检测没有任何实用价值,本书不展开讲解;而CSS选择器语法的检测属于CSS Conditional Rules Module Level 4规范中的内容,目前浏览器尚未大规模支持,暂时没有实用价值,因此本书暂不讲解。

我们现在先把条件判断的语法放一边,来看几个你可能不知道但很有用的关于@supports规则的细节知识。

(1)在现代浏览器中,每一个逻辑判断的语法的合法性是独立的。例如:

/* 合法 */
@supports (display: flex) or (anything;) {}

但Edge浏览器会认为上面的语句是不合法的,会忽略整行语句,我认为这是Edge浏览器的bug。因此Edge12~Edge14浏览器虽然不支持CSS自定义属性,但无法使用@supports规则检测出来:

/* Edge12-Edge14忽略下面语句 */
@supports not (--var: blue) {}

(2)浏览器还提供了CSS.supports()接口,让我们可以在JavaScript代码中检测当前浏览器是否支持某个CSS特性,语法如下:

CSS.supports(propertyName, value);
CSS.supports(supportCondition);

(3)@supports规则的花括号可以包含其他任意@规则,甚至是包含@supports规则自身。例如:

@supports (display: flex) {
    /* 支持内嵌媒体查询语法 */
    @media screen and (max-width: 9999px) {
        .supports-match {
            color: #fff;            
        }
    }
    /* 支持内嵌@supports语法 */
    @supports (animation: none) {
        .supports-match {
            animation: colorful 1s linear alternate infinite;
        }
    }
    /* 支持内嵌@keyframes语法 */
    @keyframes colorful {
        from { background-color: deepskyblue; }
        to   { background-color: deeppink; }
    }
}

此时,在现代浏览器中可以看到文字背景色不停变化的动画效果,图2-10展示的就是背景色变化时的效果。

图2-10 @supports规则嵌套下的背景色动画效果示意

眼见为实,读者可以在浏览器中进入https://demo.cssworld.cn/new/2/5-4.php页面,或者扫描右侧的二维码查看效果。

3.@supports规则与渐进增强案例

@supports规则使用案例在后续章节会多次出现,到时候大家可以仔细研究,这里就先不展示了。

接下来将陆续介绍上百个CSS新特性,其中很多新特性都存在兼容性的问题,主要是IE浏览器不支持。现在移动端用户的浏览器都是现代浏览器,80%~90%桌面端用户的浏览器也都是现代浏览器,如果我们因为占比很少的低版本浏览器用户,而放弃使用这些让用户体验更好的CSS新特性,那将是一件非常遗憾的事情。身为前端开发者,如果没能在用户体验上创造更大的价值,总是使用传统的技术做一些重复性的工作,那么我们的工作激情很快就会被消磨掉,我们的竞争优势也会在日复一日的重复劳动中逐渐丧失。

因此,我觉得大家在日常工作中,应该大胆使用CSS新特性,同时再多花一点额外的时间对这些新特性做一些兼容性方面的工作。这绝对是一件非常划算的事情,无论是对用户还是对自身的成长都非常有帮助。

当然,虽然本节介绍了多个CSS新特性兼容处理的技巧,但是在实际开发的时候还是会遇到很多单纯使用CSS无法搞定的情况,此时就需要借助JavaScript代码和DOM API来实现兼容,即如果出现CSS无法做到兼容,或者低版本浏览器希望有近似的交互体验效果的情况,就需要JavaScript代码的处理。例如使用position:sticky实现滚动粘滞效果,传统方法都是使用JavaScript脚本来实现的,但现在大部分浏览器已经支持这个特性,我们可以让传统浏览器继续使用传统的JavaScript方法,现代浏览器则单纯使用CSS方法以得到更好的交互体验。

千万不要觉得麻烦,说什么“所有浏览器都直接使用JavaScript实现就好啦”。所谓技术成就人生,如果完成需求的心态都是为了应付工作,哪里来的足以成就人生的技术呢?要知道,你所获得的报酬是跟你创造的价值成正比的,如果你想获得超出常人的报酬,那你就需要比那些普通开发者创造的价值更高,而这些价值的差异往往就源自对这些技术细节的处理,日积月累之后就会有明显的差异。

这额外的一点兼容性处理工作其实也花不了你什么时间,例如:

.adsense {
    position: relative;
    position: sticky;
}

JavaScript代码中就多一行判断代码而已:

if (!window.CSS || !CSS.supports || !CSS.supports('position', 'sticky')) {
// 传统的JavaScript方法调用……
}

[1] 本书中的Edge浏览器专指Edge 12~Edge 18版本的浏览器(参见2.5节)。