红宝书之第十三章:客户端检测

509 阅读6分钟

能力检测

能力检测(又称特性检测)即在 JavaScript 运行时中使用一套简单的检测逻辑,测试浏览器是否支 持某种特性。这这种方式不要求事先知道特定浏览器的信息,只需检测自己关心的能力是否存在即可。能力检测的基本模式如下:

if (object.propertyInQuestion) {
  // 使用 object.propertyInQuestion
}

应该先检测最常用的方式

安全能力检测

能力检测最有效的场景是检测能力是否存在的同时,验证其是否能够展现出预期的行为。

比如我们需要一个排序方法sort,我们就需要sort必须是一个函数,检测方法如下:

// 好一些,检测 sort 是不是函数
function isSortable(object) { 
 return typeof object.sort == "function"; 
}
基于能力检测进行浏览器分析

1. 检测特性

可以按照能力将浏览器归类。如果你的应用程序需要使用特定的浏览器能力,那么最好集中检测所 有能力,而不是等到用的时候再重复检测。

// 检测浏览器是否支持 Netscape 式的插件
let hasNSPlugins = !!(navigator.plugins && navigator.plugins.length); 
// 检测浏览器是否具有 DOM Level 1 能力
let hasDOM1 = !!(document.getElementById && document.createElement && 
 document.getElementsByTagName);

一项是确定浏览器是否支持 Netscape 式的插件,另一项是检测浏览器是否具有 DOM Level 1 能力。保存在变量中的布尔值可以用在后面的条件语句中,这样比重复检测省事多了。

2. 检测浏览器

根据不同浏览器独有的行为推断出浏览器的身份。这里故意没有使用 navigator. userAgent 属性,后面会讨论它:

class BrowserDetector {
  constructor() {
    // 测试条件编译
    // IE6~10 支持
    this.isIE_Gte6Lte10 = /*@cc_on!@*/ false;
    // 测试 documentMode
    // IE7~11 支持
    this.isIE_Gte7Lte11 = !!document.documentMode;
    // 测试 StyleMedia 构造函数
    // Edge 20 及以上版本支持
    this.isEdge_Gte20 = !!window.StyleMedia;
    // 测试 Firefox 专有扩展安装 API
    // 所有版本的 Firefox 都支持
    this.isFirefox_Gte1 = typeof InstallTrigger !== "undefined";
    // 测试 chrome 对象及其 webstore 属性
    // Opera 的某些版本有 window.chrome,但没有 window.chrome.webstore
    // 所有版本的 Chrome 都支持
    this.isChrome_Gte1 = !!window.chrome && !!window.chrome.webstore;
    // Safari 早期版本会给构造函数的标签符追加"Constructor"字样,如:
    // window.Element.toString(); // [object ElementConstructor]
    // Safari 3~9.1 支持
    this.isSafari_Gte3Lte9_1 = /constructor/i.test(window.Element);
    // 推送通知 API 暴露在 window 对象上
    // 使用默认参数值以避免对 undefined 调用 toString()
    // Safari 7.1 及以上版本支持
    this.isSafari_Gte7_1 = (({ pushNotification = {} } = {}) =>
      pushNotification.toString() == "[object SafariRemoteNotification]")(
      window.safari
    );
    // 测试 addons 属性
    // Opera 20 及以上版本支持
    this.isOpera_Gte20 = !!window.opr && !!window.opr.addons;
  }
  isIE() {
    return this.isIE_Gte6Lte10 || this.isIE_Gte7Lte11;
  }
  isEdge() {
    return this.isEdge_Gte20 && !this.isIE();
  }
  isFirefox() {
    return this.isFirefox_Gte1;
  }
  isChrome() {
    return this.isChrome_Gte1;
  }
  isSafari() {
    return this.isSafari_Gte3Lte9_1 || this.isSafari_Gte7_1;
  }
  isOpera() {
    return this.isOpera_Gte20;
  }
}

3. 能力检测的局限

通过检测一种或一组能力,并不总能确定使用的是哪种浏览器。以下“浏览器检测”代码(或其他 类似代码)经常出现在很多网站中,但都没有正确使用能力检测:

// 不要这样做!不够特殊
let isFirefox = !!(navigator.vendor && navigator.vendorSub); 
// 不要这样做!假设太多
let isIE = !!(document.all && document.uniqueID);

注意:能力检测最适合用于决定下一步该怎么做,而不一定能够作为辨识浏览器的标志。

用户代理检测

其实就是:navigator.userAgent,用户代理检测被认为是不可 靠的,只应该在没有其他选项时再考虑;因为很可能被篡改;

用户代理的历史(了解)

HTTP 规范(1.0 和 1.1)要求浏览器应该向服务器发送包含浏览器名称和版本信息的简短字符串。

其实就是ua的演变过程,有兴趣可以看看书上说的P(390页)(作为了解)

浏览器分析

想要知道自己代码运行在什么浏览器上,大部分开发者会分析 window.navigator.userAgent返回的字符串值。

相比于能力检测,用户代理检测还是有一定优势的,但是也存在一些缺点

伪造用户代理

现代浏览器都会提供 userAgent 这个只读属性,简单地给这个属性设置其他值不会有效;

如:

console.log(window.navigator.userAgent); 
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) 
Chrome/65.0.3325.181 Safari/537.36 
window.navigator.userAgent = 'foobar'; 
console.log(window.navigator.userAgent); 
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) 
Chrome/65.0.3325.181 Safari/537.36

不过,通过简单的办法可以绕过这个限制。比如,有些浏览器提供伪私有的__defineGetter__方法, 利用它可以篡改用户代理字符串:

console.log(window.navigator.userAgent); 
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) 
Chrome/65.0.3325.181 Safari/537.36 
window.navigator.__defineGetter__('userAgent', () => 'foobar'); 
console.log(window.navigator.userAgent); 
// foobar

所以说这个UA还是不可靠的

分析浏览器

通过解析浏览器返回的用户代理字符串,可以极其准确地推断出下列相关的环境信息:

  •  浏览器
  •  浏览器版本
  •  浏览器渲染引擎
  •  设备类型(桌面/移动)
  •  设备生产商
  •  设备型号
  •  操作系统
  •  操作系统版本

因为新浏览器、新操作系统和新硬件设备随时可能出现,其中很多可能有着类似但并不相同的用户代理字符串。UA频繁变化,推荐一些 GitHub 上维护比较频繁的第三方用户代理解析程序

软件与硬件检测(了解)

现代浏览器提供了一组与页面执行环境相关的信息,包括浏览器、操作系统、硬件和周边设备信息。这些属性可以通过暴露在 window.navigator 上的一组 API 获得。不过,这些 API 的跨浏览器支持还不够好,远未达到标准化的程度。

识别浏览器与操作系统
  • navigator.oscpu
  • navigator.vendor
  • navigator.platform
  • screen.colorDepth 和 screen.pixelDepth
  • screen.orientation
浏览器元数据

Geolocation API

navigator.geolocation 属性暴露了 Geolocation API,可以让浏览器脚本感知当前设备的地理位 置。这个 API 只在安全执行环境(通过 HTTPS 获取的脚本)中可用。

根据宿主系统的硬件和配置,返回结果的精度可能不一样。手机 GPS 的坐标系统可能具有极高的精度,而 IP 地址的精度就要差很多。

Connection State 和 NetworkInformation API

浏览器会跟踪网络连接状态并以两种方式暴露这些信息:连接事件和 navigator.onLine 属性。 在设备连接到网络时,浏览器会记录这个事实并在 window 对象上触发 online 事件。

这个属性返回一个布尔值,表示浏览器是否联网。

const connectionStateChange = () => console.log(navigator.onLine); 
window.addEventListener('online', connectionStateChange); 
window.addEventListener('offline', connectionStateChange); 
// 设备联网时:
// true 
// 设备断网时:
// false

navigator 对象还暴露了 NetworkInformation API,可以通过 navigator.connection 属性使用。

Battery Status API 浏览器可以访问设备电池及充电状态的信息。navigator.getBattery()方法会返回一个期约实 例,解决为一个 BatteryManager 对象。

navigator.getBattery().then((b) => console.log(b));

硬件

浏览器检测硬件的能力相当有限。不过,navigator 对象还是通过一些属性提供了基本信息。

  1. 处理器核心数
  2. 设备内存大小
  3. 最大触点数

小结

客户端检测是 JavaScript 中争议最多的话题之一。因为不同浏览器之间存在差异,所以经常需要根据浏览器的能力来编写不同的代码。客户端检测有不少方式,但下面两种用得最多。

  • 能力检测,在使用之前先测试浏览器的特定能力。
  • 用户代理检测,通过用户代理字符串确定浏览器。

在选择客户端检测方法时,首选是使用能力检测。特殊能力检测要放在次要位置,作为决定代码逻辑的参考。用户代理检测是最后一个选择,因为它过于依赖用户代理字符串。

浏览器也提供了一些软件和硬件相关的信息。这些信息通过 screen 和 navigator 对象暴露出来。利用这些 API,可以获取关于操作系统、浏览器、硬件、设备位置、电池状态等方面的准确信息。