跳到主要内容

客户端检测

能力检测

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

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

测试最常用的方案可以优化代 码执行,这是因为在多数情况下都可以避免无谓检测。 其次是必须检测切实需要的特性。某个能力存在并不代表别的能力也存在。

安全能力检测

进行能力检测时应该尽量使用 typeof 操作符,但光有它还不够。尤其是某些宿主对象并不保证对 typeof 测试返回合理的值。最有名的例子就是 Internet Explorer ( IE )。

基于能力检测进行浏览器分析

检测特性

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

检测浏览器

可以根据对浏览器特性的检测并与已知特性对比,确认用户使用的是什么浏览器。这样可以获得比 用户代码嗅探(稍后讨论)更准确的结果。

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 支持 t
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;
}
}

这个类暴露的通用浏览器检测方法使用了检测浏览器范围的能力测试。随着浏览器的变迁及发展, 可以不断调整底层检测逻辑,但主要的 API 可以保持不变。

能力检测的局限

通过检测一种或一组能力,并不总能确定使用的是哪种浏览器。

用户代理检测

用户代理检测通过浏览器的用户代理字符串确定使用的是什么浏览器。用户代理字符串包含在每个 HTTP 请求的头部,在 JavaScript 中可以通过 navigator.userAgent 访问。在服务器端,常见的做法 是根据接收到的用户代理字符串确定浏览器并执行相应操作。而在客户端,用户代理检测被认为是不可 靠的,只应该在没有其它选项时再考虑。

软件与硬件检测

识别浏览器与操作系统

浏览器元数据

navigator 对象暴露出一些 API ,可以提供浏览器和操作系统的状态信息。

Geolocation API

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

这个 API 可以查询宿主系统并尽可能精确地返回设备的位置信息。根据宿主系统的硬件和配置,返回结果的精度可能不一样。手机 GPS 的坐标系统可能具有极高的精度,而 IP 地址的精度就要差很多。

浏览器也可能会利用 Google Location Service ( Chrome 和 Firefox )等服务确定位置。 有时候,你可能会发现自己并没有 GPS ,但浏览器给出的坐标却非常精确。浏览器会收集 所有可用的无线网络,包括 Wi-Fi 和蜂窝信号。拿到这些信息后,再去查询网络数据库。 这样就可以精确地报告出你的设备位置。

要获取浏览器当前的位置,可以使用 getCurrentPosition()方法。这个方法返回一个

Coordinates 对象,其中包含的信息不一定完全依赖宿主系统的能力:

// getCurrentPosition()会以 position 对象为参数调用传入的回调函数
navigator.geolocation.getCurrentPosition(position => (p = position));

这个 position 对象中有一个表示查询时间的时间戳,以及包含坐标信息的 Coordinates 对象:

console.log(p.timestamp); // 1525364883361
console.log(p.coords); // Coordinates {...}

Coordinates 对象中包含标准格式的经度和纬度,以及以米为单位的精度。精度同样以确定设备位置的机制来判定。

console.log(p.coords.latitude, p.coords.longitude); // 37.4854409, -122.2325506

console.log(p.coords.accuracy); // 58

Coordinates 对象包含一个 altitude (海拔高度)属性,是相对于 1984 世界大地坐标系( World Geodetic System , 1984)地球表面的以米为单位的距离。此外也有一个 altitudeAccuracy 属性,这 个精度值单位也是米。为了取得 Coordinates 中包含的这些信息,当前设备必须具备相应的能力(比 如 GPS 或高度计)。很多设备因为没有能力测量高度,所以这两个值经常有一个或两个是空的。

console.log(p.coords.altitude); // -8.800000190734863
console.log(p.coords.altitudeAccuracy); // 200

Coordinates 对象包含一个 speed 属性,表示设备每秒移动的速度。还有一个 heading (朝向) 属性,表示相对于正北方向移动的角度( 0 ≤ heading 360)。为获取这些信息,当前设备必须具备相 应的能力(比如加速计或指南针)。很多设备因为没有能力测量高度,所以这两个值经常有一个是空的, 或者两个都是空的。

获取浏览器地理位置并不能保证成功。因此 getCurrentPosition()方法也接收失败回调函数作 为第二个参数,这个函数会收到一个 PositionError 对象。在失败的情况下, PositionError 对象 中会包含一个 code 属性和一个 message 属性,后者包含对错误的简短描述。 code 属性是一个整数, 表示以下 3 种错误。

  • PERMISSION_DENIED :浏览器未被允许访问设备位置。页面第一次尝试访问 Geolocation API 时,浏览器会弹出确认对话框取得用户授权(每个域分别获取)。如果返回了这个错误码,则要 么是用户不同意授权,要么是在不安全的环境下访问了 Geolocation API 。 message 属性还会提 供额外信息。

  • POSITION_UNAVAILABLE :系统无法返回任何位置信息。这个错误码可能代表各种失败原因, 但相对来说并不常见,因为只要设备能上网,就至少可以根据 IP 地址返回一个低精度的坐标

  • TIMEOUT :系统不能在超时时间内返回位置信息

// 浏览器会弹出确认对话框请用户允许访问 Geolocation API
/// 这个例子显示了用户拒绝之后的结果
navigator.geolocation.getCurrentPosition(
() => {},
e => {
console.log(e.code); // 1
console.log(e.message); // User denied Geolocation
},
);
// 这个例子展示了在不安全的上下文中执行代码的结果
navigator.geolocation.getCurrentPosition(
() => {},
e => {
console.log(e.code); // 1
console.log(e.message); // Only secure origins are allowed
},
);

Geolocation API 位置请求可以使用 PositionOptions 对象来配置,作为第三个参数提供。

  • enableHighAccuracy :布尔值, true 表示返回的值应该尽量精确,默认值为 false 。默认情 况下,设备通常会选择最快、最省电的方式返回坐标。这通常意味着返回的是不够精确的坐标。 比如,在移动设备上,默认位置查询通常只会采用 Wi-Fi 和蜂窝网络的定位信息。而在 enableHighAccuracy 为 true 的情况下,则会使用设备的 GPS 确定设备位置,并返回这些值 的混合结果。使用 GPS 会更耗时、耗电,因此在使用 enableHighAccuracy 配置时要仔细权 衡一下。
  • timeout :毫秒,表示在以 TIMEOUT 状态调用错误回调函数之前等待的最长时间。默认值是 0xFFFFFFFF ( 232 – 1)。 0 表示完全跳过系统调用而立即以 TIMEOUT 调用错误回调函数
  • maximumAge :毫秒,表示返回坐标的最长有效期,默认值为 0。因为查询设备位置会消耗资源, 所以系统通常会缓存坐标并在下次返回缓存的值(遵从位置缓存失效策略)。系统会计算缓存期, 如果 GeolocationAPI 请求的配置要求比缓存的结果更新,则系统会重新查询并返回值。 0 表示强 制系统忽略缓存的值,每次都重新查询。而 Infinity 会阻止系统重新查询,只会返回缓存的 值。 JavaScript 可以通过检查 Position 对象的 timestamp 属性值是否重复来判断返回的是不 是缓存值。

Connection State 和 NetworkInformation API

浏览器会跟踪网络连接状态并以两种方式暴露这些信息:连接事件和 navigator.onLine 属性。 在设备连接到网络时,浏览器会记录这个事实并在 window 对象上触发 online 事件。相应地,当设备断开网络连接后,浏览器会在 window 对象上触发 offline 事件。任何时候,都可以通过 navigator. onLine 属性来确定浏览器的联网状态。这个属性返回一个布尔值,表示浏览器是否联网。

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

以下是 NetworkInformation API 暴露的属性。

  • downlink :整数,表示当前设备的带宽(以 MB/s 为单位),舍入到最接近的 25KB/s 。这个值 可能会根据历史网络吞吐量计算,也可能根据连接技术的能力来计算。
  • downlinkMax :整数,表示当前设备最大的下行带宽(以 MB/s 为单位),根据网络的第一跳来 确定。因为第一跳不一定反映端到端的网络速度,所以这个值只能用作粗略的上限值。
  • effectiveType :字符串枚举值,表示连接速度和质量。这些值对应不同的蜂窝数据网络连接 技术,但也用于分类无线网络。这个值有以下 4 种可能
    • slow-2g
      • 往返时间 > 2000ms
      • 下行带宽 < KB/s
    • 2g
      • 2000ms > 往返时间 ≥ 1400ms
      • KB/s > 下行带宽 ≥ KB/s
    • 3g
      • 1400ms > 往返时间 ≥ 270ms
      • 700kB/s > 下行带宽 ≥ 700KB/s
    • 4g
      • 270ms > 往返时间 ≥ 0ms
      • 下行带宽 ≥ 700KB/s
  • rtt :毫秒,表示当前网络实际的往返时间,舍入为最接近的 25 毫秒。这个值可能根据历史网 络吞吐量计算,也可能根据连接技术的能力来计算
    • type :字符串枚举值,表示网络连接技术。这个值可能为下列值之一
      • bluetooth :蓝牙
      • cellular :蜂窝
      • ethernet :以太网
      • none :无网络连接。相当于 navigator.onLine === false
      • mixed :多种网络混合
      • other :其它
      • unknown :不确定
      • wifi : Wi-Fi
      • wimax : WiMAX
  • saveData :布尔值,表示用户设备是否启用了"节流"( reduced data )模式。
  • onchange :事件处理程序,会在任何连接状态变化时激发一个 change 事件。可以通过 navigator.connection.addEventListener('change',changeHandler)或 navigator.connection. onchange = changeHandler 等方式使用

Battery Status API

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

navigator.getBattery().then(b => console.log(b));
// BatteryManager { ... }

BatteryManager 包含 4 个只读属性,提供了设备电池的相关信息

  • charging :布尔值,表示设备当前是否正接入电源充电。如果设备没有电池,则返回 true
  • chargingTime :整数,表示预计离电池充满还有多少秒。如果电池已充满或设备没有电池,则 返回 0
  • dischargingTime :整数,表示预计离电量耗尽还有多少秒。如果设备没有电池,则返回 Infinity
  • level :浮点数,表示电量百分比。电量完全耗尽返回 0.0,电池充满返回 1.0。如果设备没有电 池,则返回 1.0

这个 API 还提供了 4 个事件属性,可用于设置在相应的电池事件发生时调用的回调函数。可以通过 给 BatteryManager 添加事件监听器,也可以通过给事件属性赋值来使用这些属性。

  • onchargingchange

  • onchargingtimechange

  • ondischargingtimechange

  • onlevelchange

navigator.getBattery().then(battery => {
// 添加充电状态变化时的处理程序
const chargingChangeHandler = () => console.log('chargingChange');
battery.onchargingchange = chargingChangeHandler;

// 或
battery.addEventListener('chargingChange', chargingChangeHandler);
// 添加充电时间变化时的处理程序
const chargingTimeChangeHandler = () => console.log('chargingTimeChange');
battery.onchargingtimechange = chargingTimeChangeHandler; // 或
battery.addEventListener('chargingTimeChange', chargingTimeChangeHandler);
// 添加放电时间变化时的处理程序 const
dischargingTimeChangeHandler = () => console.log('dischargingTimeChange');
battery.ondischargingtimechange = dischargingTimeChangeHandler;
// 或
battery.addEventListener(
'dischargingTimeChange',
dischargingTimeChangeHandler,
); //添加电量百分比变化时的处理程序
const levelChangeHandler = () => console.log('levelChange');
battery.onlevelchange = levelChangeHandler; //
或 battery.addEventListener('levelChange', levelChangeHandler);
});