第 13 章 客户端检测
13.1 能力检测
能力检测(又称特性检测)即在 JavaScript 运行时中使用一套简单的检测逻辑,测试浏览器是否支持某种特性。这种方式不要求事先知道特定浏览器的信息,只需检测自己关心的能力是否存在即可。能力检测的基本模式如下:
if (object.propertyInQuestion) {
// 使用object.propertyInQuestion
}
能力检测的关键是理解两个重要概念。首先,如前所述,应该先检测最常用的方式。其次是必须检测切实需要的特性。某个能力存在并不代表别的能力也存在。
13.1.1 安全能力检测
13.1.2 基于能力检测进行浏览器分析
1.检测特性
可以按照能力将浏览器归类。如果你的应用程序需要使用特定的浏览器能力,那么最好集中检测所有能力,而不是等到用的时候再重复检测。(可以使用惰性函数)
2.检测浏览器
3.能力检测的局限
可以根据对浏览器特性的检测并与已知特性对比,确认用户使用的是什么浏览器。
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.能力检测的局限
13.2 用户代理检测
用户代理检测通过浏览器的用户代理字符串确定使用的是什么浏览器。用户代理字符串包含在每个 HTTP 请求的头部,在 JavaScript 中可以通过 navigator.userAgent
访问。在服务器端,常见的做法是根据接收到的用户代理字符串确定浏览器并执行相应操作。而在客户端,用户代理检测被认为是不可靠的,只应该在没有其他选项时再考虑。
13.2.1 用户代理的历史
13.2.2 浏览器分析
1.伪造用户代理
通过检测用户代理来识别浏览器并不是完美的方式,毕竟这个字符串是可以造假的。只不过实现 window.navigator
对象的浏览器(即所有现代浏览器)都会提供 userAgent
这个只读属性。因此,简单地给这个属性设置其他值不会有效。不过,通过简单的办法可以绕过这个限制。比如,有些浏览器提供伪私有的__defineGetter__
方法,利用它可以篡改用户代理字符串:
console.log(window.navigator.userAgent)
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
window.navigator.__defineGetter__('userAgent', () => 'foobar')
console.log(window.navigator.userAgent) // 'foobar'
2.分析浏览器
通过解析浏览器返回的用户代理字符串,可以极其准确地推断出下列相关的环境信息:
❑ 浏览器
❑ 浏览器版本
❑ 浏览器渲染引擎
❑ 设备类型(桌面/移动)
❑ 设备生产商
❑ 设备型号
❑ 操作系统
❑ 操作系统版本
当然,新浏览器、新操作系统和新硬件设备随时可能出现,其中很多可能有着类似但并不相同的用户代理字符串。因此,用户代理解析程序需要与时俱进,频繁更新,以免落伍。自己手写的解析程序如果不及时更新或修订,很容易就过时了。本书上一版写过一个用户代理解析程序,但这一版并不推荐读者自己从头再写一个。相反,这里推荐一些 GitHub 上维护比较频繁的第三方用户代理解析程序:
❑ Bowser
❑ UAParser.js
❑ Platform.js
❑ CURRENT-DEVICE
❑ Google Closure
❑ Mootools
13.3 软件与硬件检测
13.3.1 识别浏览器与操作系统
1.navigator.oscpu(已弃用)
2.navigator.vendor(已弃用)
3.navigator.platform(已弃用)
4.screen.colorDepth 和 screen.pixelDepth
screen.colorDepth
和 screen.pixelDepth
返回一样的值,即显示器每像素颜色的位深。
根据 CSSOM( CSS 对象模型 ) 视图,为兼容起见,该值总为 24。
5.screen.orientation
screen.orientation
属性返回一个 ScreenOrientation
对象,其中包含 Screen Orientation API 定义的屏幕信息。这里面最有意思的属性是 angle
和 type
,前者返回相对于默认状态下屏幕的角度,后者返回以下 4 种枚举值之一:
❑ portrait-primary
❑ portrait-secondary
❑ landscape-primary
❑ landscape-secondary
13.3.2 浏览器元数据
1.Geolocation API
navigator.geolocation
属性暴露了 Geolocation API,可以让浏览器脚本感知当前设备的地理位置。这个 API 只在安全执行环境(通过 HTTPS 获取的脚本)中可用。
这个 API 可以查询宿主系统并尽可能精确地返回设备的位置信息。根据宿主系统的硬件和配置,返回结果的精度可能不一样。手机 GPS 的坐标系统可能具有极高的精度,而 IP 地址的精度就要差很多。根据 Geolocation API 规范:
地理位置信息的主要来源是 GPS 和 IP 地址、射频识别(RFID)、Wi-Fi 及蓝牙 Mac 地址、GSM/CDMA 蜂窝 ID 以及用户输入等信息。
注意
浏览器也可能会利用 Google Location Service(Chrome 和 Firefox)等服务确定位置。有时候,你可能会发现自己并没有 GPS,但浏览器给出的坐标却非常精确。浏览器会收集所有可用的无线网络,包括 Wi-Fi 和蜂窝信号。拿到这些信息后,再去查询网络数据库。这样就可以精确地报告出你的设备位置。
要获取浏览器当前的位置,可以使用 getCurrentPosition()
方法。这个方法返回一个 Coordinates
对象,其中包含的信息不一定完全依赖宿主系统的能力:
var options = {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
function success(pos) {
var crd = pos.coords // 返回一个定义了当前位置的Coordinates 对象。
var timestamp = pos.timestamp // 返回一个时间戳DOMTimeStamp,这个时间戳表示获取到的位置的时间。
console.log('Your current position is:')
console.log('Latitude : ' + crd.latitude)
console.log('Longitude: ' + crd.longitude)
console.log('altitude: ' + crd.altitude) // 海拔高度
console.log('heading: ' + crd.heading) // 相对于正北方向移动的角度(0 ≤ heading < 360)
console.log('speed: ' + crd.speed) // 设备每秒移动的速度
console.log('altitudeAccuracy: ' + crd.altitudeAccuracy)
console.log('More or less ' + crd.accuracy + ' meters.')
}
function error(err) {
console.warn('ERROR(' + err.code + '): ' + err.message)
}
navigator.geolocation.getCurrentPosition(success, error, options)
获取浏览器地理位置并不能保证成功。因此 ·方法也接收失败回调函数作为第二个参数,这个函数会收到一个 PositionError
对象。在失败的情况下,PositionError
对象中会包含一个 code
属性和一个 message
属性,后者包含对错误的简短描述。code
属性是一个整数,表示以下 3 种错误。
❑ PERMISSION_DENIED:浏览器未被允许访问设备位置。页面第一次尝试访问 Geolocation API 时,浏览器会弹出确认对话框取得用户授权(每个域分别获取)。如果返回了这个错误码,则要么是用户不同意授权,要么是在不安全的环境下访问了 Geolocation API。message 属性还会提供额外信息。
❑ POSITION_UNAVAILABLE:系统无法返回任何位置信息。这个错误码可能代表各种失败原因,但相对来说并不常见,因为只要设备能上网,就至少可以根据 IP 地址返回一个低精度的坐标。
❑ TIMEOUT:系统不能在超时时间内返回位置信息。
Geolocation API 位置请求可以使用 PositionOptions
对象来配置,作为第三个参数提供。这个对象支持以下 3 个属性。
❑ enableHighAccuracy:布尔值,true 表示返回的值应该尽量精确,默认值为 false。默认情况下,设备通常会选择最快、最省电的方式返回坐标。这通常意味着返回的是不够精确的坐标。比如,在移动设备上,默认位置查询通常只会采用 Wi-Fi 和蜂窝网络的定位信息。而在 enableHighAccuracy 为 true 的情况下,则会使用设备的 GPS 确定设备位置,并返回这些值的混合结果。使用 GPS 会更耗时、耗电,因此在使用 enableHighAccuracy 配置时要仔细权衡一下。
❑ timeout:毫秒,表示在以 TIMEOUT 状态调用错误回调函数之前等待的最长时间。默认值是 0xFFFFFFFF(232-1)。0 表示完全跳过系统调用而立即以 TIMEOUT 调用错误回调函数。
❑ maximumAge:毫秒,表示返回坐标的最长有效期,默认值为 0。因为查询设备位置会消耗资源,所以系统通常会缓存坐标并在下次返回缓存的值(遵从位置缓存失效策略)。系统会计算缓存期,如果 Geolocation API 请求的配置要求比缓存的结果更新,则系统会重新查询并返回值。0 表示强制系统忽略缓存的值,每次都重新查询。而 Infinity 会阻止系统重新查询,只会返回缓存的值。JavaScript 可以通过检查 Position 对象的 timestamp 属性值是否重复来判断返回的是不是缓存值。
2.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
navigator
对象还暴露了 NetworkInformation API,可以通过 navigator.connection
属性使用。这个 API 提供了一些只读属性,并为连接属性变化事件处理程序定义了一个事件对象。
❑ downlink:整数,表示当前设备的带宽(以 Mbit/s 为单位),舍入到最接近的 25kbit/s。这个值可能会根据历史网络吞吐量计算,也可能根据连接技术的能力来计算。
❑ downlinkMax:整数,表示当前设备最大的下行带宽(以 Mbit/s 为单位),根据网络的第一跳来确定。因为第一跳不一定反映端到端的网络速度,所以这个值只能用作粗略的上限值。
❑ effectiveType:字符串枚举值,表示连接速度和质量。这些值对应不同的蜂窝数据网络连接技术,但也用于分类无线网络。这个值有以下 4 种可能。
■ slow-2g
➢ 往返时间> 2000ms
➢ 下行带宽< 50kbit/s
■ 2g
➢ 2000ms >往返时间 ≥1400ms
➢ 70kbit/s >下行带宽 ≥50kbit/s
■ 3g
➢ 1400ms >往返时间 ≥270ms
➢ 700kbit/s >下行带宽 ≥70kbit/s
■ 4g
➢ 270ms >往返时间 ≥0ms
➢ 下行带宽 ≥700kbit/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 等方式使用。
3.Battery Status API
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)
})
13.3.3 硬件
1.处理器核心数
navigator.hardwareConcurrency
属性返回浏览器支持的逻辑处理器核心数量,包含表示核心数的一个整数值(如果核心数无法确定,这个值就是 1)。关键在于,这个值表示浏览器可以并行执行的最大工作线程数量,不一定是实际的 CPU 核心数。
2.设备内存大小
navigator.deviceMemory
属性返回设备大致的系统内存大小,包含单位为 GB 的浮点数(舍入为最接近的 2 的幂:512MB 返回 0.5,4GB 返回 4)。
3.最大触点数
navigator.maxTouchPoints
属性返回触摸屏支持的最大关联触点数量,包含一个整数值。