实现特性检测

特性检测是指确定浏览器是否支持某段代码,并根据支持与否运行不同的代码,以便浏览器始终提供可用的体验,而不是在某些浏览器中崩溃/报错。本文详细介绍了如何编写自己的简单特性检测,如何使用库来加速实现,以及用于特性检测的本地特性,例如 @supports

预备知识 熟悉核心的 HTMLCSSJavaScript 语言;对 跨浏览器测试 的高级原则有所了解。
目标 了解特性检测的概念,并能够在 CSS 和 JavaScript 中实现合适的解决方案。

特性检测的概念

特性检测背后的思想是,你可以运行一个测试来确定当前浏览器是否支持某个特性,然后有条件地运行代码,以便在**支持**该特性的浏览器和**不支持**该特性的浏览器中都能提供可接受的体验。如果你不这样做,不支持你代码中使用的特性的浏览器可能无法正确显示你的网站,或者完全失败,从而造成糟糕的用户体验。

让我们回顾并查看我们在 JavaScript 调试和错误处理 文章中提及的示例——地理定位 API(它公开了 Web 浏览器运行设备可用的位置数据)的主要入口点是全局 Navigator 对象上可用的 geolocation 属性。因此,你可以通过使用类似以下代码来检测浏览器是否支持地理定位:

js
if ("geolocation" in navigator) {
  navigator.geolocation.getCurrentPosition((position) => {
    // show the location on a map, such as the Google Maps API
  });
} else {
  // Give the user a choice of static maps
}

在我们继续之前,我们想先说明一件事——不要将特性检测与**浏览器嗅探**(检测访问网站的具体浏览器)混淆——这是一种糟糕的做法,应不惜一切代价加以制止。有关更多详细信息,请参阅使用用户代理字符串进行浏览器检测(UA 嗅探)

编写自己的特性检测测试

在本节中,我们将介绍如何在 CSS 和 JavaScript 中实现自己的特性检测测试。

CSS

你可以通过在 JavaScript 中测试 *element.style.property* 的存在(例如,paragraph.style.rotate)来编写 CSS 特性的测试。

一个经典的例子是测试浏览器对 子网格(Subgrid) 的支持;对于支持 grid-template-columnsgrid-template-rows 属性的 subgrid 值的浏览器,我们可以在布局中使用子网格。对于不支持的浏览器,我们可以使用普通的网格,它也能正常工作,但看起来没有那么酷。

以此为例,如果支持该值,我们可以包含一个子网格样式表;如果不支持,则包含一个常规网格样式表。为此,我们可以在 HTML 文件的头部包含两个样式表:一个用于所有样式,另一个用于在不支持子网格时实现默认布局。

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 来测试对子网格值的支持,然后根据浏览器支持更新条件样式表的 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-rule。它的工作方式与媒体查询类似,但它不是根据分辨率、屏幕宽度或宽高比等媒体特性选择性地应用 CSS,而是根据是否支持某个 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;
  }
}

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

@supports 还具有 ANDORNOT 逻辑——如果子网格选项不可用,另一个块将应用常规网格布局:

css
@supports not (grid-template-columns: subgrid) {
  /* rules in here */
}

这比之前的示例更方便——我们可以在 CSS 中完成所有特性检测,无需 JavaScript,并且我们可以在一个 CSS 文件中处理所有逻辑,减少 HTTP 请求。因此,这是确定浏览器对 CSS 特性支持的首选方法。

JavaScript

我们之前已经看到了一个 JavaScript 特性检测测试的例子。通常,此类测试通过几种常见模式之一完成。

可检测特性的常见模式包括:

对象的成员

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

我们之前的示例使用了这种模式,通过测试 navigator 对象中是否存在 geolocation 成员来检测 地理定位 支持:

js
if ("geolocation" in navigator) {
  // Access navigator.geolocation APIs
}
元素的属性

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

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

js
function supports_canvas() {
  return !!document.createElement("canvas").getContext;
}

if (supports_canvas()) {
  // Create and draw on canvas elements
}

注意: 上例中的双重 NOT (!!) 是一种强制返回值成为“正确”布尔值的方法,而不是可能导致结果偏差的 真值(Truthy)/假值(Falsy)

元素上某个方法的特定返回值

使用 Document.createElement() 在内存中创建一个元素,然后检查该元素上是否存在某个方法。如果存在,则检查其返回值。

元素保留分配属性值

使用 Document.createElement() 在内存中创建一个元素,将一个属性设置为特定值,然后检查该值是否被保留。

请记住,有些特性是无法检测到的。在这些情况下,你需要使用不同的方法,例如使用 polyfill

matchMedia

在此我们也想提及 Window.matchMedia JavaScript 特性。这是一个允许你在 JavaScript 内部运行媒体查询测试的属性。它看起来像这样:

js
if (window.matchMedia("(width <= 480px)").matches) {
  // run JavaScript in here.
}

举个例子,我们的 Snapshot 演示就利用了它,有选择地应用 Brick JavaScript 库并用它来处理 UI 布局,但仅限于小屏幕布局(480px 宽或更小)。我们首先使用 media 属性,只有当页面宽度为 480px 或更小时才将 Brick CSS 应用到页面上:

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

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

js
if (window.matchMedia("(width <= 480px)").matches) {
  deck.shuffleTo(1);
}

总结

本文详细介绍了特性检测,讲解了主要概念并展示了如何实现自己的特性检测测试。

接下来,我们将开始探讨自动化测试。