JS高级程序设计读书笔记 (第九章 客户端检测)

第九章 客户端检测

能力检测

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

if (object.propertyInQuestion){
//使用 object.propertyInQuestion
}

例子:

function getElement(id){
	if (document.getElementById){
		return document.getElementById(id);
	} else if (document.all){
		return document.all[id];
	} else {
		throw new Error("No way to retrieve element!");
	}
}
更可靠的能力检测

上一节中的例子利用类型转换来确定某个对象成员是否存在,但这样你还是不知道该成员是不是你想要的。来看下面的函数,它用来确定一个对象是否支持排序。

//不要这样做!这不是能力检测——只检测了是否存在相应的方法
function isSortable(object){
	return !!object.sort;
}

这个函数通过检测对象是否存在 sort() 方法,来确定对象是否支持排序。问题是,任何包含 sort属性的对象也会返回 true 。var result = isSortable({ sort: true });
检测某个属性是否存在并不能确定对象是否支持排序。更好的方式是检测 sort 是不是一个函数。

//这样更好:检查 sort 是不是函数
function isSortable(object){
	return typeof object.sort == "function";
}

在可能的情况下,要尽量使用 typeof 进行能力检测。特别是,宿主对象没有义务让 typeof 返回合理的值。最令人发指的事儿就发生在 IE 中。大多数浏览器在检测到 document.createElement()存在时,都会返回 true 。

//在 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” 。

关于 typeof 的行为不标准,IE 中还可以举出例子来。ActiveX 对象(只有 IE 支持)与其他对象的行为差异很大。例如,不使用 typeof 测试某个属性会导致错误,如下所示。

//在 IE 中会导致错误
var xhr = new ActiveXObject("Microsoft.XMLHttp");
if (xhr.open){ //这里会发生错误
	//执行操作
}

像这样直接把函数作为属性访问会导致 JavaScript 错误。使用 typeof 操作符会更靠谱一点,但 IE对 typeof xhr.open 会返回 “unknown” 。这就意味着,在浏览器环境下测试任何对象的某个特性是否存在,要使用下面这个函数。

//作者:Peter Michaux
function isHostMethod(object, property) {
	var t = typeof object[property];
	return t=='function' ||
			(!!(t=='object' && object[property])) ||
			t=='unknown';
}
//可以像下面这样使用这个函数:
result = isHostMethod(xhr, "open"); //true
result = isHostMethod(xhr, "foo"); //false

目前使用 isHostMethod() 方法还是比较可靠的,因为它考虑到了浏览器的怪异行为。不过也要注意,宿主对象没有义务保持目前的实现方式不变,也不一定会模仿已有宿主对象的行为。所以,这个函数——以及其他类似函数,都不能百分之百地保证永远可靠。作为开发人员,必须对自己要使用某个功能的风险作出理性的估计。

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

检测某个或某几个特性并不能够确定浏览器。下面给出的这段代码(或与之差不多的代码)可以在许多网站中看到,这种“浏览器检测”代码就是错误地依赖能力检测的典型示例。

//错误!还不够具体
var isFirefox = !!(navigator.vendor && navigator.vendorSub);
//错误!假设过头了
var isIE = !!(document.all && document.uniqueID);

这两行代码代表了对能力检测的典型误用。以前,确实可以通过检测 navigator.vendor 和navigator.vendorSub 来确定 Firefox 浏览器。但是,Safari 也依葫芦画瓢地实现了相同的属性。于是,这段代码就会导致人们作出错误的判断。为检测 IE,代码测试了 document.all 和document.uniqueID 。这就相当于假设 IE 将来的版本中仍然会继续存在这两个属性,同时还假设其他浏览器都不会实现这两个属性。最后,这两个检测都使用了双逻辑非操作符来得到布尔值(比先存储后访问的效果更好)。
实际上,根据浏览器不同将能力组合起来是更可取的方式。如果你知道自己的应用程序需要使用某些特定的浏览器特性,那么最好是一次性检测所有相关特性,而不要分别检测。看下面的例子。

//确定浏览器是否支持 Netscape 风格的插件
var hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);
//确定浏览器是否具有 DOM1 级规定的能力
var hasDOM1 = !!(document.getElementById && document.createElement &&
document.getElementsByTagName);

以上例子展示了两个检测:一个检测浏览器是否支持 Netscapte 风格的插件;另一个检测浏览器是否具备 DOM1 级所规定的能力。得到的布尔值可以在以后继续使用,从而节省重新检测能力的时间。

怪癖检测

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

var hasDontEnumQuirk = function(){
	var o = { toString : function(){} };
	for (var prop in o){
		if (prop == "toString"){
			return false;
		}
	}
	return true;
}();

以上代码通过一个匿名函数来测试该“怪癖”,函数中创建了一个带有 toString() 方法的对象。
在正确的 ECMAScript 实现中, toString 应该在 for-in 循环中作为属性返回。

另一个经常需要检测的“怪癖”是 Safari 3 以前版本会枚举被隐藏的属性。可以用下面的函数来检测该“怪癖”。

var hasEnumShadowsQuirk = function(){
	var o = { toString : function(){} };
	var count = 0;
	for (var prop in o){
		if (prop == "toString"){
			count++;
		}
	}
	return (count > 1);
}();

如果浏览器存在这个 bug,那么使用 for-in 循环枚举带有自定义的 toString() 方法的对象,就会返回两个 toString 的实例。

用户代理检测

第三种,也是争议最大的一种客户端检测技术叫做用户代理检测。用户代理检测通过检测用户代理字符串来确定实际使用的浏览器。在每一次 HTTP 请求过程中,用户代理字符串是作为响应首部发送的,而且该字符串可以通过 JavaScript 的 navigator.userAgent 属性访问。在服务器端,通过检测用户代理字符串来确定用户使用的浏览器是一种常用而且广为接受的做法。而在客户端,用户代理检测一般被当作一种万不得已才用的做法,其优先级排在能力检测和(或)怪癖检测之后。

提到与用户代理字符串有关的争议,就不得不提到电子欺骗(spoofing)。所谓电子欺骗,就是指浏览器通过在自己的用户代理字符串加入一些错误或误导性信息,来达到欺骗服务器的目的。

用户代理字符串的历史

HTTP 规范(包括 1.0 和 1.1版)明确规定,浏览器应该发送简短的用户代理字符串,指明浏览器的名称和版本号。
上述规范进一步规定,用户代理字符串应该以一组产品的形式给出,字符串格式为:标识符/产品版本号。但是,现实中的用户代理字符串则绝没有如此简单。

用户代理字符串检测技术

1、识别呈现引擎
我们要编写的脚本将主要检测五大呈现引擎:IE、Gecko、WebKit、KHTML 和 Opera。
为了不在全局作用域中添加多余的变量,我们将使用模块增强模式来封装检测脚本。检测脚本的基本代码结构如下所示:

var client = function(){
	var engine = {
		//呈现引擎
		ie: 0,
		gecko: 0,
		webkit: 0,
		khtml: 0,
		opera: 0,
		//具体的版本号
		ver: null
	};
	//在此检测呈现引擎、平台和设备
	return {
		engine : engine
	};
}();

如果检测到了哪个呈现引擎,那么就以浮点数值形式将该引擎的版本号写入相应的属性。而呈现引擎的完整版本(是一个字符串),则被写入 ver 属性。

if (client.engine.ie) { //如果是 IE,client.ie 的值应该大于 0
	//针对 IE 的代码
} else if (client.engine.gecko > 1.5){
	if (client.engine.ver == "1.8.1"){
	//针对这个版本执行某些操作
	}
}

在检测到一个呈现引擎之后,其 client.engine 中对应的属性将被设置为一个大于 0 的值,该值可以转换成布尔值 true 。这样,就可以在 if 语句中检测相应的属性,以确定当前使用的呈现引擎,连具体的版本号都不必考虑。鉴于每个属性都包含一个浮点数值,因此有可能丢失某些版本信息。例如,将字符串 “1.8.1” 传入 parseFloat() 后会得到数值 1.8。不过,在必要的时候可以检测 ver 属性,该属性中会保存完整的版本信息。

要正确地识别呈现引擎,关键是检测顺序要正确。由于用户代理字符串存在诸多不一致的地方,如果检测顺序不对,很可能会导致检测结果不正确。为此,第一步就是识别 Opera,因为它的用户代理字符串有可能完全模仿其他浏览器。我们不相信 Opera,是因为(任何情况下)其用户代理字符串(都)不会将自己标识为 Opera。
要识别 Opera,必须得检测 window.opera 对象。Opera 5 及更高版本中都有这个对象,用以保存与浏览器相关的标识信息以及与浏览器直接交互。在 Opera 7.6 及更高版本中,调用 version() 方法可以返回一个表示浏览器版本的字符串,而这也是确定Opera版本号的最佳方式。

if (window.opera){
	engine.ver = window.opera.version();
	engine.opera = parseFloat(engine.ver);
}

应该放在第二位检测的呈现引擎是 WebKit。因为 WebKit 的用户代理字符串中包含 “Gecko” 和"KHTML" 这两个子字符串,所以如果首先检测它们,很可能会得出错误的结论。
不过,WebKit 的用户代理字符串中的 “AppleWebKit” 是独一无二的,因此检测这个字符串最合适。下面就是检测该字符串的示例代码:

var ua = navigator.userAgent;
if (window.opera){
	engine.ver = window.opera.version();
	engine.opera = parseFloat(engine.ver);
} else if (/AppleWebKit\/(\S+)/.test(ua)){
	engine.ver = RegExp["$1"];
	engine.webkit = parseFloat(engine.ver);
}

代码首先将用户代理字符串保存在变量 ua 中。然后通过正则表达式来测试其中是否包含字符串"AppleWebKit" ,并使用捕获组来取得版本号。由于实际的版本号中可能会包含数字、小数点和字母,所以捕获组中使用了表示非空格的特殊字符( \S )。用户代理字符串中的版本号与下一部分的分隔符是一个空格,因此这个模式可以保证捕获所有版本信息。 test() 方法基于用户代理字符串运行正则表达式。如果返回 true ,就将捕获的版本号保存在 engine.ver 中,而将版本号的浮点表示保存在engine.webkit 中。

接下来要测试的呈现引擎是 KHTML。同样,KHTML 的用户代理字符串中也包含 “Gecko” ,因此在排除 KHTML 之前,我们无法准确检测基于 Gecko 的浏览器。KHTML 的版本号与 WebKit 的版本号在用户代理字符串中的格式差不多,因此可以使用类似的正则表达式。
此外,由于 Konqueror 3.1 及更早版本中不包含 KHTML 的版本,故而就要使用 Konqueror 的版本来代替。

var ua = navigator.userAgent;
if (window.opera){
	engine.ver = window.opera.version();
	engine.opera = parseFloat(engine.ver);
} else if (/AppleWebKit\/(\S+)/.test(ua)){
	engine.ver = RegExp["$1"];
	engine.webkit = parseFloat(engine.ver);
} else if (/KHTML\/(\S+)/.test(ua) || /Konqueror\/([^;]+)/.test(ua)){
	engine.ver = RegExp["$1"];
	engine.khtml = parseFloat(engine.ver);
}

与前面一样,由于 KHTML 的版本号与后继的标记之间有一个空格,因此仍然要使用特殊的非空格字符来取得与版本有关的所有字符。然后,将字符串形式的版本信息保存在 engine.ver 中,将浮点数值形式的版本保存在 engin.khtml 中。如果 KHTML 不在用户代理字符串中,那么就要匹配 Konqueror后跟一个斜杠,再后跟不包含分号的所有字符。

在排除了 WebKit 和 KHTML 之后,就可以准确地检测 Gecko 了。但是,在用户代理字符串中,Gecko的版本号不会出现在字符串 “Gecko” 的后面,而是会出现在字符串 “rv:” 的后面。这样,我们就必须使用一个比前面复杂一些的正则表达式,如下所示。

var ua = navigator.userAgent;
if (window.opera){
	engine.ver = window.opera.version();
	engine.opera = parseFloat(engine.ver);
} else if (/AppleWebKit\/(\S+)/.test(ua)){
	engine.ver = RegExp["$1"];
	engine.webkit = parseFloat(engine.ver);
} else if (/KHTML\/(\S+)/.test(ua)) {
	engine.ver = RegExp["$1"];
	engine.khtml = parseFloat(engine.ver);
} else if (/rv:([^\)]+)\) Gecko\/\d{8}/.test(ua)){
	engine.ver = RegExp["$1"];
	engine.gecko = parseFloat(engine.ver);
}

Gecko 的版本号位于字符串 “rv:” 与一个闭括号之间,因此为了提取出这个版本号,正则表达式要查找所有不是’)'的字符,还要查找字符串 “Gecko/” 后跟 8 个数字。如果上述模式匹配,就提取出版本号并将其保存在相应的属性中。

最后一个要检测的呈现引擎就是 IE 了。IE 的版本号位于字符串 “MSIE” 的后面、一个分号的前面,因此相应的正则表达式非常简单,如下所示:

var ua = navigator.userAgent;
if (window.opera){
	engine.ver = window.opera.version();
	engine.opera = parseFloat(engine.ver);
} else if (/AppleWebKit\/(\S+)/.test(ua)){
	engine.ver = RegExp["$1"];
	engine.webkit = parseFloat(engine.ver);
} else if (/KHTML\/(\S+)/.test(ua)) {
	engine.ver = RegExp["$1"];
	engine.khtml = parseFloat(engine.ver);
} else if (/rv:([^\)]+)\) Gecko\/\d{8}/.test(ua)){
	engine.ver = RegExp["$1"];
	engine.gecko = parseFloat(engine.ver);
} else if (/MSIE ([^;]+)/.test(ua)){
	engine.ver = RegExp["$1"];
	engine.ie = parseFloat(engine.ver);
}

以上呈现引擎检测脚本的最后一部分,就是在正则表达式中使用取反的字符类来取得不是分号的所有字符。IE 通常会保证以标准浮点数值形式给出其版本号,但有时候也不一定。因此,取反的字符类 [^;]可以确保取得多个小数点以及任何可能的字符。

2、识别浏览器
大多数情况下,识别了浏览器的呈现引擎就足以为我们采取正确的操作提供依据了。可是,只有呈现引擎还不能说明存在所需的 JavaScript 功能。苹果公司的 Safari 浏览器和谷歌公司的 Chrome 浏览器都使用 WebKit 作为呈现引擎,但它们的 JavaScript 引擎却不一样。在这两款浏览器中, client.webkit都会返回非 0值,但仅知道这一点恐怕还不够。对于它们,有必要像下面这样为 client 对象再添加一些新的属性。

var client = function(){
	var engine = {
		//呈现引擎
		ie: 0,
		gecko: 0,
		webkit: 0,
		khtml: 0,
		opera: 0,
		//具体的版本
		ver: null
	};
	var browser = {
		// 浏览器
		ie: 0,
		firefox: 0,
		safari: 0,
		konq: 0,
		opera: 0,
		chrome: 0,
		// 具体的版本
		ver: null
	};
	//在此检测呈现引擎、平台和设备
	return {
		engine: engine,
		browser: browser
	};
}();

代码中又添加了私有变量 browser ,用于保存每个主要浏览器的属性。与 engine 变量一样,除了当前使用的浏览器,其他属性的值将保持为 0;如果是当前使用的浏览器,则这个属性中保存的是浮点数值形式的版本号。同样, ver 属性中在必要时将会包含字符串形式的浏览器完整版本号。由于大多数浏览器与其呈现引擎密切相关,所以下面示例中检测浏览器的代码与检测呈现引擎的代码是混合在一起的。

//检测呈现引擎及浏览器
var ua = navigator.userAgent;
if (window.opera){
	engine.ver = browser.ver = window.opera.version();
	engine.opera = browser.opera = parseFloat(engine.ver);
} else if (/AppleWebKit\/(\S+)/.test(ua)){
	engine.ver = RegExp["$1"];
	engine.webkit = parseFloat(engine.ver);
	// 确定是 Chrome  还是 Safari
	if (/Chrome\/(\S+)/.test(ua)){
		browser.ver = RegExp["$1"];
		browser.chrome = parseFloat(browser.ver);
	} else if (/Version\/(\S+)/.test(ua)){
		browser.ver = RegExp["$1"];
		browser.safari = parseFloat(browser.ver);
	} else {
		// 近似地确定版本号
		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 (/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)){
	engine.ver = RegExp["$1"];
	engine.gecko = parseFloat(engine.ver);
	// 确定是不是 Firefox
	if (/Firefox\/(\S+)/.test(ua)){
		browser.ver = RegExp["$1"];
		browser.firefox = parseFloat(browser.ver);
	}
} else if (/MSIE ([^;]+)/.test(ua)){
	engine.ver = browser.ver = RegExp["$1"];
	engine.ie = browser.ie = parseFloat(engine.ver);
}

对 Opera 和 IE 而言, browser 对象中的值等于 engine 对象中的值。对 Konqueror 而言, browser.konq 和 browser.ver 属性分别等于 engine.khtml 和 engine.ver 属性。
为了检测 Chrome 和 Safari,我们在检测引擎的代码中添加了 if 语句。提取 Chrome 的版本号时,需要查找字符串 “Chrome/” 并取得该字符串后面的数值。而提取 Safari 的版本号时,则需要查找字符串"Version/" 并取得其后的数值。由于这种方式仅适用于 Safari 3 及更高版本,因此需要一些备用的代码,将 WebKit 的版本号近似地映射为 Safari 的版本号(参见上一小节中的表格)。
在检测 Firefox 的版本时,首先要找到字符串 “Firefox/” ,然后提取出该字符串后面的数值(即版本号)。当然,只有呈现引擎被判别为 Gecko 时才会这样做。

有了上面这些代码之后,我们就可以编写下面的逻辑。

if (client.engine.webkit) { //if it’s WebKit
	if (client.browser.chrome){
	//执行针对 Chrome 的代码
	} else if (client.browser.safari){
	//执行针对 Safari 的代码
	}
} else if (client.engine.gecko){
	if (client.browser.firefox){
	//执行针对 Firefox 的代码
	} else {
	//执行针对其他 Gecko 浏览器的代码
	}
}

3、识别平台
很多时候,只要知道呈现引擎就足以编写出适当的代码了。但在某些条件下,平台可能是必须关注的问题。那些具有各种平台版本的浏览器(如 Safari、Firefox 和 Opera)在不同的平台下可能会有不同的问题。目前的三大主流平台是 Windows、Mac 和 Unix(包括各种 Linux)。为了检测这些平台,还需要像下面这样再添加一个新对象。

var client = function(){
	var engine = {
		//呈现引擎
		ie: 0,
		gecko: 0,
		webkit: 0,
		khtml: 0,
		opera: 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
	};
	//在此检测呈现引擎、平台和设备
	return {
		engine: engine,
		browser: browser,
		system: system
	};
}();

显然,上面的代码中又添加了一个包含 3 个属性的新变量 system 。其中, win 属性表示是否为Windows 平台, mac 表示 Mac,而 x11 表示 Unix。与呈现引擎不同,在不能访问操作系统或版本的情况下,平台信息通常是很有限的。对这三个平台而言,浏览器一般只报告 Windows 版本。为此,新变量 system 的每个属性最初都保存着布尔值 false ,而不是像呈现引擎属性那样保存着数字值。
在确定平台时,检测 navigator.platform 要比检测用户代理字符串更简单,后者在不同浏览器中会给出不同的平台信息。而 navigator.platform 属性可能的值包括 “Win32” 、 “Win64” 、“MacPPC” 、 “MacIntel” 、 “X11” 和 “Linux i686” ,这些值在不同的浏览器中都是一致的。检测平台的代码非常直观,如下所示:

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);

以上代码使用 indexOf() 方法来查找平台字符串的开始位置。虽然 “Win32” 是当前浏览器唯一支持的 Windows 字符串,但随着向 64 位 Windows架构的迁移,将来很可能会出现 “Win64” 平台信息值。
为了对此有所准备,检测平台的代码中查找的只是字符串 “Win” 的开始位置。而检测 Mac 平台的方式也类似,同样是考虑到了 MacPPC 和 MacIntel 。在检测 Unix 时,则同时检查了字符串 “X11” 和 "Linux"在平台字符串中的开始位置,从而确保了代码能够向前兼容其他变体。

4、识别 Windows 操作系统
第一步就是匹配 Windows 95 和 Windows 98 这两个字符串。对这两个字符串,只有 Gecko 与其他浏览器不同,即没有 “dows” ,而且 “Win” 与版本号之间没有空格。要匹配这个模式,可以使用下面这个简单的正则表达式。/Win(?:dows )?([^do]{2})/
这个正则表达式中的捕获组会返回操作系统的版本。由于版本可能是任何两个字符编码(例如 95、98、9x、NT、ME 及 XP),因此要使用两个非空格字符。Gecko 在表示 Windows NT 时会在末尾添加 “4.0” ,与其查找实际的字符串,不如像下面这样查找小数值更合适。/Win(?:dows )?([^do]{2})(\d+\.\d+)?/
这样,正则表达式中就包含了第二个捕获组,用于取得 NT 的版本号。由于该版本号对于 Windows95 和 Windows 98 而言是不存在的,所以必须设置为可选。这个模式与 Opera 表示 Windows NT 的字符串之间唯一的区别,就是 “NT” 与 “4.0” 之间的空格,这在模式中很容易添加。/Win(?:dows )?([^do]{2})\s?(\d+\.\d+)?/

if (system.win){
	if (/Win(?:dows )?([^do]{2})\s?(\d+\.\d+)?/.test(ua)){
		if (RegExp["$1"] == "NT"){
			switch(RegExp["$2"]){
				case "5.0":
					system.win = "2000";
					break;
				case "5.1":
					system.win = "XP";
					break;
				case "6.0":
					system.win = "Vista";
					break;
				case "6.1":
					system.win = "7";
					break;
				default:
					system.win = "NT";
					break;
			}
		} else if (RegExp["$1"] == "9x"){
			system.win = "ME";
		} else {
			system.win = RegExp["$1"];
		}
	}
}

如果 system.win 的值为 true ,那么就使用这个正则表达式从用户代理字符串中提取具体的信息。鉴于 Windows 将来的某个版本也许不能使用这个方法来检测,所以第一步应该先检测用户代理字符串是否与这个模式匹配。在模式匹配的情况下,第一个捕获组中可能会包含 “95” 、 “98” 、 “9x” 或 “NT” 。如果这个值是 “NT” ,可以将 system.win 设置为相应操作系统的字符串;如果是 “9x” ,那么 system.win就要设置成 “ME” ;如果是其他值,则将所捕获的值直接赋给 system.win 。有了这些检测平台的代码后,我们就可以编写如下代码。

if (client.system.win){
	if (client.system.win == "XP") {
		//说明是 XP
	} else if (client.system.win == "Vista"){
		//说明是 Vista
	}
}

5、识别移动设备
要检测相应的设备,第一步是为要检测的所有移动设备添加属性,如
下所示。

var client = function(){
	var engine = {
		//呈现引擎
		ie: 0,
		gecko: 0,
		webkit: 0,
		khtml: 0,
		opera: 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,
		// 移动设备
		iphone: false,
		ipod: false,
		ipad: false,
		ios: false,
		android: false,
		nokiaN: false,
		winMobile: false };
		//在此检测呈现引擎、平台和设备
	return {
		engine: engine,
		browser: browser,
		system: system
	};
}();

然后,通常简单地检测字符串 “iPhone” 、 “iPod” 和 “iPad” ,就可以分别设置相应属性的值了。

system.iphone = ua.indexOf("iPhone") > -1;
system.ipod = ua.indexOf("iPod") > -1;
system.ipod = ua.indexOf("iPad") > -1;

除了知道iOS设备,最好还能知道iOS的版本号。在iOS 3之前,用户代理字符串中只包含 “CPU like Mac OS” ,后来 iPhone 中又改成 “CPU iPhone OS 3_0 like Mac OS X” ,iPad 中又改成 “CPU OS 3_2 like Mac OS X” 。也就是说,检测 iOS 需要正则表达式反映这些变化。

//检测 iOS 版本
if (system.mac && ua.indexOf("Mobile") > -1){
	if (/CPU (?:iPhone )?OS (\d+_\d+)/.test(ua)){
		system.ios = parseFloat(RegExp.$1.replace("_", "."));
	} else {
		system.ios = 2; //不能真正检测出来,所以只能猜测
	}
}

检查系统是不是 Mac OS、字符串中是否存在 “Mobile” ,可以保证无论是什么版本, system.ios中都不会是 0。然后,再使用正则表达式确定是否存在 iOS 的版本号。如果有,将 system.ios 设置为表示版本号的浮点值;否则,将版本设置为 2。(因为没有办法确定到底是什么版本,所以设置为更早的版本比较稳妥。)

检测 Android 操作系统也很简单,也就是搜索字符串 “Android” 并取得紧随其后的版本号。

//检测 Android 版本
if (/Android (\d+\.\d+)/.test(ua)){
	system.android = parseFloat(RegExp.$1);
}

由于所有版本的 Android 都有版本值,因此这个正则表达式可以精确地检测所有版本,并将system.android 设置为正确的值。
诺基亚 N 系列手机使用的也是 WebKit,其用户代理字符串与其他基于 WebKit 的手机很相似,例如:

Mozilla/5.0 (SymbianOS/9.2; U; Series60/3.1 NokiaN95/11.0.026; Profile MIDP-2.0
Configuration/CLDC-1.1) AppleWebKit/413 (KHTML, like Gecko) Safari/413

虽然诺基亚 N 系列手机在用户代理字符串中声称使用的是 “Safari” ,但实际上并不是 Safari,尽管确实是基于 WebKit 引擎。只要像下面检测一下用户代理字符串中是否存在 “NokiaN” ,就足以确定是不是该系列的手机了。

system.nokiaN = ua.indexOf("NokiaN") > -1;

在了解这些设备信息的基础上,就可以通过下列代码来确定用户使用的是什么设备中的 WebKit 来访问网页:

if (client.engine.webkit){
	if (client.system. iOS){
		//iOS 手机的内容
	} else if (client.system.android){
		//Android 手机的内容
	} else if (client.system.nokiaN){
		//诺基亚手机的内容
	}
}

最后一种主要的移动设备平台是 Windows Mobile(也称为 Windows CE),用于 Pocket PC 和Smartphone 中。由于从技术上说这些平台都属于 Windows 平台,因此 Windows 平台和操作系统都会返回正确的值。对于 Windows Mobile 5.0 及以前版本,这两种设备的用户代理字符串非常相似,如下所示:

Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; PPC; 240x320)
Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Smartphone; 176x220)

第一个来自 Pocket PC中的移动 Internet Explorer 4.01,第二个来自 Smartphone 中的同一个浏览器。当 Windows 操作系统检测脚本检测这两个字符串时, system.win 将被设置为 “CE” ,因此在检测Windows Mobile 时可以使用这个值:

system.winMobile = (system.win == "CE");

不建议测试字符串中的 “PPC” 或 “Smartphone” ,因为在 Windows Mobile 5.0 以后版本的浏览器中,这些记号已经被移除了。不过,一般情况下,只知道某个设备使用的是 Windows Mobile 也就足够了。
Windows Phone 7 的用户代理字符串稍有改进,基本格式如下:

Mozilla/4.0 (compatible; MSIE 7.0; Windows Phone OS 7.0; Trident/3.1; IEMobile/7.0)
Asus;Galaxy6

其中,Windows 操作符的标识符与已往完全不同,因此在这个用户代理中 client.system.win等于 “Ph” 。从中可以取得有关系统的更多信息:

//windows mobile
if (system.win == "CE"){
	system.winMobile = system.win;
} else if (system.win == "Ph"){
	if(/Windows Phone OS (\d+.\d+)/.test(ua)){;
		system.win = "Phone";
		system.winMobile = parseFloat(RegExp["$1"]);
	}
}

如果 system.win 的值是 “CE” ,就说明是老版本的 Windows Mobile,因此 system.winMobile会被设置为相同的值(只能知道这个信息)。如果 system.win 的值是 “Ph” ,那么这个设备就可能是Windows Phone 7 或更新版本。因此就用正则表达式来测试格式并提取版本号,将 system.win 的值重置为 “Phone” ,而将 system.winMobile 设置为版本号。

识别游戏系统
除了移动设备之外,视频游戏系统中的 Web 浏览器也开始日益普及。任天堂 Wii 和 Playstation 3 或者内置 Web 浏览器,或者提供了浏览器下载。Wii 中的浏览器实际上是定制版的 Opera,是专门为 Wii Remote 设计的。Playstation 的浏览器是自己开发的,没有基于前面提到的任何呈现引擎。这两个浏览器中的用户代理字符串如下所示:

Opera/9.10 (Nintendo Wii;U; ; 1621; en)
Mozilla/5.0 (PLAYSTATION 3; 2.00)

第一个字符串来自运行在 Wii 中的 Opera,它忠实地继承了 Opera 最初的用户代理字符串格式(Wii上的 Opera 不具备隐瞒身份的能力)。第二个字符串来自 Playstation3,虽然它为了兼容性而将自己标识为 Mozilla 5.0,但并没有给出太多信息。而且,设备名称居然全部使用了大写字母,让人觉得很奇怪;强烈希望将来的版本能够改变这种情况。
在检测这些设备以前,我们必须先为 client.system 中添加适当的属性,如下所示:

var client = function(){
	var engine = {
		//呈现引擎
		ie: 0,
		gecko: 0,
		webkit: 0,
		khtml: 0,
		opera: 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,
		//移动设备
		iphone: false,
		ipod: false,
		ipad: false,
		ios: false,
		android: false,
		nokiaN: false,
		winMobile: false,
		// 游戏系统
		wii: false,
		ps: false
	};
	//在此检测呈现引擎、平台和设备
	return {
		engine: engine,
		browser: browser,
		system: system
	};
}();

检测前述游戏系统的代码如下:

system.wii = ua.indexOf("Wii") > -1;
system.ps = /playstation/i.test(ua);

对于 Wii,只要检测字符串 “Wii” 就够了,而其他代码将发现这是一个 Opera 浏览器,并将正确的版本号保存在 client.browser.opera 中。对于 Playstation,我们则使用正则表达式来以不区分大小写的方式测试用户代理字符串。

完整的代码
var client = function(){
	//呈现引擎
	var engine = {
		ie: 0,
		gecko: 0,
		webkit: 0,
		khtml: 0,
		opera: 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,
		//移动设备
		iphone: false,
		ipod: false,
		ipad: false,
		ios: false,
		android: false,
		nokiaN: false,
		winMobile: false,
		//游戏系统
		wii: false,
		ps: false
	};
	//检测呈现引擎和浏览器
	var ua = navigator.userAgent;
	if (window.opera){
		engine.ver = browser.ver = window.opera.version();
		engine.opera = browser.opera = parseFloat(engine.ver);
	} else if (/AppleWebKit\/(\S+)/.test(ua)){
		engine.ver = RegExp["$1"];
		engine.webkit = parseFloat(engine.ver);
		//确定是 Chrome 还是 Safari
		if (/Chrome\/(\S+)/.test(ua)){
			browser.ver = RegExp["$1"];
			browser.chrome = parseFloat(browser.ver);
		} else if (/Version\/(\S+)/.test(ua)){
			browser.ver = RegExp["$1"];
			browser.safari = parseFloat(browser.ver);
		} else {
	//近似地确定版本号
		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 (/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)){
		engine.ver = RegExp["$1"];
		engine.gecko = parseFloat(engine.ver);
		//确定是不是 Firefox
		if (/Firefox\/(\S+)/.test(ua)){
			browser.ver = RegExp["$1"];
			browser.firefox = parseFloat(browser.ver);
		}
	} else if (/MSIE ([^;]+)/.test(ua)){
		engine.ver = browser.ver = RegExp["$1"];
		engine.ie = browser.ie = parseFloat(engine.ver);
	}
	//检测浏览器
	browser.ie = engine.ie;
	browser.opera = engine.opera;
	//检测平台
	var p = navigator.platform;
	system.win = p.indexOf("Win") == 0;
	system.mac = p.indexOf("Mac") == 0;
	system.x11 = (p == "X11") || (p.indexOf("Linux") == 0);
	//检测 Windows 操作系统
	if (system.win){
		if (/Win(?:dows )?([^do]{2})\s?(\d+\.\d+)?/.test(ua)){
			if (RegExp["$1"] == "NT"){
				switch(RegExp["$2"]){
					case "5.0":
						system.win = "2000";
						break;
					case "5.1":
						system.win = "XP";
						break;
					case "6.0":
						system.win = "Vista";
						break;
					case "6.1":
						system.win = "7";
						break;
					default:
						system.win = "NT";
						break;
				}
			} else if (RegExp["$1"] == "9x"){
				system.win = "ME";
			} else {
				system.win = RegExp["$1"];
			}
		}
	}
	//移动设备
	system.iphone = ua.indexOf("iPhone") > -1;
	system.ipod = ua.indexOf("iPod") > -1;
	system.ipad = ua.indexOf("iPad") > -1;
	system.nokiaN = ua.indexOf("NokiaN") > -1;
	//windows mobile
	if (system.win == "CE"){
		system.winMobile = system.win;
	} else if (system.win == "Ph"){
		if(/Windows Phone OS (\d+.\d+)/.test(ua)){;
			system.win = "Phone";
			system.winMobile = parseFloat(RegExp["$1"]);
		}
	}
	//检测 iOS 版本
	if (system.mac && ua.indexOf("Mobile") > -1){
		if (/CPU (?:iPhone )?OS (\d+_\d+)/.test(ua)){
			system.ios = parseFloat(RegExp.$1.replace("_", "."));
		} else {
			system.ios = 2; //不能真正检测出来,所以只能猜测
		}
	}
	//检测 Android 版本
	if (/Android (\d+\.\d+)/.test(ua)){
		system.android = parseFloat(RegExp.$1);
	}
	//游戏系统
	system.wii = ua.indexOf("Wii") > -1;
	system.ps = /playstation/i.test(ua);
	//返回这些对象
	return {
		engine: engine,
		browser: browser,
		system: system
	};
}();
使用方法

我们在前面已经强调过了,用户代理检测是客户端检测的最后一个选择。只要可能,都应该优先采用能力检测和怪癖检测。用户代理检测一般适用于下列情形。

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

小结

客户端检测是 JavaScript 开发中最具争议的一个话题。由于浏览器间存在差别,通常需要根据不同浏览器的能力分别编写不同的代码。有不少客户端检测方法,但下列是最经常使用的。
 能力检测:在编写代码之前先检测特定浏览器的能力。例如,脚本在调用某个函数之前,可能要先检测该函数是否存在。这种检测方法将开发人员从考虑具体的浏览器类型和版本中解放出来,让他们把注意力集中到相应的能力是否存在上。能力检测无法精确地检测特定的浏览器和版本。
 怪癖检测:怪癖实际上是浏览器实现中存在的 bug,例如早期的 WebKit 中就存在一个怪癖,即它会在 for-in 循环中返回被隐藏的属性。怪癖检测通常涉及到运行一小段代码,然后确定浏览器是否存在某个怪癖。由于怪癖检测与能力检测相比效率更低,因此应该只在某个怪癖会干扰脚本运行的情况下使用。怪癖检测无法精确地检测特定的浏览器和版本。
 用户代理检测:通过检测用户代理字符串来识别浏览器。用户代理字符串中包含大量与浏览器有关的信息,包括浏览器、平台、操作系统及浏览器版本。用户代理字符串有过一段相当长的发展历史,在此期间,浏览器提供商试图通过在用户代理字符串中添加一些欺骗性信息,欺骗网站相信自己的浏览器是另外一种浏览器。用户代理检测需要特殊的技巧,特别是要注意 Opera会隐瞒其用户代理字符串的情况。即便如此,通过用户代理字符串仍然能够检测出浏览器所用的呈现引擎以及所在的平台,包括移动设备和游戏系统。

在决定使用哪种客户端检测方法时,一般应优先考虑使用能力检测。怪癖检测是确定应该如何处理代码的第二选择。而用户代理检测则是客户端检测的最后一种案,因为这种方法对用户代理字符串具有很强的依赖性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值