这是由于各家浏览器的具体实现存在差异所导致的。
客户端检测的方式:(1)能力检测;(2)用户代理检测
能力检测
使用API或函数前先检测客户端浏览器有没有该API和函数。
好处是,不需要考虑特定浏览器或版本。
用户代理检测
请求报文里的User-agent字段。通过该字段确定浏览器。
该字段是为了让服务器直到发起请求的客户端的身份,包括浏览器名称和版本。
Mozilla 2.0火起来的时候,微软推出了自家的浏览器IE3,因为服务器在返回资源前都会特意检查用户代理字符串,如果检测到不是Mozilla,那就可能是IE浏览器打不开网页。于是IE浏览器用套壳的方式,把自己的信息插入到用户代理字符串的括号里面,伪装成Mozilla。
此后的Safari,Konqueror,Chrome都模仿此做法,以达到兼容的效果,尽管这违背了User-agent原本用来识别客户端的意图。
Firefox才是Mozilla正统
下面是请求同一网页时的各家浏览器的user-agent
Chrome
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
Firefox
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0
Edge
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36 Edg/95.0.1020.30
Opera
Opera 最近的版本已经改为在更标准的字符串末尾追加 “OPR” 标识符和版本号。这样,除了末尾的 “OPR” 标识符和版本号,字符串的其他部分与 WebKit 浏览器是类似的。
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 OPR/80.0.4170.63
iOS和Android移动操作系统上默认的浏览器都是基于WebKit的,因此具有与相应桌面浏览器一样的用户代理字符串。
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设备
这个用户代理字符串是谷歌 Nexus One 手机上的默认浏览器的。不过,其他 Android 设备上的浏览
器也遵循相同的模式。与iOS设备相比,一样有“Mobile”标识,但是没有后面的版本号。
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
渲染引擎和JavaScript引擎
渲染引擎起源于开源渲染引擎KHTML,它是linux平台浏览器Konqueror使用的渲染引擎。
苹果基于KHTML开发了渲染引擎WebKit
Gecko是Firefox的核心,最初是作为通用Mozilla浏览器(即后来的Netscape 6)的一部分开发的。
谷歌的 Chrome浏览器使用 Blink 作为渲染引擎,使用 V8 作为 JavaScript 引擎。Chrome 的用户代理
字符串包含所有 WebKit 的信息,另外又加上了 Chrome及其版本的信息。
Presto 是 Opera 的渲染引擎
浏览器分析
- 可以通过分析window.navigator.userAgent返回的字符串值来判断浏览器类型,获得关于浏览器和操作系统的比较精确的结果。
- 而能力检测,则只关注客户端能不能正常执行,可以什么方法完成我想要做的事情,而不在乎它是谁。
伪造用户代理
经检验,Chrome、Firefox、Opera和Edge都可以通过这种方式篡改用户代理字符串。如果相信浏览器返回的用户代理字符串,那就可以用它来判断浏览器。如果怀疑脚本或浏览器可能篡改这个值,那最好还是使用能力检测。window.navigator.__defineGetter__("userAgent",()=>'foobar');
console.log(window.navigator.userAgent); //foobar
当然,新浏览器、新操作系统和新硬件设备随时可能出现,其中很多可能有着类似但并不相同的用
户代理字符串。因此,用户代理解析程序需要与时俱进,频繁更新,以免落伍。自己手写的解析程序如
果不及时更新或修订,很容易就过时了。本书上一版写过一个用户代理解析程序,但这一版并不推荐读
者自己从头再写一个。相反,这里推荐一些GitHub 上维护比较频繁的第三方用户代理解析程序:
- Bowser
- UAParser.js
- Platform.js
- CURRENT-DEVICE
- Google Closure
- Mootools
Mozilla 维基有一个页面“Compatibility/UADetectionLibraries”,其中提供了用户代理解析程序的列表,可以用来识别 Mozilla 浏览器(甚至所有主流浏览器)。这些解析程序是按照语言分组的。这个页面好像维护不频繁,但其中给出了所有主流的解析库。(注意JavaScript 部分包含客户端库和 Node.js库。)GitHub 上的文章“Are We Detectable Yet?”中还有一张可视化的表格,能让我们对这些库的检测能力一目了然
软硬件检测
识别浏览器和操作系统
navigator.oscpu
返回一个字符串,通常对应用户代理字符串中操作系统/系统架构相关信息,经测试,Chrome、Edge、Opera和Firefox四种浏览器,只有Firefox的navigator拥有该属性,其余三种都没有。Firefox的返回信息如下:navigator.hasOwnProperty("oscpu");
//"Windows NT 10.0; Win64; x64"
navigator.userAgent
//"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0"
navigator.vendor
返回一个字符串,包含浏览器开发商的信息。可能返回的是空字符串,”Google Inc”,”Apple Computer,Inc”。Chrome、Opera、Edge都返回了”Google Inc”;Firefox返回了空字符串。navigator.platform
返回一个字符串,通常表示浏览器所在的平台。例如,Windows系统下 Chrome 中的这个 navigator.platform 属性返回下面的字符串:console.log(navigator.platform); // "Win32"
问题:为什么返回win32而不是win64
W3C的解释:
曾经的定义:
navigator.platform indicates the machine type for which the browser was compiled.(指为其浏览器编译的机器类型)
现在的定义:
Must return either the empty string or a string representing the platform on which the browser is executing, e.g. “MacIntel”, “Win32”, “FreeBSD i386”, “WebTV OS”.
所以回答上面的问题:尽管用户的window系统可能是64位的,但依然按照32位来编译浏览器,这意味着就Windows而言,它们一直遵循旧的定义。
相关讨论:What is the list of possible values for navigator.platform as of today?
[
screen.colorDepth
和screen.pixelDepth
返回一样的值,即显示器每像素颜色的位深,一般为24。根据
CSS 对象模型(CSSOM)规范:
screen.colorDepth 和 screen.pixelDepth 属性应该返回输出设备中每像素用于显示颜色的位数,不包含 alpha 通道。
screen.orientation
属性返回一个 ScreenOrientation 对象,其中包含 Screen Orientation API
定义的屏幕信息。这里面最有意思的属性是 angle 和 type ,前者返回相对于默认状态下屏幕的角度,
后者返回以下 4 种枚举值之一:
portrait-primary
portrait-secondary
landscape-primary
landscape-secondary
根据规范,这些值的初始化取决于浏览器和设备状态。因此,不能假设 portrait-primary 和 0度始终是初始值。这两个值主要用于确定设备旋转后浏览器的朝向变化。
浏览器元数据
- 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 和蜂窝信号。拿到这些信息后,再去查询网络数据库。这样就可以精确地报告出你的设备位置。
//获取地址信息
navigator.geolocation.getCurrentPosition(
(position)=>p = position,
(err)=>console.log(err.code,err.message)
);
console.log(p);
getCurrentPosition
的第一个回调函数是成功时调用的函数,在第一次尝试访问Geolocation API时,浏览器会弹出对话框以获取用户授权(每个域独立,分别获取),用户授权后则调用第一个回调函数。如果用户不授权,或者在不安全的环境下访问了Geolocation API,则会以失败结果调用第二个回调函数。
成功时回调函数的参数 position 对象中有一个表示查询时间的时间戳,以及包含坐标信息的 Coordinates 对象:
- timestamp:表示查询时间的时间戳。根据时间戳可以判断是不是缓存的信息。
- Coordinates中的一些属性:
- accuracy:坐标精度。值越大,越精确。
- latitude:纬度
- longtitude:经度
- altitude:海拔高度
- speed:设备移动速度
失败时,第二个回调函数会接收到一个PositionError对象。在失败的情况下, PositionError 对象中会包含一个 code 属性和一个 message 属性,后者包含对错误的简短描述。 code 属性是一个整数,表示以下 3 种错误:
- PERMISSION_DENIED:要么不被授权,要么在不安全的环境下访问了Geolocaation API。
- POSITION_UNAVAILABLE:系统无法返回任务位置信息。
- TIMEOUT:系统在超时时间内无法返回位置信息。
getCurrentPosition
的第三个可选参数是PositionOptions对象。
var positionOptions = {
enableHighAccuracy:false,
timeout:0xffffffff,
maximumAge:0
}
以上均是默认值,enableHighAccuracy
为true时,表示要求高精度的位置信息,timeout
则是超时时间,单位为毫秒,maximumAge
单位毫秒,表示返回坐标的最长有效期。
- 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);
但有些浏览器可能会认为连入局域网也算是“在线”,而不管是不是真正连入了互联网。
navigator 对象还暴露了 NetworkInformation API,可以通过 navigator.connection 属性使用。这个 API 提供了一些只读属性,并为连接属性变化事件处理程序定义了一个事件对象。
以下是 NetworkInformation 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 。
注意到Firefox的navigator并未提供NetworkInformation API。
调用的例子:
navigator.connection.downlink; //10(chrome) 10(opera) 0.6(Edge)
Edge:
Chrome:
Opera:
- 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 对象还是通过一些属性提供了基本信息。
- 处理器核心数
navigator.hardwareConcurrency
属性返回浏览器支持的逻辑处理器核心数量,包含表示核心
数的一个整数值(如果核心数无法确定,这个值就是 1)。关键在于,这个值表示浏览器可以并行执行的
最大工作线程数量,不一定是实际的 CPU 核心数。 - 设备内存大小
navigator.deviceMemory
属性返回设备大致的系统内存大小,包含单位为 GB 的浮点数(舍入
为最接近的 2 的幂:512MB 返回 0.5,4GB 返回 4)。但也并不准确,因为我的笔记本本来是8G内存,但我添加了一个8G的内存条,总共16G,结果值却是8。 - 最大触点数
navigator.maxTouchPoints
属性返回触摸屏支持的最大关联触点数量,包含一个整数值。非触摸笔记本屏幕也会返回值,我的是20。
小结
在选择客户端检测方法时,首选是使用能力检测。特殊能力检测要放在次要位置,作为决定代码逻
辑的参考。用户代理检测是最后一个选择,因为它过于依赖用户代理字符串。
利用用户代理作为客户端检测的工具库:
- Bowser
- UAParser.js
- Platform.js
- CURRENT-DEVICE
- Google Closure
- Mootools