使用特性检测编写跨浏览器兼容的 CSS

Avatar of Schalk Venter
Schalk Venter

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

2017 年初,我举办了几场关于 CSS 特性检测的研讨会,主题为2017 年的 CSS 特性检测

我的一位朋友,来自 New Media LabsJustin Slack,最近向我发送了一个指向出色的特性查询管理器扩展(可在 ChromeFirefox 中使用)的链接,该扩展由尼日利亚开发者 Ire Aderinokun 开发。这似乎是我关于该主题的研讨会材料的完美补充。

然而,在重新审视这些材料时,我意识到我在过去 18 个月里在这方面的工作已经过时了。

CSS 领域经历了一些重大的变化

以上情况促使我不仅重新审视现有的材料,还思考 CSS 特性检测在未来 18 个月内的发展状况。

简而言之

  1. ❓ 为什么我们需要 CSS 特性检测?
  2. 🛠️ 进行特性检测的良好(以及不太好的)方法有哪些?
  3. 🤖 CSS 特性检测的未来会怎样?

跨浏览器兼容的 CSS

在使用 CSS 时,似乎首要关注的问题之一始终是浏览器之间功能支持的不一致。这意味着 CSS 样式在我的首选浏览器上可能看起来完美无缺,但在其他(也许更流行的)浏览器上可能完全失效。

幸运的是,由于 CSS 语言本身设计中的一个关键特性,处理浏览器支持不一致的问题非常简单。这种行为称为**容错性**,这意味着浏览器会忽略它们不理解的 CSS 代码。这与 JavaScript 或 PHP 等语言形成鲜明对比,这些语言会停止所有执行以抛出错误。

这里的重要含义是,如果我们相应地分层 CSS,则只有在浏览器理解其含义时才会应用属性。例如,您可以包含以下 CSS 规则,浏览器将忽略它——覆盖初始的黄色,但忽略第三个无意义的值

background-color: yellow;
background-color: blue; /* Overrides yellow */
background-color: aqy8godf857wqe6igrf7i6dsgkv; /* Ignored */

为了说明如何在实践中使用它,让我从一个人为的但简单的场景开始

一位客户强烈希望在他的主页上包含一个号召性用语(以弹出窗口的形式)。凭借您惊人的前端技能,您可以快速制作出人类已知的最令人讨厌的弹出消息

不幸的是,事实证明他的妻子有一台运行 Internet Explorer 8 的旧 Windows XP 机器。您震惊地发现她看到的不再像弹出窗口了。

但是!我们记得可以通过使用 CSS 容错性的魔力来解决这个问题。我们识别出所有对样式至关重要的部分(例如,阴影很好,但对可用性没有任何帮助),并在所有核心样式之前添加回退。

这意味着我们的 CSS 现在看起来像这样(为了清楚起见,突出显示了覆盖部分)

.overlay {
  background: grey;
  background: rgba(0, 0, 0, 0.4);
  border: 1px solid grey;
  border: 1px solid rgba(0, 0, 0, 0.4);
  padding: 64px;
  padding: 4rem;
  display: block;
  display: flex;
  justify-content: center; /* if flex is supported */
  align-items: center; /* if flex is supported */
  height: 100%;
  width: 100%;
}

.popup {
  background: white;
  background-color: rgba(255, 255, 255, 1);
  border-radius: 8px;
  border: 1px solid grey;
  border: 1px solid rgba(0, 0, 0, 0.4);
  box-shadow: 
    0 7px 8px -4px rgba(0,0, 0, 0.2),
    0 13px 19px 2px rgba(0, 0, 0, 0.14),
    0 5px 24px 4px rgba(0, 0, 0, 0.12);
  padding: 32px;
  padding: 2rem;
  min-width: 240px;
}

button {
  background-color: #e0e1e2;
  background-color: rgba(225, 225, 225, 1);
  border-width: 0;
  border-radius: 4px;
  border-radius: 0.25rem;
  box-shadow: 
    0 1px 3px 0 rgba(0,0,0,.2), 
    0 1px 1px 0 rgba(0,0,0,.14), 
    0 2px 1px -1px rgba(0,0,0,.12);
  color: #5c5c5c;
  color: rgba(95, 95, 95, 1);
  cursor: pointer;
  font-weight: bold;
  font-weight: 700;
  padding: 16px;
  padding: 1rem;
}

button:hover {
  background-color: #c8c8c8;
  background-color: rgb(200,200,200); 
}

以上示例通常属于更广泛的 渐进增强 方法。如果您有兴趣了解有关渐进增强的更多信息,请查看 Aaron Gustafson 关于该主题的优秀书籍的第二版,名为 自适应 Web 设计:使用渐进增强打造丰富的体验(2016)。

如果您是前端开发新手,您可能想知道如何知道特定 CSS 属性的支持级别。简而言之,您使用 CSS 的次数越多,您就会越了解这些属性。但是,有一些工具可以帮助我们。

即使拥有上述所有工具,记住 CSS 支持也将有助于我们提前计划样式并提高编写样式的效率。

CSS 容错性的限制

下周,您的客户带着新的请求回来了。他希望收集一些用户对之前对主页所做更改的反馈——同样,使用弹出窗口

在 Internet Explorer 8 中,它将再次如下所示

这次更加积极主动,您使用新的回退技能建立了一个在 Internet Explorer 8 上有效的基本样式,以及适用于其他所有内容的渐进样式。不幸的是,我们仍然遇到了问题……

为了用 ASCII 爱心替换默认的单选按钮,我们使用了::before 伪元素。但是,Internet Explorer 8 不支持此伪元素。这意味着爱心图标不会呈现;但是,<input type="radio"> 元素上的display: none 属性仍然在 Internet Explorer 8 上触发。这意味着既没有显示替换行为,也没有显示默认行为。

感谢 John Faulds 指出,如果您将官方的双冒号语法替换为单个冒号,实际上可以在 Internet Explorer 8 中使::before 伪元素工作。

简而言之,我们有一个规则(display: none),其执行不应该绑定到它自己的支持(以及它自己的回退结构),而应该绑定到完全独立的 CSS 特性(::before)的支持级别。

出于所有意图和目的,常见的方法是探索是否还有更直接的解决方案,而无需依赖::before。但是,为了举例说明,假设上述解决方案是不可协商的(有时确实如此)。

引入用户代理检测

一种解决方案可能是确定用户正在使用什么浏览器,然后仅在他们的浏览器支持::before 伪元素时应用display: none

事实上,这种方法几乎与网络本身一样古老。它被称为**用户代理检测**,或者更通俗地说,浏览器嗅探。

它通常如下进行

  • 所有浏览器都会在全局窗口对象上添加一个 JavaScript 属性,称为navigator,并且此对象包含一个userAgent 字符串属性。
  • 在我的情况下,userAgent 字符串为:Mozilla/5.0 (Windows NT10.0;Win64;x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.9 Safari/537.36
  • Mozilla 开发者网络有一个 综合列表,说明如何使用以上内容来确定浏览器。
  • 如果我们使用 Chrome,则以下内容应返回 true:(navigator.userAgent.indexOf("chrome") !== -1)
  • 但是,在 MDN 上的 Internet Explorer 部分,我们只得到Internet Explorer。IE 不会以BrowserName/VersionNumber 格式显示其名称。
  • 幸运的是,Internet Explorer 以 条件注释 的形式提供了自己的本机检测。

这意味着在我们的 HTML 中添加以下内容就足够了

<!--[if lt IE 9]>
  <style>
    input {
      display: block;
    }
  </style>
<![endif]-->

这意味着如果浏览器是低于版本 9 的 Internet Explorer 版本(IE 9 支持::before),则将应用上述内容——有效地覆盖display: none 属性。
看起来很简单,对吧?

不幸的是,随着时间的推移,用户代理检测中出现了一些严重的缺陷。以至于从版本 10 开始,Internet Explorer 就停止了对条件注释的支持。您还会注意到,在Mozilla 开发者网络链接本身中,以下内容以橙色警报呈现

值得重申的是:使用用户代理嗅探很少是一个好主意。您几乎总能找到更好的、更广泛兼容的方法来解决您的问题!

用户代理检测最大的缺点是,由于以下原因,浏览器供应商开始伪造他们的用户代理字符串

  • 开发者添加了浏览器不支持的 CSS 功能。
  • 开发者添加了用户代理检测代码,以便为浏览器提供回退。
  • 浏览器最终添加了对该特定 CSS 功能的支持。
  • 原始的用户代理检测代码未更新以考虑这一点。
  • 即使浏览器现在支持 CSS 功能,代码也会始终显示回退。
  • 浏览器使用伪造的用户代理字符串,以便为用户提供最佳的网络体验。

此外,即使我们能够准确地确定每种浏览器类型和版本,我们也必须主动维护和更新我们的用户代理检测,以反映这些浏览器的功能支持状态(不包括尚未开发的浏览器)。

需要注意的是,尽管特性检测和用户代理检测之间存在表面上的相似性,但特性检测采取的方法与用户代理检测完全不同。根据Mozilla 开发者网络,当我们使用特性检测时,我们实际上是在执行以下操作

  1. 🔎 测试浏览器是否能够实际运行特定行(或多行)HTML、CSS 或 JavaScript 代码。
  2. 💪 根据测试结果采取特定操作。

我们还可以参考维基百科以获取更正式的定义(我的重点)

特性检测(也称为特性测试)是一种技术,用于 Web 开发中处理运行时环境(通常是 Web 浏览器或用户代理)之间的差异,通过以编程方式测试环境是否提供某些功能的线索。然后,此信息用于使应用程序以某种方式适应环境:利用某些 API 或调整以获得更好的用户体验。

虽然有点深奥,但此定义确实突出了特性检测的两个重要方面

  • 特性检测是一种技术,而不是特定的工具或技术。这意味着有多种(同样有效)的方法可以实现特性检测。
  • 特性检测以编程方式测试代码。这意味着浏览器实际上会运行一段代码以查看会发生什么,而不是仅仅使用推理或将其与理论参考/列表进行比较,就像使用用户代理检测一样。

使用 @supports 进行 CSS 特性检测

核心概念不是问“这是什么浏览器?”而是问“您的浏览器是否支持我想使用的功能?”。

——Rob Larson,《不确定的 Web:变化环境中的 Web 开发》(2014 年)

大多数现代浏览器都支持一组称为 CSS 条件规则的原生 CSS 规则。这些规则允许我们在样式表本身中测试某些条件。最新版本(称为模块级别 3)由层叠样式表工作组描述如下

此模块包含 CSS 用于条件处理样式表部分的功能,这些功能以处理器的功能或正在应用样式表的文档为条件。它包含并扩展了 CSS 级别 2 [CSS21] 的功能,该功能建立在 CSS 级别 1 [CSS1] 的基础上。与级别 2 相比,主要扩展包括允许在“@media”内部嵌套某些 @规则,以及添加“@supports”规则以进行条件处理。

如果您之前使用过@media@document@import,那么您已经具备了使用 CSS 条件规则的经验。例如,当使用CSS 媒体查询时,我们会执行以下操作

  • 将单个或多个 CSS 声明包装在用花括号{ }括起来的代码块中。
  • 在代码块前面加上带有其他信息的@media查询。
  • 包含可选的媒体类型。这可以是allprintspeech或常用的screen类型。
  • 使用and/or链接表达式以确定范围。例如,如果我们使用(min-width: 300px) and (max-width: 800px),则如果屏幕尺寸大于 300 像素并且小于 800 像素,它将触发查询。

功能查询规范(编辑草案)规定了与上述示例方便类似的行为。我们不是使用查询表达式根据屏幕尺寸设置条件,而是编写表达式以根据浏览器的 CSS 支持对我们的代码块进行范围限定(我的重点)

“@supports 规则允许 CSS 以实现对 CSS 属性和值的的支持为条件。此规则使作者更容易使用新的 CSS 功能并为不支持这些功能的实现提供良好的回退。这对于提供新布局机制的 CSS 功能以及其他需要根据属性支持对一组相关样式进行条件处理的情况尤其重要。

简而言之,功能查询是一个小型内置 CSS 工具,它允许我们仅在浏览器支持单独的 CSS 功能时才执行代码(如上面的display: none示例)——并且与媒体查询非常相似,我们可以链接表达式,如下所示:@supports (display: grid) and ((animation-name: spin) or (transition: transform(rotate(360deg))

因此,理论上,我们应该能够执行以下操作

@supports (::before) {
  input {
    display: none;
  }
}

不幸的是,似乎在我们上面的示例中,display: none属性没有触发,尽管事实上您的浏览器可能支持::before

这是因为使用@supports有一些注意事项

  • 首先,CSS 功能查询仅支持CSS 属性,而不支持CSS 伪元素,如::before
  • 其次,您会看到,在上面的示例中,我们的@supports (transform: scale(2)) and (animation-name: beat)条件正确触发。但是,如果我们在 Internet Explorer 11(它支持transform: scale(2)animation-name: beat)中测试它,它不会触发。怎么回事?简而言之,@supports是一个 CSS 功能,它有自己的支持矩阵

使用 Modernizr 进行 CSS 特性检测

幸运的是,修复方法相当简单!它以名为Modernizr的开源 JavaScript 库的形式出现,最初由Faruk Ateş开发(尽管现在它背后有一些非常大的名字,比如来自 Chrome 的Paul Irish和来自 Stripe 的Alex Sexton)。

在深入研究 Modernizr 之前,让我们解决一个让许多开发者感到困惑的话题(部分原因是“Modernizr”这个名称本身)。Modernizr 不会转换您的代码或神奇地启用不受支持的功能。事实上,Modernizr 对您的代码所做的唯一更改是在您的<html>标签中追加特定的 CSS 类。

这意味着您最终可能会得到以下内容

<html class="js flexbox flexboxlegacy canvas canvastext webgl no-touch geolocation postmessage websqldatabase indexeddb hashchange history draganddrop websockets rgba hsla multiplebgs backgroundsize borderimage borderradius boxshadow textshadow opacity cssanimations csscolumns cssgradients cssreflections csstransforms csstransforms3d csstransitions fontface generatedcontent video audio localstorage sessionstorage webworkers applicationcache svg inlinesvg smil svgclippaths">

这是一个很大的 HTML 标签!但是,它使我们能够做一些非常强大的事情:使用CSS 后代选择器有条件地应用 CSS 规则。

当 Modernizr 运行时,它使用 JavaScript 检测用户的浏览器支持什么,如果它确实支持该功能,Modernizr 会将其名称作为类注入到<html>中。或者,如果浏览器不支持该功能,它会在注入的类前面加上no-(例如,在我们的::before示例中为no-generatedcontent)。这意味着我们可以在样式表中按如下方式编写条件规则

.generatedcontent input {
  display: none
}

此外,我们能够在 Modernizr 中复制@supports表达式的链接,如下所示

/* default */
.generatedcontent input { }

/* 'or' operator */
.generatedcontent input, .csstransforms input { }

/* 'and' operator */
.generatedcontent.csstransformsinput { }

/* 'not' operator */
.no-generatedcontent input { }

由于 Modernizr 在 JavaScript 中运行(并且不使用任何原生浏览器 API),因此它实际上在几乎所有浏览器上都受支持。这意味着通过利用诸如generatedcontentcsstransforms之类的类,我们能够涵盖 Internet Explorer 8 的所有基础,同时仍然为最新的浏览器提供尖端的 CSS。

需要注意的是,自从 Modernizr 3.0 发布以来,我们不再能够下载一个标准的modernizr.js文件包含除了厨房水槽之外的所有内容。相反,我们必须通过他们的向导明确生成我们自己的自定义 Modernizr 代码(以复制或下载)。这很可能是为了应对过去几年全球范围内对 Web 性能日益关注的结果。检查更多功能会导致更多加载,因此 Modernizr 希望我们只检查我们需要的内容。

所以,我应该始终使用 Modernizr 吗?

鉴于 Modernizr 在所有浏览器中都得到有效支持,是否还有必要使用 CSS 功能查询?具有讽刺意味的是,我不仅认为我们应该这样做,而且功能查询仍然应该是我们的首选。

首先,Modernizr 不直接插入浏览器 API 的事实是其最大的优势——它不依赖于特定浏览器 API 的可用性。但是,此优势是有代价的,并且此代价是对大多数浏览器通过@supports开箱即用支持的事物的额外开销——尤其是在您为了少量边缘用户而无差别地向所有用户提供此额外开销时。需要注意的是,在我们上面的示例中,Internet Explorer 8 目前仅占全球使用量的 0.18%)。

@supports的轻量级相比,Modernizr 存在以下缺点

  • Modernizr 开发背后的理念是基于这样一个假设:Modernizr 从一开始就“旨在最终变得不再必要。”
  • 在大多数情况下,Modernizr 需要阻塞渲染。这意味着在网页甚至能够在屏幕上显示内容之前,需要下载并执行 Modernizr 的 JavaScript 代码——这增加了我们的页面加载时间(尤其是在移动设备上)!
  • 为了运行测试,Modernizr 通常需要实际构建隐藏的 HTML 节点并测试其是否有效。例如,为了测试<canvas>的支持情况,Modernizr 执行以下 JavaScript 代码:return !!(document.createElement('canvas').getContext && document.createElement('canvas').getContext('2d'));。这会消耗本可用于其他地方的 CPU 处理能力。
  • Modernizr 使用的 CSS 后代选择器模式会增加CSS 特异性。(参见 Harry Roberts 关于为什么“特异性是一个最好避免的特性”的精彩文章。)
  • 尽管 Modernizr 涵盖了大量的测试(150+),但它仍然没有覆盖像@support那样广泛的 CSS 属性。Modernizr 团队积极维护着这些无法检测到的属性列表

鉴于特性查询已经在浏览器领域得到了广泛的应用(在撰写本文时,覆盖了大约全球 93.42% 的浏览器),我已经很久没有使用 Modernizr 了。但是,了解它的存在作为一个选项是很有必要的,以防我们遇到@supports的限制,或者我们需要支持由于各种潜在原因而仍然停留在旧版浏览器或设备上的用户。

此外,使用 Modernizr 时,通常会结合@supports,如下所示

.generatedcontent input {
  display: none;
}

label:hover::before {
  color: #c6c8c9;
}

input:checked + label::before {
  color: black;
}

@supports (transform: scale(2)) and (animation-name: beat) {
  input:checked + label::before {
    color: #e0e1e2;
    animation-name: beat;
    animation-iteration-count: infinite;
    animation-direction: alternate;
  }
}

这将触发以下操作

  • 如果浏览器不支持::before,我们的 CSS 将回退到默认的 HTML 单选选择。
  • 如果浏览器既不支持transform(scale(2))也不支持animation-name: beat,但支持::before,那么选中时,爱心图标将变为黑色而不是动画。
  • 如果浏览器支持transform(scale(2)animation-name: beat::before,那么选中时,爱心图标将进行动画。

CSS 特性检测的未来

到目前为止,我一直避免在JavaScript 吞噬世界,或者可能是后 JavaScript 时代中谈论特性检测。也许是有意为之,因为 CSS 和 JavaScript 交汇处的当前迭代极具争议性,并且存在分歧

从那一刻起,网络社区因一场激烈的辩论而一分为二,这场辩论发生在那些将 CSS 视为“关注点分离”范式中不可触碰的层级(内容 + 呈现 + 行为,HTML + CSS + JS)的人,以及那些完全忽略了这条黄金法则并找到了不同方式来设置 UI 样式的人之间,通常是通过 JavaScript 应用 CSS 样式。这场辩论每天都在变得越来越激烈,给一个曾经不受这种“宗教战争”影响的社区带来了分裂。

——Cristiano Rastelli,让 CSS 迎来和平(2017)

但是,我认为探索如何在现代 CSS-in-JS 工具链中应用特性检测可能具有以下价值

  • 它提供了一个机会来探索 CSS 特性检测如何在根本不同的环境中工作。
  • 它展示了特性检测作为一种技术,而不是一种特定的技术或工具。

考虑到这一点,让我们首先检查一下使用最广泛的 CSS-in-JS 库(至少在撰写本文时)Styled Components实现弹出窗口的方式。

它在 Internet Explorer 8 中的外观如下

在我们之前的示例中,我们能够根据浏览器对::before(通过 Modernizr)和transform(通过@supports)的支持情况有条件地执行 CSS 规则。但是,通过利用 JavaScript,我们可以更进一步。由于@supports和 Modernizr 都通过 JavaScript 公开其 API,因此我们能够仅根据浏览器支持情况有条件地加载弹出窗口的整个部分。

请记住,您可能需要进行大量的工作才能使 React 和 Styled Components 在不支持::before的浏览器中工作(在此上下文中检查display: grid可能更有意义),但为了保持与上述示例的一致性,让我们假设我们在 Internet Explorer 8 或更低版本中运行 React 和 Styled Components。

在上面的示例中,您会注意到我们创建了一个名为ValueSelection的组件。此组件返回一个可点击的按钮,单击时会增加点赞数。如果您在稍微旧一点的浏览器上查看示例,您可能会注意到,您将看到一个下拉菜单,而不是按钮,其中包含 0 到 9 的值。

为了实现这一点,我们仅在满足以下条件时才返回增强版本的组件

if (
  CSS.supports('transform: scale(2)') &&
  CSS.supports('animation-name: beat') &&
  Modernizr.generatedcontent
) {
  return (
    <React.Fragment>
      <Modern type="button" onClick={add}>{string}</Modern> 
      <input type="hidden" name="liked" value={value} />
    </React.Fragment>
  )
}

return (
  <Base value={value} onChange={select}>
    {
      [1,2,3,4,5,6,7,8,9].map(val => (
        <option value={val} key={val}>{val}</option>
      ))
    }
  </Base>
);

这种方法的有趣之处在于,ValueSelection组件仅公开两个参数

  • 当前的点赞数
  • 更新点赞数时要运行的函数
<Overlay>
  <Popup>
    <Title>How much do you like popups?</Title>
    <form>
      <ValueInterface value={liked} change={changeLike} />
      <Button type="submit">Submit</Button>
    </form>
  </Popup>
</Overlay>

换句话说,组件的逻辑与其呈现完全分离。组件本身将在内部确定根据浏览器的支持矩阵最适合的呈现方式。将条件呈现抽象到组件本身内部,为在前端和/或设计团队中构建跨浏览器兼容的界面开辟了令人兴奋的新途径。

这是最终产品

…以及它在理论上在 Internet Explorer 8 中的样子

其他资源

如果您有兴趣深入了解以上内容,您可以访问以下资源


Schalk 是一位南非的前端开发人员/设计师,他对技术和网络作为一股向善的力量在他家乡所扮演的角色充满热情。他全职与一群具有公民科技理念的开发人员一起工作,他们来自一家名为OpenUp的南非非营利组织。

他还帮助管理着一个名为 Codebridge 的协作空间,鼓励开发人员来到这里,尝试将技术作为一种工具来弥合社会鸿沟与当地社区一起解决问题