13.1 能力检测

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

  1. if (object.propertyInQuestion) {
  2. // 使用object.propertyInQuestion
  3. }

比如,IE5之前的版本中没有document.getElementById()这个DOM方法,但可以通过document.all属性实现同样的功能。
为此,可以进行如下能力检测:

  1. function getElement(id) {
  2. if (document.getElementById) {
  3. return document.getElementById(id);
  4. } else if (document.all) {
  5. return document.all[id];
  6. } else {
  7. throw new Error('No way to retrive element!');
  8. }
  9. }

getElement()函数的目的是根据给定的ID获取元素。
因为标准的方式是使用document. getElementById(),所以首先测试它。
如果这个函数存在(不是undefined),那就使用这个方法;
否则检测document.all是否存在,如果存在则使用。
如果这两个能力都不存在(基本上不可能),则抛出错误说明功能无法实现。
能力检测的关键是理解两个重要概念:
首先,应先检测最常用的方式。
在前面的例子中就是先检测document.getElementById()再检测document.all。
测试最常用的方案可以优化代码执行,这是因为在多数情况下都可以避免无谓检测。
其次是必须检测切实需要的特性。
某个能力存在并不代表别的能力也存在。

  1. function getWindowWidth() {
  2. if (document.all) { // 假设IE
  3. return document.documentElement.clientWidth;
  4. // 不正确的用法!
  5. } else {
  6. return window.innerWidth;
  7. }
  8. }

展示了不正确的能力检测方式。
getWindowWidth()函数首先检测document.all是否存在,如果存在则返回document.documentElement.clientWidth,理由是IE8及更低版本不支持window.innerWidth。
这个例子的问题在于检测到document.all存在并不意味着浏览器是IE。
事实,也可能是某个早期版本的Opera,既支持document.all也支持windown.innerWidth。

13.1.1 安全能力检测

能力检测最有效的场景:检测能力是否存在的同时,验证其是否能够展现出预期的行为。
前一节中的例子依赖将测试对象的成员转换类型,然后再确定它是否存在。
虽然这样能够确定检测的对象成员存在,但不能确定它就是你想要的。
来看下面的例子,这个函数尝试检测某个对象是否可以排序:

  1. // 不要这种做!错误的能力检测,只能检测到能力是否存在
  2. function isSortable(object) {
  3. return !!object.sort;
  4. }

这个函数尝试通过检测,对象上是否有sort()方法,来确定它是否支持排序。
问题在于,即使这个对象有一个sort属性,这个函数也会返回true:

  1. let result = isSortable({ sort: true });

简单地测试到一个属性存在,并不代表这个对象就可以排序。
更好的方式是检测sort是不是函数:

  1. // 好一些,检测sort是不是函数
  2. function isSortable(object) {
  3. return typeof object.sort == 'function';
  4. }

使用的typeof操作符可以确定sort是不是函数,从而确认是否可以调用它对数据进行排序。
进行能力检测时应尽量使用typeof操作符,但光有它还不够。尤其是某些宿主对象并不保证对typeof测试返回合理的值。最有名的例子就是InternetExplorer(IE)。在多数浏览器中,下面的代码都会在document.createElement()存在时返回true:

  1. // 不适用于IE8及更低版本
  2. function hasCreateElement() {
  3. return typeof document.createElement == 'function';
  4. }

但在IE8及更低版本中,这个函数会返回false。
这是因为typeofdocument.createElement返回”object”而非”function”。
前面提到过,DOM对象是宿主对象,而宿主对象在IE8及更低版本中是通过COM而非JScript实现的。
因此,document.createElement()函数被实现为COM对象,typeof返回”object”。
IE9对DOM方法会返回”function”。

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

恰当地使用能力检测可以精准地分析运行代码的浏览器。
使用能力检测而非用户代理检测的优点在于:伪造用户代理字符串很简单,而伪造能够欺骗能力检测的浏览器特性却很难。

1.检测特性

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

  1. // 检测浏览器是否支持Netscape式的插件
  2. let hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);
  3. // 检测浏览器是否具有DOM Level1能力
  4. let hasDOM1 = !!(document.getElementById && document.createElement && document.getElementsByTagName);

完成了两项检测:
一项是确定浏览器是否支持Netscape式的插件,另一项是检测浏览器是否具有DOM Level 1能力。
保存在变量中的布尔值可以用在后面的条件语句中,这样比重复检测省事多了。

2.检测浏览器

可以根据对浏览器特性的检测并与已知特性对比,确认用户使用的是什么浏览器。
这样可以获得比用户代码嗅探(稍后讨论)更准确的结果。
但未来的浏览器版本可能不适用于这套方案。

3.能力检测的局限

通过检测一种或一组能力,并不总能确定使用的是哪种浏览器。
以下“浏览器检测”代码(或其他类似代码)经常出现在很多网站中,但都没有正确使用能力检测:

  1. // 不要这样做!不够特殊
  2. let isFirefox = !!(navigator.vendor && navigator.vendorSub);
  3. // 不要这样,假设太多
  4. let isIE = !!(document.all && document.uniqueID);

这是错误使用能力检测的典型示例。
过去,Firefox可以通过navigator.vendor和navigator. vendorSub来检测,但后来Safari也实现了同样的属性,于是这段代码就会产生误报。
为确定IE,这段代码检测了document.all和document.uniqueID。这是假设IE将来的版本中还会继续存在这两个属性,而且其他浏览器也不会实现它们。
不过这两个检测都使用双重否定操作符来产生布尔值(这样可以生成便于存储和访问的结果)。
注:能力检测最适合用于决定下一步该怎么做,而不一定能够作为辨识浏览器的标志。
13.2 用户代理检测
用户代理检测通过浏览器的用户代理字符串,确定使用的是什么浏览器。
用户代理字符串包含在每个HTTP请求的头部,在JavaScript中可以通过navigator.userAgent访问。
在服务器端,常见的做法:根据接收到的用户代理字符串,确定浏览器并执行相应操作。
在客户端,用户代理检测被认为是不可靠的,只应该在没有其他选项时再考虑。
用户代理字符串最受争议的地方:在很长一段时间里,浏览器都通过在用户代理字符串包含错误或误导性信息来欺骗服务器。
要理解背后的原因,必须回顾一下自Web出现之后用户代理字符串的历史。

13.2.1 用户代理的历史

HTTP规范(1.0和1.1)要求:浏览器应该向服务器发送包含浏览器名称和版本信息的简短字符串

1.早期浏览器

1993年Mosaic是早期Web浏览器的代表,其用户代理字符串相当简单,类似于:

  1. Mosaic/0.9

网景公司准备开发浏览器时,代号确定为“Mozilla”(Mosaic Killer的简写)。第一个公开发行版Netscape Navigator 2的用户代理字符串是这样的:

  1. Mozilla/Version[Language](Platform;Encryption)

遵守了将产品名称和版本作为用户代理字符串的规定,但又在后面添加了如下信息:
❑ Language:语言代码,表示浏览器的目标使用语言。
❑ Platform:表示浏览器所在的操作系统和/或平台。
❑ Encryption:包含的安全加密类型,可能的值是U(128位加密)、I(40位加密)和N(无加密)。
Netscape Navigator 2的典型用户代理字符串如下所示:

  1. Mozilla/2.02[fr](WinNT; I)

这个字符串表示Netscape Navigator 2.02,在主要使用法语地区的发行,运行在Windows NT上,40位加密。总体上看,通过产品名称还是很容易知道这是什么浏览器的。

2.Netscape Navigator 3和IE3

1996年,Netscape Navigator 3发布的浏览器,其用户代理字符串:删除了语言信息,并将操作系统或系统CPU信息(OS-or-CPU description)等列为可选信息。格式:

  1. Mozilla/Version[Language](Platform; Encryption[;OS-or-CPUdescription])

运行在Windows系统上的Netscape Navigator 3的典型用户代理字符串如下:

  1. Mozilla/3.0(Win95; U)

表示Netscape Navigator 3运行在Windows 95上,采用了128位加密。注意在Windows系统上,没有“OS-or-CPU”部分。

3.Netscape Communicator 4和IE4~8

1997年8月,Netscape Communicator 4发布(这次发布将Navigator改成了Communicator)。Netscape在这个版本中仍然沿用了上一个版本的格式

4.Gecko

Gecko渲染引擎是Firefox的核心。Gecko最初作为通用Mozilla浏览器(即后来的Netscape 6)的一部分开发。
有一个针对Netscape 6的用户代理字符串规范,规定了未来的版本应该如何构造这个字符串。

5.WebKit

Safari的渲染引擎叫WebKit,是基于Linux平台浏览器Konqueror使用的渲染引擎KHTML开发的。
这个新浏览器和渲染引擎面临的问题:怎样才能保证浏览器不被排除在流行的站点之外。
答案就是:在用户代理字符串中添加足够多的信息,让网站知道这个浏览器与其他浏览器是兼容的。
现在,所有基于WebKit的浏览器都将自己标识为Mozilla 5.0。

6.Konqueror

Konqueror是与KDE Linux桌面环境打包发布的浏览器,基于开源渲染引擎KHTML。虽然只有Linux平台的版本,Konqueror的用户却不少。为实现最大化兼容,Konqueror决定采用Internet Explore的用户代理字符串格式:

7.Chrome

谷歌的Chrome浏览器使用Blink作为渲染引擎,使用V8作为JavaScript引擎。
Chrome的用户代理字符串包含所有WebKit的信息,另外又加上了Chrome及其版本的信息。

8.Opera

在用户代理字符串方面引发争议最大的一个浏览器就是Opera。
Opera默认的用户代理字符串,是所有现代浏览器中最符合逻辑的,因为它正确标识了自己和版本。
网上已经有了很多浏览器检测代码只考虑Mozilla这个产品名称。为了不让这些检测代码判断错误,Opera坚持使用唯一标识自身的字符串。从Opera 9开始,Opera也采用了两个策略改变自己的字符串:
一是把自己标识为别的浏览器,
如Firefox或IE。这时候的字符串跟Firefox和IE的一样,只不过末尾会多一个”Opera”及其版本号。
另一个策略是伪装成Firefox或IE。
这种情况下的用户代理字符串与Firefox和IE返回的一样,末尾也没有”Opera”及其版本信息。这样就根本没办法区分Opera与其他浏览器了。
更严重的是,Opera还会根据访问的网站不同设置不同的用户代理字符串,却不通知用户。
比如,导航到My Yahoo网站会导致Opera将自己伪装成Firefox。这就导致很难通过用户代理字符串来识别Opera。

9.iOS与Androidi

OS和Android移动操作系统上默认的浏览器都是基于WebKit的,因此具有与相应桌面浏览器一样的用户代理字符串。

13.2.2 浏览器分析

想要知道自己代码运行在什么浏览器上,大部分开发者会分析window.navigator.userAgent返回的字符串值。
最终会得到关于浏览器和操作系统的比较精确的结果。
相比于能力检测,用户代理检测有一定优势。
能力检测可以保证脚本不必理会浏览器而正常执行。
现代浏览器用户代理字符串的过去、现在和未来格式都是有章可循的,我们能够利用它们准确识别浏览器。
1.伪造用户代理
通过检测用户代理来识别浏览器并不是完美的方式,毕竟这个字符串是可以造假的。
只不过实现window.navigator对象的浏览器(即所有现代浏览器)都会提供userAgent这个只读属性。
因此,简单地给这个属性设置其他值不会有效

  1. console.log(window.navigator.userAgent);
  2. window.navigator.userAgent = 'footbar';
  3. console.log(window.navigator.userAgent);
  4. Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36
  5. Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36

不过,通过简单的办法可以绕过这个限制。
比如,有些浏览器提供伪私有的defineGetter方法,利用它可以篡改用户代理字符串:

  1. console.log(window.navigator.userAgent);
  2. window.navigator.__defineGetter__('userAgent', () => 'footbar');
  3. console.log(window.navigator.userAgent);
  4. Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36
  5. footbar

对付这种造假是一件吃力不讨好的事.
与其检测造假,不如更好地专注于浏览器识别。
如果相信浏览器返回的用户代理字符串,那就可以用它来判断浏览器。
如果怀疑脚本或浏览器可能篡改这个值,那最好还是使用能力检测。
2.分析浏览器
通过解析浏览器返回的用户代理字符串,可以极其准确地推断出下列相关的环境信息:
❑ 浏览器 ❑ 浏览器版本 ❑ 浏览器渲染引擎 ❑ 设备类型(桌面/移动)
❑ 设备生产商 ❑ 设备型号 ❑ 操作系统 ❑ 操作系统版本
推荐一些GitHub上维护比较频繁的第三方用户代理解析程序:
❑ Bowser ❑ UAParser.js ❑ Platform.js
❑ CURRENT-DEVICE ❑ Google Closure ❑ Mootools