本篇内容
  • 能力检测
  • 怪癖检测
  • 用户代理检测

检测Web客户端的手段很多,而且各有利弊。但最重要的还是要知道,不到万不得已,就不要使用客户端检测。只要能找到更通用的方法,就应该优先采用更通用的方法。(先设计最通用的方案,然后再使用特定于浏览器的技术增强该方案)

能力检测

最常用也最为人们广泛接受的客户端检测形式是能力检测(又称特性检测)。能力检测的目标不是识别特定的浏览器,而是失败浏览器的能力。只要确定浏览器支持特定的能力,就能给出解决方案。能力检测的基本模式如下:

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

例:IE5.0之前的版本不支持document.getElementById()这个DOM方法。尽管可以用非标准的document.all属性实现相同的目的,但IE的早期版本中确实不存在document.getElementById()。于是就有了类似下面的能力检测代码:

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

要理解能力检测,首先必须立即两个重要的概念:

  • 先检测达成目的的最常用的特性。对上面的例子来说,就是先检查document.getElementById(),后检测document.all。先检测最常用的特性可以保证代码最优化。
  • 必须保证测试实际要用到的特性。一个特性存在不一定意味着另一个特性也存在,如
    1
    2
    3
    4
    5
    6
    7
    function getWindowWidth () {
    if (document.all) { // 假设是IE
    return document.documentElement.clientWidth; // 错误
    } else {
    return window.innerWidth;
    }
    }

这是一个错误使用能力检测的例子。getWindowWidth()函数首先检查document.all是否存在,如果是则返回document.documentElement.clientWidth。IE8及之前的版本确实不支持window.innerWidth属性。但问题是document.all存在也不一定表示浏览器是IE,Opera也支持document.all和window.innerWidth。

更可靠的能力检测

能力检测对于想知道某个特性是否会按照适当方法行事(而不仅仅是某个特性存在)非常有用。如下确定一个对象是否支持排序:

1
2
3
4
5
6
7
8
9
// wrong
function isSortable(object) {
return !!object.sort;
}

// good
function isSortable(object) {
return typeof object.sort === 'function';
}

检测某个属性是否存在并不能确定对象是否支持排序。更好的方式是坚持sort是不是一个函数,所以第二只检测更好。

在可能的情况下,要尽量使用typeof进行能力检测。
特别是,宿主对象没有义务让typeof返回合理的值,如IE这个奇葩。如下检测:

1
2
3
4
// 在IE8及之前版本中不行
function hasCreateElement () {
return typeof document.createElement === 'function';
}

在IE8及之前的版本中,这个函数返回false。因为typeof document.createElement返回的是”object”而不是”function”。因为DOM是宿主对象,IE及更早版本中的宿主对象是通过COM而非JScript实现的。因此,document.createElement()函数确实是一个COM对象,所以typeof才会返回”object”。IE9纠正了这个问题,对所有DOM方法都返回”function”。
在浏览器环境下测试任何对象的某个特性是否存在,isHostMethod()方法还是比较可靠的,因为它考虑到了浏览器的怪异行为。不过要注意,宿主对象没有义务保持目前的实现方法不变,也不一定会模仿已有宿主对象的行为。

1
2
3
4
5
6
7
8
9
function isHostMethod (object, property) {
var t = typeof object[property];
return t === 'function' ||
(!!(t === 'object' && object[property])) ||
t === 'unknown';
}

// 调用如下
var result = isHostMethod(xhr, 'open');

能力检测,不是浏览器检测

检测某个或某几个特性并不能够确定浏览器。下面的检测代码就是错误地依赖能力检测的典型示例。

1
2
3
4
5
// wrong 不够具体
var isFirefox = !!(navigator.vender && navigator.vendorSub);

// wrong 假设过头了
var isIE = !!(document.all && document.uniqueID);

确实可以通过检查navigator.vendor 和 navigator.vendorSub 来确定Firefox浏览器。但是Safari也一样地实现了相同的属性,于是会导致人们作出错误的判断;为坚持IE,代码测试了document.all 和 document.uniqueID。这就相当于假设IE将来的版本中仍然会继续存在这两个属性而且其他浏览器不会实现这两个属性。
实际上,根据浏览器不同将能力组合起来是更可取的方式。如果你知道自己的应用程序需要使用某些特定的浏览器特性,那么最好是一次性检测所以相关特性,而不要分别检测。如

1
2
3
4
5
// 确定浏览器是否支持Netscape风格的插件
var hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);

// 确定浏览器是否具有DOM1级规定的能力
var hasDOM1 = !!(document.getElementById && document.createElement && document.getElementsByTagName);

在实际开发中,应该将能力检测作为确定下一步解决方案的依据,而不是用它来判断用户使用的是什么浏览器。

怪癖检测

与能力检测类似,怪癖检测(quirks detection)的目标是识别浏览器的特殊行为。但与能力检测确认浏览器支持什么能力不同,怪癖检测是想要知道浏览器存在什么缺陷。这通常需要运行一小段代码,以确定某一特性不能正常工作。
例如,IE8及更早版本中存在一个bug,即如果某个实例属性与标记为[[DontEnum]]的某个原型属性同名,那么该实例属性将不会出现在for-in循环当中。可以用如下代码来检测这种“怪癖”:

1
2
3
4
5
6
7
8
9
10
11
var hasDontEnumQuirk = function () {
var o = {
toString: function () {}
}

for (var prop in o) {
if (prop === 'toString') return false;
}

return true;
};

以上代码通过一个匿名函数来测试该怪癖,函数中创建了一个带有toString()方法的对象。在正确的ECMAScript实现中,toString应该在for-in循环中作为属性返回。
一般来说,“怪癖”都是个别浏览器所独有的,而且通常被归为bug。在相关浏览器的新版本中,这些问题可能会也可能不会被修复。由于检测“怪癖”涉及运行代码,因此我们建议仅检查那些对你有直接影响的“怪癖”,而且最好在脚本一开始就执行此类检测

用户代理检测

也是争议最大的一种客户端检测技术,用户代理检察通过检测用户代理字符串来确定实际使用的浏览器。在每一次HTTP请求过程中,用户代理字符串是作为响应首部发送的,而且该字符串可以通过js的navigator.userAgent属性访问。在服务器端,通过检测用户代理字符串来确定用户使用的浏览器是一种常用而且广为接受的做法。而在客户端,用户代理检测一般被当作一种万不得已才用的做法,其优先级排在能力检测和怪癖检测之后。
提到与用户代理字符串有关的争议,就不得不提到电子欺骗(apoofing)。所谓电子欺骗,就是指浏览器通过自己的用户代理字符串加入一些错误或误导性信息,来达到欺骗服务器的目的。

用户代理字符串的历史

HTTP规范明确规定,浏览器应该发送简短的用户代理字符串,指明浏览器的名称和版本号。RFC2616(即HTTP1.1协议规范)是这样描述用户代理字符串的:

“产品标示符常用于通信应用程序标识自身,由软件名和版本组成。使用产品标识符的大多数领域也允许列出作为应用程序主要部分的子产品,由空格分隔。按照惯例,产品要按照相应的重要程度依次列出,以便标识应用程序。”

上述规范进一步规定,用户代理字符串应该以一组产品的形式给出,字符串格式为:标识符/产品版本号。

1.早期的浏览器
  • 1993年,世界第一款web浏览器Mosaic。其用户代理字符串:Mosaic/0.9。
  • Netscape Navigator 2的用户代理字符串格式:
    Mozilla/版本号 [语言] (平台;加密类型)
    语言:语言代码,表示应用程序针对哪种语言设计;平台:操作系统和平台,表示应用程序的运行环境;加密类型:安全加密的雷系,U(128位加密)、I(40位加密)和N(未加密)。
  • 1996年,Netscape Navigator 3发布,用户代理字符串删除了语言标记,同时允许添加操作系统或系统使用的CPU等可选信息。格式:
    Mozilla/版本号 (平台;加密类型 [;操作系统或CPU说明])
    同期IE3格式:Mozilla/2.0 (compatible; MSIE 版本号; 操作系统)
  • 1997年,Netscape Communicator 4 发布,格式:
    Mozilla/版本号 (平台;加密类型[;操作系统CPU说明])
    而IE4-8则出现了诡异的用户代理字符串现象。
  • Gecko:Gecko是Firefox的呈现引擎。第一个采用该引擎的浏览器是Netscape 6。格式:
    Mozilla/Mozilla 版本号 (平台;加密类型;操作系统或CPU;语言;预先发行版本) Gecko/Gecko 版本号 应用程序或产品/应用程序或产品版本号
  • Webkit:2003年,Apple公司宣布要发布自己的web浏览器,名字定为Safari。Safari的呈现引擎叫Webkit,是Linux平台中Konqueror浏览器的呈现引擎KHTML的一个分支。几年后,Webkit独立出来称为了一个开源项目,专注于呈现引擎的开发。格式:
    Mozilla/5.0 (平台;加密类型;操作系统或CPU;语言) AppleWebkit/ AppleWebkit版本号 (KHTML, like Gecko) Safari/Safari 版本号
  • Konqueror:只在linux中使用,格式:
    Mozilla/5.0 (compatible;Konqueror/ 版本号;操作系统或CPU)
  • Chrome:以Webkit作为呈现引擎,但使用了不同的js引擎。格式:
    Mozilla/5.0 (平台;加密类型;操作系统或CPU;语言) AppleWebkit/ AppleWebkit版本号 (KHTML, like Gecko) Chrome/Chrome 版本号 Safari/Safari 版本号
  • Opera:格式:
    Opera/ 版本号 (操作系统或CPU;加密类型) [语言]
    Opera 10 对代理字符串进行了修改,格式:
    Opera/ 9.80 (操作系统或CPU;加密类型;语言) Presto/Presto版本号 Version/版本号
  • ios和android:ios和android默认的浏览器都基于Webkit,
    ios格式:
    Mozilla/5.0 (平台;加密类型;操作系统或CPU like Mac OS X;语言) AppleWebkit/AppleWebkit版本号 (KHTML, like Gecko) Version/浏览器版本号 Mobile/移动版本号 Safari/Safari版本号。Mobile几号的版本号没什么用,主要是用来确定Webkit是移动版,而非桌面版,而平台可以是”iPhone”、”iPod”或”iPad”。
    安卓类似

用户代理字符串检测技术

确切知道浏览器的名字和版本号不如确切知道它使用的是说明呈现引擎。如果Firefox、Camino和Netscape都使用相同版本的Gecko,那它们一定支持相同的特性。
以下脚本可以检测呈现引擎、平台、window操作系统、移动设备和游戏系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
var client = function() {

var engine = { //呈现引擎
trident: 0,
gecko: 0,
webkit: 0,
khtml: 0,
presto: 0,
ver: null //具体的版本号
};
var browser = { //浏览器
ie: 0,
firefox: 0,
safari: 0,
konq: 0,
opera: 0,
chrome: 0,
ver: null //具体的版本号
};
var system = { //操作系统
win: false,
mac: false,
x11: false
};

var ua = navigator.userAgent;
if (/AppleWebKit\/(\S+)/.test(ua)) { //匹配Webkit内核浏览器(Chrome、Safari、新Opera)
engine.ver = RegExp["$1"];
engine.webkit = parseFloat(engine.ver);
if (/OPR\/(\S+)/.test(ua)) { //确定是不是引用了Webkit内核的Opera
browser.ver = RegExp["$1"];
browser.opera = parseFloat(browser.ver);
} else if (/Chrome\/(\S+)/.test(ua)) { //确定是不是Chrome
browser.ver = RegExp["$1"];
browser.chrome = parseFloat(browser.ver);
} else if (/Version\/(\S+)/.test(ua)) { //确定是不是高版本(3+)的Safari
browser.ver = RegExp["$1"];
browser.safari = parseFloat(browser.ver);
} else { //近似地确定低版本Safafi版本号
var SafariVersion = 1;
if (engine.webkit < 100) {
SafariVersion = 1;
} else if (engine.webkit < 312) {
SafariVersion = 1.2;
} else if (engine.webkit < 412) {
SafariVersion = 1.3;
} else {
SafariVersion = 2;
}
browser.safari = browser.ver = SafariVersion;
}
} else if (window.opera) { //只匹配拥有Presto内核的老版本Opera 5+(12.15-)
engine.ver = browser.ver = window.opera.version();
engine.presto = browser.opera = parseFloat(engine.ver);
} else if (/Opera[\/\s](\S+)/.test(ua)) { //匹配不支持window.opera的Opera 5-或伪装的Opera
engine.ver = browser.ver = RegExp["$1"];
engine.presto = browser.opera = parseFloat(engine.ver);
} else if (/KHTML\/(\S+)/.test(ua) || /Konqueror\/([^;]+)/.test(ua)) {
engine.ver = browser.ver = RegExp["$1"];
engine.khtml = browser.konq = parseFloat(engine.ver);
} else if (/rv:([^\)]+)\) Gecko\/\d{8}/.test(ua)) { //判断是不是基于Gecko内核
engine.ver = RegExp["$1"];
engine.gecko = parseFloat(engine.ver);
if (/Firefox\/(\S+)/.test(ua)) { //确定是不是Firefox
browser.ver = RegExp["$1"];
browser.firefox = parseFloat(browser.ver);
}
} else if (/Trident\/([\d\.]+)/.test(ua)) { //确定是否是Trident内核的浏览器(IE8+)
engine.ver = RegExp["$1"];
engine.trident = parseFloat(engine.ver);
if (/rv\:([\d\.]+)/.test(ua) || /MSIE ([^;]+)/.test(ua)) { //匹配IE8-11+
browser.ver = RegExp["$1"];
browser.ie = parseFloat(browser.ver);
}
} else if (/MSIE ([^;]+)/.test(ua)) { //匹配IE6、IE7
browser.ver = RegExp["$1"];
browser.ie = parseFloat(browser.ver);
engine.ver = browser.ie - 4.0; //模拟IE6、IE7中的Trident值
engine.trident = parseFloat(engine.ver);
}

var p = navigator.platform; //判断操作系统
system.win = p.indexOf("Win") == 0;
system.mac = p.indexOf("Mac") == 0;
system.x11 = (p.indexOf("X11") == 0) || (p.indexOf("Linux") == 0);
if (system.win) {
if (/Win(?:dows )?([^do]{2})\s?(\d+\.\d+)?/.test(ua)) {
if (RegExp["$1"] == "NT") {
system.win = ({
"5.0": "2000",
"5.1": "XP",
"6.0": "Vista",
"6.1": "7",
"6.2": "8",
"6.3": "8.1",
"10": "10"
})[RegExp["$2"]] || "NT";
} else if (RegExp["$1"] == "9x") {
system.win = "ME";
} else {
system.win = RegExp["$1"];
}
}
}

return {
ua: ua, //用户浏览器Ua原文
engine: engine, //包含着用户浏览器引擎(内核)信息
browser: browser, //包括用户浏览器品牌与版本信息
system: system //用户所用操作系统及版本信息
};

} ();

以下函数用于检测移动系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* mobile
* 获取手机系统版本(ios,android)
* @return {Object} 系统信息
*/
function getMobileOS() {
var _os = {
android: '',
ios: ''
};

try {
var ua = navigator.userAgent,
_android = ua.match(/(Android);?[\s\/]+([\d.]+)?/),
_ios = ua.match(/([iPad,iPod,iPhone]).*OS\s([\d_]+)/);

if (_android) _os.android = +_android[2] || '';
if (_ios) _os.ios = +_ios[2].replace(/_/g, '.') || '';
} catch (e) {}
return _os
}

用户代理一般适用于以下情形

  • 不能直接准确地使用能力检测或怪癖检测。例如,某些浏览器实现了为将来功能预留的存根(sub)函数。在这种情况下,进测试相应的函数是否存在还得不到足够的信息;
  • 同一款浏览器在不同平台下具备不同的能力。这时候,可能就有必要确定浏览器位于哪个平台下。
  • 为了跟踪分析等目的需要知道确切的浏览器。

温习:

  • 能力检测、怪癖检测和用户代理检测及注意事项

(完)