如今的浏览器虽然接口已大致统一,但是每家浏览器难免都有自己的“想法”,于是会出现个别的不统一,这些差异迫使Web开发者自己去设计兼容这些差异,客户端检测就是最常见的检测手段,通过检测结果来进一步克服和避免这些缺陷。

客户端检测可大致分为三种:能力检测,用户代理检测,软件与硬件检测。

能力检测

  • 能力检测也成为特性检测,因为不同浏览器提供的接口不是完全相同,于是可以通过简单的逻辑判断来检测在该浏览器环境下能否调用特定API,同时还能间接判断出浏览器类型。
  • 比如,在IE5之前没有document.getElementById这个DOM方法,但是可以通过document.all来实现相同的功能。于是,可以进行如下的能力检测。
const getElementById = (id) => {
  if(document.getElementById){
    return document.getElementById(id)
  } else if (document.all){
    return document.all[id]
  } else {
    throw new Error('该浏览不支持任何通过ID获取DOM元素的方法')
  }
}
  • 需要注意的是,实现能力检测是一定要落实到具体的功能上,即某个能力的存在并不能代表其他能力也存在
function getWindowWidth() { 
   if (document.all) { // 假设 IE 
   	return document.documentElement.clientWidth; // 不正确的用法!
   } else { 
   	return window.innerWidth; 
   } 
}
  • 比如上述例子,document.all的存在并不能说明documentElement.clientWidth的存在。其实这段代码的本意是通过document.all来判断当前浏览器是不是IE浏览器,事实document.all的存在并不能一定确认该浏览器就是IE浏览器。

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

  • 除了上述可以进行基本的功能检测以外,还可以通过能力检测来进行浏览器的特性支持检测,比如是否支持Netscape插件,是否具有DOM Level 1能力等等。
// 红宝书P384
// 检测浏览器是否支持 Netscape 式的插件
let hasNSPlugins = !!(navigator.plugins && navigator.plugins.length); 
// 检测浏览器是否具有 DOM Level 1 能力
let hasDOM1 = !!(document.getElementById && document.createElement && 
 document.getElementsByTagName);
  • 当然,也可以通过特定的能力检测来判断浏览器的类型,即根据对浏览器特性的检测与已知特性对比,来确认用户使用的是什么浏览器
// 红宝书 P385
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; } 
} 
  • 上述代码会随着浏览器的变迁及发展而不同,不过提供的主要API可以保持不变。

用户代理检测

  • 每个浏览器都包含一串用户代理字符串,在浏览器端,可以通过navigator.userAgent来获得。在服务器端,常见的做法是根据接受到的用户代理字符串来确定浏览器并执行响应操作。而用户代理字符串会在浏览器发起HTTP请求时自动附加在HTTP请求头里,及user-agent字段。

  • 但同时用户代理字符串也是饱受争议的,因为用户代理字段有很长一段时间具有很大的欺诈性,无论是人为伪造,还是各大厂商浏览器自带的用户代理字符串。这牵扯到浏览器的发展史,详细浏览器发展史请参考红宝书P386

  • 为啥会出现连浏览器自带的用户代理字符串都会有欺诈性,原因我大概总结一下:各大厂商研发浏览器时,为了让自己浏览器不被冷落而是快速融入当时的Web环境中而不得不选择的手段。当年Netscape Navigator 3浏览器大火之时,IE3页横空出世,但是网景公司的浏览器的用户代理字段代号是Mozilla,而当时市面上基本所有的开发者都会去检测用户代理字段看其是否为Mozilla,如果IE3的用户代理字段不顺应潮流,那么根本融入不进当时的Web环境,因为几乎没有网页去适配IE3,所以微软才打算,把代号改成和网景浏览器相同的代号Mozilla

  • 同理,后面的浏览器都纷纷效仿这钟做法(除了Opera,但是Opera 9以后还是选择了妥协)。但是这违背了用户代理字段设计出来的初衷,于是为了增加浏览器的辨识度,浏览器厂商考虑在用户代理字符串中再加入一段特定的标识符,用来说明浏览器类型,比如Chrome的userAgent如下所示。

> navigator.userAgent
//'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36'
  • 所以因为这段历史,目前市面上所有的浏览器的用户代理字符串开头的代号都是Mozilla

软件与硬件检测

  • 除开浏览器功能上的检测,浏览器还提供了许多操作系统,硬件和周边设备的信息,这些属性都暴露在window.navigator上。
  • 这一部分的接口很多,提供了诸如地址信息,硬件内存,当前网络情况等信息的API接口,这里不一一列举,需要用的时候再去查找相关用法。(红宝书P394 - P400)