实现特性检测

特性检测包括确定浏览器是否支持某个代码块,并根据支持(或不支持)运行不同的代码,这样浏览器就能始终提供正常的使用体验,而不会在某些浏览器中崩溃或出错。本文详细介绍了如何编写自己的简单特征检测、如何使用库加快实现速度,以及原生的特征检测特性(如 @supports)。

前提: 熟悉核心的 HTMLCSS 以及 JavaScript 语言,了解顶层的跨浏览器测试原则
目标: 了解特性检测的概念,并能够在 CSS 和 JavaScript 中实现合适的解决方案。

特性检测的概念

特性检测的理念在于,通过执行一个测试来确认当前浏览器是否支持某项特性。然后,根据测试结果有条件地执行代码,确保在支持该特性的浏览器和不支持的浏览器中都能提供一个可接受的用户体验。如果不采取这种做法,那么在那些不支持你代码中所用特性的浏览器上,你的网站将无法正确显示,从而导致糟糕的用户体验。

让我们回顾一下我们在处理常见 JavaScript 问题一文中提及的例子——地理位置 API(它能够访问运行网页浏览器的设备的位置数据),其主要使用入口点是全局 Navigator 对象上的 geolocation 属性。因此,你可以使用类似以下的方法来检测浏览器是否支持地理位置特性:

js
if ("geolocation" in navigator) {
  navigator.geolocation.getCurrentPosition(function (position) {
    // 使用如 Google Maps API 显示地图上的位置
  });
} else {
  // 为用户提供静态地图
}

在我们继续之前,先说一件事——不要把特性检测和浏览器嗅探(检测什么浏览器在访问网站)混淆起来——后者是一种糟糕的做法,应该不惜一切代价加以阻止。参见不要嗅探浏览器以了解更多细节。

撰写自己的特性检测代码

在本节中,我们将研究如何在 CSS 和 JavaScript 中实现特性检测测试。

CSS

你可以通过在 JavaScript 中测试元素.style.属性(例如 paragraph.style.rotate)的存在来检测 CSS 特性。

一个经典的例子可能是在浏览器中测试子网格的支持情况。对于支持 grid-template-columnsgrid-template-rows 属性的 subgrid 值的浏览器来说,我们可以在布局中使用子网格。对于不支持的浏览器,我们可以使用常规的网格布局,虽然不太酷,但也能正常工作。

以此为例,我们可用在支持该值的情况下包含子网格样式表,而在不支持的情况下包含常规网格样式表。要实现这一点,我们可以在 HTML 文件的 head 部分包含两个样式表:一个包含所有的样式,另一个在不支持子网格的情况下实现默认的布局。

html
<link href="basic-styling.css" rel="stylesheet" />
<link class="conditional" href="grid-layout.css" rel="stylesheet" />

这里,basic-styling.css 处理所有每个浏览器都支持的样式。我们还有两个额外的 CSS 文件,grid-layout.csssubgrid-layout.css,它们包含了我们想根据浏览器的支持程度有选择地应用的 CSS。

我们先使用 JavaScript 来测试对 subgrid 值的支持,然后根据浏览器的支持情况更新我们的条件样式表的 href

我们可以在我们的文档中添加 <script></script> 元素,填充以下 JavaScript:

js
const conditional = document.querySelector(".conditional");
if (CSS.supports("grid-template-columns", "subgrid")) {
  conditional.setAttribute("href", "subgrid-layout.css");
}

在我们的条件语句中,我们使用 CSS.supports() 测试 grid-template-columns 属性是否支持 subgrid 值。

@supports

CSS 有一个原生的特性检测机制:@supports at-规则。它的工作方式与媒体查询类似,但不同的是,媒体查询是根据分辨率、屏幕宽度或长宽比等媒体特性来选择性地应用 CSS,而 @supports 则是根据是否支持某个 CSS 特性来选择性地应用 CSS,类似于 CSS.supports()

例如,我们可以使用 @supports 重写我们之前的例子:

css
@supports (grid-template-columns: subgrid) {
  main {
    display: grid;
    grid-template-columns: repeat(9, 1fr);
    grid-template-rows: repeat(4, minmax(100px, auto));
  }

  .item {
    display: grid;
    grid-column: 2 / 7;
    grid-row: 2 / 4;
    grid-template-columns: subgrid;
    grid-template-rows: repeat(3, 80px);
  }

  .subitem {
    grid-column: 3 / 6;
    grid-row: 1 / 3;
  }
}

只有当浏览器支持 grid-template-columns: subgrid; 声明时,这个 at 规则块才会应用其中的 CSS 规则。为了使带有值的条件生效,你需要包含完整的声明(而不仅仅是属性名称),并且不包括结尾的分号。

@supports 还支持 ANDORNOT 逻辑——如果 subgrid 选项不可用,则另一个代码块会应用常规网格布局:

css
@supports not (grid-template-columns: subgrid) {
  /* 这里写其他规则 */
}

这比前面的例子更方便,因为我们可以在 CSS 中进行所有的特性检测,而不需要使用 JavaScript。此外,我们可以在一个 CSS 文件中处理所有的逻辑,从而减少 HTTP 请求。因此,这是确定浏览器对 CSS 特性支持的首选方法。

JavaScript

我们在前面已经看到了一个 JavaScript 特性检测的例子。一般来说,这种测试是通过几种常见的模式之一完成的。

常见的探测特性的模式包括:

对象的成员

检查一个特定的方法或属性(通常是使用 API 的入口或你正在检测的其他特性)是否存在于其父 Object 中。

我们前面的例子使用这种模式(通过测试 navigator 对象的 geolocation 成员)来检测 Geolocation 的支持:

js
if ("geolocation" in navigator) {
  // 访问 navigator.geolocation API
}
元素的属性

使用 Document.createElement() 在内存中创建一个元素,然后检查其上是否存在属性。

这个例子展示了一种检测 Canvas API 支持的方法:

js
function supports_canvas() {
  return !!document.createElement("canvas").getContext;
}
if (supports_canvas()) {
  // 创建 canvas 元素并在其上作画
}

备注:上例中的双非运算符(!!)是一种将返回值强制转换为“适当的”布尔值的方法,而不是真值/假值,后者可能使结果偏离。

方法在元素上的特定返回值

使用 Document.createElement() 在内存中创建一个元素,然后检查该元素是否存在方法。如果有的话,检查它的返回值。请参阅深入了解 HTML 视频格式检测中的特性测试,了解这种模式的一个例子。

元素保留分配的属性值

使用 Document.createElement() 在内存中创建一个元素,将一个属性设置为特定值,然后检查该值是否被保留。关于这种模式的例子,请参见深入了解 HTML <input> 类型检测中的特性测试。

不过要记住,有些特征是无法检测到的。在这种情况下,你需要使用其他的方法,例如使用 polyfill

matchMedia

现在,我们还想提到 Window.matchMedia 这个 JavaScript 特性。借助这个属性,你可以在 JavaScript 内部运行媒体查询测试。它看起来像这样:

js
if (window.matchMedia("(max-width: 480px)").matches) {
  // 在这里运行 JavaScript
}

举个例子,我们的 Snapshot 演示了如何有选择地应用 Brick JavaScript 库来处理 UI 布局,使其仅应用于小屏幕布局(宽度为 480px 或以下)。我们首先使用 media 属性,仅在页面宽度为 480px 或更小的情况下,将 Brick CSS 应用于页面:

html
<link
  href="dist/brick.css"
  rel="stylesheet"
  media="all and (max-width: 480px)" />

然后我们在 JavaScript 中多次使用 matchMedia(),只在小屏幕布局时运行 Brick 导航功能(在宽屏幕布局中,所有内容都可以一次看到,所以我们不需要在不同的视图之间导航)。

js
if (window.matchMedia("(max-width: 480px)").matches) {
  deck.shuffleTo(1);
}

总结

本文详细介绍了特征检测的主要概念,并向你展示了如何实施自己的特征检测测试。

接下来,我们将开始了解自动化测试。