JS高级程序设计学习笔记(八)客户端检测

不到万不得已,就不要使用客户端检测。只要能找到更通用的方法,就应该优先采用更通用的方法。一言以蔽之,先设计最通用的方案,然后再使用特定于浏览器的技术增强该方案。

能力检测

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

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

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

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!");
	}
}

这里的 getElement()函数的用途是返回具有给定 ID 的元素。因为 document.getElementById()是实现这一目的的标准方式,所以一开始就测试了这个方法。如果该函数存在(不是未定义),则使用该函数。否则,就要继续检测 document.all 是否存在,如果是,则使用它。如果上述两个特性都不存在(很有可能),则创建并抛出错误,表示这个函数无法使用。

要理解能力检测,首先必须理解两个重要的概念。如前所述,第一个概念就是先检测达成目的的最常用的特性。对前面的例子来说,就是要先检测 document.getElementById(),后检测 document.all。先检测最常用的特性可以保证代码最优化,因为在多数情况下都可以避免测试多个条件。

第二个重要的概念就是必须测试实际要用到的特性。一个特性存在,不一定意味着另一个特性也存在。来看一个例子:

function getWindowWidth(){
	if(document.all){
		return document.documentElement.clientWidth; //错误的用法
	}else
	{
		return window.innerWidth;
	}
}

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

更可靠的能力检测

能力检测对于想知道某个特性是否会按照适当方式行事(而不仅仅是某个特性存在)非常有用。上一节中的例子利用类型转换来确定某个对象成员是否存在,但这样你还是不知道该成员是不是你想要的。来看下面的函数,它用来确定一个对象是否支持排序。

//不要这样做!这不是能力检测——只检测了是否存在相应的方法
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 操作符用于确定 sort 的确是一个函数,因此可以调用它对数据进行排序。

在可能的情况下,要尽量使用 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 的某个原型属性同名,那么该实例属性将不会出现在fon-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循环中作为属性返回。

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

var hasEnumShadowsQuik = 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的实例。

一般来说,“怪癖”都是个别浏览器所独有的,而且通常被归为 bug。在相关浏览器的新版本中,这些问题可能会也可能不会被修复。由于检测“怪癖”涉及运行代码,因此我们建议仅检测那些对你有直接影响的“怪癖”,而且最好在脚本一开始就执行此类检测,以便尽早解决问题。

用户代理检测

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

考虑到历史原因以及现代浏览器中用户代理字符串的使用方式,通过用户代理字符串来检测特定的浏览器并不是一件轻松的事。因此,首先要确定的往往是你需要多么具体的浏览器信息。一般情况下,知道呈现引擎和最低限度的版本就足以决定正确的操作方法了。例如,我们不推荐使用下列代码:

if(isIE6 || isIE7){//不推荐
	//代码
}

这个例子是想要在浏览器为 IE6 或 IE7 时执行相应代码。这种代码其实是很脆弱的,因为它要依据特定的版本来决定做什么。如果是 IE8 怎么办呢?只要 IE 有新版本出来,就必须更新这些代码。不过,像下面这样使用相对版本号则可以避免此问题:

if(ieVer >=6){
	//代码
}

这个例子首先检测 IE 的版本号是否至少等于 6,如果是则执行相应操作。这样就可以确保相应的代码将来照样能够起作用。我们下面的浏览器检测脚本就将本着这种思路来编写。

1.识别呈现引擎

为了不在全局作用域中添加多余的变量,我们将使用模块增强模式来封装检测脚本。检测脚本的基本代码结构如下:

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

这里声明了一个名为 client 的全局变量,用于保存相关信息。匿名函数内部定义了一个局部变量engine,它是一个包含默认设置的对象字面量。在这个对象字面量中,每个呈现引擎都对应着一个属性,属性的值默认为 0。如果检测到了哪个呈现引擎,那么就以浮点数值形式将该引擎的版本号写入相应的属性。而呈现引擎的完整版本(是一个字符串),则被写入 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版本号的最佳方式。要检测更早版本的Opera,可以直接检查用户代理字符串,因为那些版本还不支持隐瞒身份。不过,2007 底 Opera 的最高版本已经是 9.5 了,所以不太可能有人还在使用 7.6 之前的版本。那么,检测呈现引擎代码的第一步,就是编写如下代码:

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

这里,将版本的字符串表示保存在了 engine.ver 中,将浮点数值表示的版本保存在了engine.opera 中。如果浏览器是 Opera,测试 window.opera 就会返回 true;否则,就要看看是其他的什么浏览器了。

应该放在第二位检测的呈现引擎是 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)||/Konqueror\/([^;]+)/.test(ua)){
	engine.ver = RegExp["$1"];
	engine.khtml = parseFloat(engine.ver);
}else if(/rv:([^\)]+)\) Gevko\/\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)||/Konqueror\/([^;]+)/.test(ua)){
	engine.ver = RegExp["$1"];
	engine.khtml = parseFloat(engine.ver);
}else if(/rv:([^\)]+)\) Gevko\/\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还是Safri
	if(/Crome\/(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.kong = parseFloat(engine.ver);
}else if(/rv:([^\)]+)\) Gevko\/\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,
		x11:false
	};
}();

显然,上面的代码中又添加了一个包含 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操作系统

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 
	} 
}

由于非空字符串会转换为布尔值 true,因此可以将 client.system.win 作为布尔值用在 if 语句中。而在需要更多有关操作系统的信息时,则可以使用其中保存的字符串值。

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,
		x11:false
	};
}();

然后,通常简单地检测字符串"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 likeMac OS",后来 iPhone 中又改成"CPU iPhone OS 3_0 like Mac OS X",iPad 中又改成"CPU OS 3_2like 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 系列手机在用户代理字符串中声称使用的是"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 设置为版本号。

6.识别游戏系统

除了移动设备之外,视频游戏系统中的 Web 浏览器也开始日益普及。任天堂 Wii 和 Playstation 3 或者内置 Web 浏览器,或者提供了浏览器下载。Wii 中的浏览器实际上是定制版的 Opera,是专门为 WiiRemote 设计的。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,
		x11:false
	};
}();

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

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还是Safri
		if(/Crome\/(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.kong = parseFloat(engine.ver);
	}else if(/rv:([^\)]+)\) Gevko\/\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 mobie
	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)函数。在这种情况下,仅测试相应的函数是否存在还得不到足够的信息。

同一款浏览器在不同平台下具备不同的能力。这时候,可能就有必要确定浏览器位于哪个平台下。

为了跟踪分析等目的需要知道确切的浏览器。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值