为什么?

虽然浏览器厂商齐心协力想要实现一致的接口,但事实上仍然是每家浏览器都有自己的长处与不足。

跨平台的浏览器尽管版本相同,但总会存在不同的问题。

这些差异迫使 Web 开发者要么面向最大公约数而设计,要么(更常见地)使用各种方法来检测客户端,以克服或避免这些缺陷。

检测 浏览器能力

可以根据对浏览器特性的检测并与已知特性对比,确认用户使用的是什么浏览器。

这个类暴露的通用浏览器检测方法使用了检测浏览器范围的能力测试。

随着浏览器的变迁及发展,可以不断调整底层检测逻辑,但主要的 API 可以保持不变

  1. class BrowserDetector {
  2. constructor() {
  3. // 测试条件编译
  4. // IE6~10 支持
  5. this.isIE_Gte6Lte10 = /*@cc_on!@*/false;
  6. // 测试 documentMode
  7. // IE7~11 支持
  8. this.isIE_Gte7Lte11 = !!document.documentMode;
  9. // 测试 StyleMedia 构造函数
  10. // Edge 20 及以上版本支持
  11. this.isEdge_Gte20 = !!window.StyleMedia;
  12. // 测试 Firefox 专有扩展安装 API
  13. // 所有版本的 Firefox 都支持
  14. this.isFirefox_Gte1 = typeof InstallTrigger !== 'undefined';
  15. // 测试 chrome 对象及其 webstore 属性
  16. // Opera 的某些版本有 window.chrome,但没有 window.chrome.webstore
  17. // 所有版本的 Chrome 都支持
  18. this.isChrome_Gte1 = !!window.chrome && !!window.chrome.webstore;
  19. // Safari 早期版本会给构造函数的标签符追加"Constructor"字样,如:
  20. // window.Element.toString(); // [object ElementConstructor]
  21. // Safari 3~9.1 支持
  22. this.isSafari_Gte3Lte9_1 = /constructor/i.test(window.Element);
  23. // 推送通知 API 暴露在 window 对象上
  24. // 使用默认参数值以避免对 undefined 调用 toString()
  25. // Safari 7.1 及以上版本支持
  26. this.isSafari_Gte7_1 =
  27. (({pushNotification = {}} = {}) =>
  28. pushNotification.toString() == '[object SafariRemoteNotification]'
  29. )(window.safari);
  30. // 测试 addons 属性
  31. // Opera 20 及以上版本支持
  32. this.isOpera_Gte20 = !!window.opr && !!window.opr.addons;
  33. }
  34. isIE() { return this.isIE_Gte6Lte10 || this.isIE_Gte7Lte11; }
  35. isEdge() { return this.isEdge_Gte20 && !this.isIE(); }
  36. isFirefox() { return this.isFirefox_Gte1; }
  37. isChrome() { return this.isChrome_Gte1; }
  38. isSafari() { return this.isSafari_Gte3Lte9_1 || this.isSafari_Gte7_1; }
  39. isOpera() { return this.isOpera_Gte20; }
  40. }

检测 浏览器版本

可以通过navigator.userAgent 访问。如

  1. navigator.userAgent;
  2. //"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36"

一开始这个属性就是展示浏览器及其版本,但因为历史原因,这些信息可能不准确(可能其他浏览器会伪造修改这个信息)

下面为参考例子:
IE9:Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/5.0)
Firefox 4 :Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox 4.0.1
Safari:Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/522.15.5 (KHTML, like Gecko) Version/3.0.3 Safari/522.15.5
Konqueror:Mozilla/5.0 (compatible; Konqueror/3.5; SunOS) KHTML/3.5.0 (like Gecko)
Chrome:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36
Opera:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36 OPR/52.0.2871.64
iOS:Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16
Android:Mozilla/5.0 (Linux; U; Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1

推荐一些 GitHub 上维护比较频繁的第三方解析程序,方便分析检测浏览器版本
Bowser
UAParser.js
Platform.js
CURRENT-DEVICE
Google Closure
Mootools

检测 操作系统及硬件

  1. // 通常表示浏览器所在的操作系统
  2. navigator.platform; // 如"MacIntel"、"Win32"、"FreeBSD i386"或"WebTV OS"

浏览器元数据

Geolocation API 位置信息

可以让浏览器脚本感知当前设备的地理位置。

这个 API 只在安全执行环境(通过 HTTPS 获取的脚本)中可用。

可以查询宿主系统并尽可能精确地返回设备的位置信息。

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

根据 Geolocation API 规范:地理位置信息的主要来源是 GPS 和 IP 地址、射频识别(RFID)、Wi-Fi 及蓝牙 Mac 地址、GSM/CDMA 蜂窝 ID 以及用户输入等信息。

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

  1. // getCurrentPosition()会以 position 对象为参数调用传入的回调函数
  2. navigator.geolocation.getCurrentPosition( // 浏览器会弹出确认对话框请用户允许访问 Geolocation API
  3. (position) => {
  4. p = position
  5. // 操作p
  6. }
  7. );

时间戳和位置

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

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

经纬度

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

  1. console.log(p.coords.latitude, p.coords.longitude); // 37.4854409, -122.2325506
  2. console.log(p.coords.accuracy); // 58

海拔

Coordinates 对象包含一个 altitude(海拔高度)属性
altitudeAccuracy 这个精度值单位也是米
很多设备因为没有能力测量高度,所以这两个值经常有一个或两个是空的。

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

速度

Coordinates 对象包含一个 speed 属性,表示设备每秒移动的速度。
还有一个 heading(朝向)属性,表示相对于正北方向移动的角度(0 ≤ heading < 360)。

为获取这些信息,当前设备必须具备相应的能力(比如加速计或指南针)。很多设备因为没有能力测量高度,所以这两个值经常有一个是空的,或者两个都是空的。

失败回调

获取浏览器地理位置并不能保证成功。

因此 getCurrentPosition()方法也接收失败回调函数作为第二个参数,这个函数会收到一个 PositionError 对象。

在失败的情况下,PositionError 对象中会包含一个 code 属性和一个 message 属性,后者包含对错误的简短描述。

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

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

3、TIMEOUT:系统不能在超时时间内返回位置信息。关于如何配置超时,会在后面介绍。

  1. // 浏览器会弹出确认对话框请用户允许访问 Geolocation API
  2. // 这个例子显示了用户拒绝之后的结果
  3. navigator.geolocation.getCurrentPosition(
  4. (p) => {
  5. // 操作p
  6. },
  7. (e) => {
  8. console.log(e.code); // 1
  9. console.log(e.message); // User denied Geolocation
  10. }
  11. );
  12. // 这个例子展示了在不安全的上下文中执行代码的结果
  13. navigator.geolocation.getCurrentPosition(
  14. () => {},
  15. (e) => {
  16. console.log(e.code); // 1
  17. console.log(e.message); // Only secure origins are allowed
  18. }
  19. );

精确、超时、有效期

Geolocation API 位置请求可以使用 PositionOptions 对象来配置,作为第三个参数提供。这个对象支持以下 3 个属性。

1、enableHighAccuracy:布尔值,true 表示返回的值应该尽量精确,默认值为 false。
默认情况下,设备通常会选择最快、最省电的方式返回坐标。这通常意味着返回的是不够精确的坐标。
比如,在移动设备上,默认位置查询通常只会采用 Wi-Fi 和蜂窝网络的定位信息。而在enableHighAccuracy 为 true 的情况下,则会使用设备的 GPS 确定设备位置,并返回这些值的混合结果。使用 GPS 会更耗时、耗电,因此在使用 enableHighAccuracy 配置时要仔细权衡一下。

2、timeout:毫秒,表示在以 TIMEOUT 状态调用错误回调函数之前等待的最长时间。
默认值是0xFFFFFFFF(232 – 1)。0 表示完全跳过系统调用而立即以 TIMEOUT 调用错误回调函数。

3、maximumAge:毫秒,表示返回坐标的最长有效期,默认值为 0。
因为查询设备位置会消耗资源,所以系统通常会缓存坐标并在下次返回缓存的值(遵从位置缓存失效策略)。
系统会计算缓存期,如果 Geolocation API 请求的配置要求比缓存的结果更新,则系统会重新查询并返回值。
0 表示强制系统忽略缓存的值,每次都重新查询。
而 Infinity 会阻止系统重新查询,只会返回缓存的值。
JavaScript 可以通过检查 Position 对象的 timestamp 属性值是否重复来判断返回的是不是缓存值

Connection State 和 NetworkInformation API

网络连接状态

浏览器会跟踪网络连接状态并以两种方式暴露这些信息:连接事件和 navigator.onLine 属性。

在设备连接到网络时,浏览器会记录这个事实并在 window 对象上触发 online 事件。

断开网络连接后,浏览器会在 window 对象上触发 offline 事件。

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

有些浏览器可能会认为只要连接到局域网就算“在线”,而不管是否真正接入了互联网。

网络连接信息

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

这个 API 提供了一些只读属性,并为连接属性变化事件处理程序定义了一个事件对象。

  1. navigator.connection
  2. /*NetworkInformation {
  3. //
  4. onchange: null,
  5. effectiveType: "4g",
  6. rtt: 50,
  7. // 整数,表示当前设备的带宽(以 Mbit/s 为单位),舍入到最接近的 25kbit/s。
  8. // 这个值可能会根据历史网络吞吐量计算,也可能根据连接技术的能力来计算
  9. downlink: 10,
  10. saveData: false
  11. }
  12. */

downlink:整数,表示当前设备的带宽(以 Mbit/s 为单位),舍入到最接近的 25kbit/s。
这个值可能会根据历史网络吞吐量计算,也可能根据连接技术的能力来计算。

downlinkMax:整数,表示当前设备最大的下行带宽(以 Mbit/s 为单位),根据网络的第一跳来确定。
因为第一跳不一定反映端到端的网络速度,所以这个值只能用作粗略的上限值。

effectiveType:字符串枚举值,表示连接速度和质量。这些值对应不同的蜂窝数据网络连接技术,但也用于分类无线网络。
这个值有以下 4 种可能:
1、slow-2g
往返时间 > 2000ms
下行带宽 < 50kbit/s
2、2g
 2000ms > 往返时间 ≥ 1400ms
 70kbit/s > 下行带宽 ≥ 50kbit/s
3、3g
1400ms > 往返时间 ≥ 270ms
700kbit/s > 下行带宽 ≥ 70kbit/s
4、4g
270ms > 往返时间 ≥ 0ms
下行带宽 ≥ 700kbit/s

rtt:毫秒,表示当前网络实际的往返时间,舍入为最接近的 25 毫秒。
这个值可能根据历史网络吞吐量计算,也可能根据连接技术的能力来计算。

type:字符串枚举值,表示网络连接技术。这个值可能为下列值之一。
1、bluetooth:蓝牙。
2、cellular:蜂窝。
3、ethernet:以太网。
4、none:无网络连接。相当于 navigator.onLine === false。
5、mixed:多种网络混合。
6、other:其他。
7、unknown:不确定。
8、wifi:Wi-Fi。  wimax:WiMAX。

saveData:布尔值,表示用户设备是否启用了“节流”(reduced data)模式。

onchange:事件处理程序,会在任何连接状态变化时激发一个 change 事件。
可以通过 navigator. connection.addEventListener(‘change’,changeHandler)或navigator.connection.onchange = changeHandler 等方式使用

Battery Status API 电池

浏览器可以访问设备电池及充电状态的信息

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

BatteryManager 包含 4 个只读属性,提供了设备电池的相关信息。
1、charging:布尔值,表示设备当前是否正接入电源充电。如果设备没有电池,则返回 true。
2、chargingTime:整数,表示预计离电池充满还有多少秒。如果电池已充满或设备没有电池,则返回 0。
3、dischargingTime:整数,表示预计离电量耗尽还有多少秒。如果设备没有电池,则返回 Infinity。
4、level:浮点数,表示电量百分比。电量完全耗尽返回 0.0,电池充满返回 1.0。如果设备没有电池,则返回 1.0。

这个 API 还提供了 4 个事件属性,可用于设置在相应的电池事件发生时调用的回调函数。

可以通过给 BatteryManager 添加事件监听器,也可以通过给事件属性赋值来使用这些属性。

  1. navigator.getBattery().then((battery) => {
  2. // 添加充电状态变化时的处理程序
  3. const chargingChangeHandler = () => console.log('chargingchange');
  4. battery.onchargingchange = chargingChangeHandler;
  5. // 或
  6. battery.addEventListener('chargingchange', chargingChangeHandler);
  7. // 添加充电时间变化时的处理程序
  8. const chargingTimeChangeHandler = () => console.log('chargingtimechange');
  9. battery.onchargingtimechange = chargingTimeChangeHandler;
  10. // 或
  11. battery.addEventListener('chargingtimechange', chargingTimeChangeHandler);
  12. // 添加放电时间变化时的处理程序
  13. const dischargingTimeChangeHandler = () => console.log('dischargingtimechange');
  14. battery.ondischargingtimechange = dischargingTimeChangeHandler;
  15. // 或
  16. battery.addEventListener('dischargingtimechange', dischargingTimeChangeHandler);
  17. // 添加电量百分比变化时的处理程序
  18. const levelChangeHandler = () => console.log('levelchange');
  19. battery.onlevelchange = levelChangeHandler;
  20. // 或
  21. battery.addEventListener('levelchange', levelChangeHandler);
  22. });

硬件

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

处理器核心数

  1. navigator.hardwareConcurrency // 4 属性返回浏览器支持的逻辑处理器核心数量,包含表示核心

数的一个整数值(如果核心数无法确定,这个值就是 1)。
关键在于,这个值表示浏览器可以并行执行的最大工作线程数量,不一定是实际的 CPU 核心数。

设备内存大小

  1. navigator.deviceMemory //属性返回设备大致的系统内存大小,包含单位为 GB 的浮点数(舍入为最接近的 2 的幂:512MB 返回 0.5,4GB 返回 4)。

最大触点数

  1. navigator.maxTouchPoints //属性返回触摸屏支持的最大关联触点数量,包含一个整数值。