检测 Web 客户端的手段很多,而且各有利弊,但最重要的是,不到万不得已,就不要使用客户端检测。
只要能够找到更通用的方法,就应该优先采用更通用的方法。
一言而蔽之,先设计最通用的方案,然后再使用特定于浏览器的技术增强该方案。
一、能力检测
最常用也最为人们广泛接受的客户端检测形式是能力检测(又称特性检测)。能力检测的目标不是识别特定的浏览器,而是识别浏览器的能力。采用这种方式不必顾忌特定的浏览器如何,只要确定浏览器支持特定的能力,就可以给出解决方案。
能力检测的基本模式如下:
if( object.propertyInQuestion){ //使用 object.propertyInQuestion }
两个重要的概念:
第一个概念是先检测达成目的的最常用的特性。先检测最常用的特性,可以保证代码最优化,因为在多数情况下都可以避免测试多个条件。
第二个概念是必须测试实际要用到的特性。一个特性存在,不一定意味着另一个特性也存在。
检测某个或某几个特性并不能够确定浏览器。实际上,根据浏览器不同将能力组合起来是更可取的方式。
如果知道自己的应用程序需要使用某些特定的浏览器特性,那么最好是一次性检测所有相关特性,而不要分别检测。
//确定浏览器是否支持 Netscape 风格的插件 var hasNSPlugins = !!(navigator.plugins && navigator.plugins.length ); //确定浏览器是否具有 DOM1 级规定的能力 var hasDOM1 = !!(document.getElementById && document.createElement && document.getElementByTagName);
以上两个例子,一个检测是否支持 Netscape 风格的产检;另一个检测浏览器是否具备DOM1 级所规定的能力。所得到的布尔值可以在以后继续使用,从而节省重新检测能力的时间。
在实际开发中,应该将能力检测作为确定下一步解决方案的依据,而不是用它来判断用户使用的是什么浏览器。
二、怪癖检测
与能力检测类似,怪癖检测(quirks detection)的目标是识别浏览器的特殊行为。
但与能力检测不同,怪癖检测是想要知道浏览器存在什么缺陷。这个通常要运行一小段代码,以确定某一特性不能正常工作。
例如,IE中存在的一个 bug ,即如果某个实例属性与标记为 [[DontEnum]] 的某个原型属性同名,那么该实例属性将不会出现在 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 循环中作为属性返回。
另一个经常要检测的"怪癖"是safari 3.0 以前版本会美剧被隐藏的属性。可以用一下函数来检测:
var hasEnumShadownsQuirk = function(){ var o = { toString : functionn(){} }; 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 属性访问。
在服务器端,通过检测用户代理字符串来确定用户使用的浏览器是一种常用而且广为接受的做法。
而在客户端,用户代理检测一般被当作一种万不得已才使用的做法,其优先级排在能力检测和怪癖检测之后。
一、用户代理字符串检测技术
1)识别呈现引擎
确切的知道浏览器的名字和版本号不如确定他们使用的是什么呈现引擎。如果Firefox、Camino 和 Netsacpe 都使用相同版本的 Gecko ,那么他们一定支持相同的特性。
类似的,不管是什么浏览器,只要它跟 Safari 3 使用的是同一个版本的 WebKit,那么该浏览器也就跟 Safari 3 具备同样的功能。
因此,我们需要编写的脚本将主要检测五大呈现引擎: IE、Gecko、WebKit、KHTML 和 Opera。
为了不在全局作用域中添加多余的变量,我们将使用模块增强模式来封装检测脚本。
检测到一个呈现引擎后,其 client.engine 中对应的属性将被设置成一个大于 0 的值,该值可以转换成布尔值的 true。这样就可以在 if 语句中检测相应的属性,以确定当前使用的呈现引擎,连具体的版本号都不需要考虑。
鉴于每个属性都包含一个浮点数值,因此有可能丢失某些版本信息。例如,将字符串"1.8.1"传入 parseFloat() 后悔得到数值 1.8。不过,在必要的时候,可以检测 ver 属性,该属性中保存着完整的版本信息。
正确的识别呈现引擎关键是检测顺序要正确。
首先应该检测的是 Opera
if ( window.opera ){ engine.ver = window.opera.version(); engine.opera = parseFloat( engine.ver ); }
其次检测的事 WebKit
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); }
接下来测试 KHTML
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); }
然后是 Gecko
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:([^\)]+)\) Gecko\/\d{8}/.test(ua)){ engine.ver = RegExp["$1"]; engine.gecko = parseFloat(engine.ver); }
最后一个就是ie了
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:([^\)]+)\) Gecko\/\d{8}/.test(ua)){ engine.ver = RegExp["$1"]; engine.gecko = parseFloat(engine.ver); } else if (/MSIE ([^;]+)/.test(ua)){ engine.ver = browser.ver = RegExp["$1"]; engine.ie = browser.ie = parseFloat(engine.ver); }
2)识别浏览器
大多数情况下,识别了浏览器的呈现引擎就足以为我们采取正确的操作提供依据了。可是,只有呈现引擎还不恩能够说明存在所需的JavaScript功能。
var client = function(){ //呈现引擎 var engine = { ie : 0, gecko : 0, webkit : 0, khtml : 0, opera : 0, //具体的版本号 ver : null }; var browser = { //浏览器 ie : 0, firefox : 0, konq : 0, opera : 0, chrome : 0, safari : 0, //具体的版本 ver : null }; //再次检测呈现引擎、平台和设备 return { engine : engine, browser : browser }; }();
由于大多数浏览器与其呈现引擎密切相关,所以下面实力中检测浏览器的代码与呈现引擎的代码是混合在一起的
//检测呈现引擎及浏览器 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); //确定是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"]; borwser.safari = parseFloat(browser.ver); } else { //近似的确定版本号 var safariVersion = 1; if (engine.webkit < 100 ){ safariVersion = 1; } else if (engine.webkit < 312){ safariVersoin = 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 = RegExp["$1"]; engine.khtml = 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); }
3、识别平台
很多时候,只要知道呈现引擎就可以编写出适合的代码了,但在有些情况下,平台可能是必须关注的问题。为了检测这些平台,需要再添加一个对象:
var client = function(){ //呈现引擎 var engine = { ie : 0, gecko : 0, webkit : 0, khtml : 0, opera : 0, //具体的版本号 ver : null }; var browser = { //浏览器 ie : 0, firefox : 0, konq : 0, opera : 0, chrome : 0, safari : 0, //具体的版本 ver : null }; var system = { win : false, mac : false, xll : false }; //再次检测呈现引擎、平台和设备 return { engine : engine, browser : browser, system : system }; }(); var p = navigator.platform; system.win = p.indexOf("Win") == 0; systemp.mac = p.indexOf("Mac") == 0; system.xll = (p.indexOf("Xll")) == 1 || (p.indexOf("Linux") == 0);
然后还有识别移动设备的
完整的代码如下:
var client = function(){ //呈现引擎 var engine = { ie : 0, gecko : 0, webkit : 0, khtml : 0, opera : 0, //具体的版本号 ver : null }; var browser = { //浏览器 ie : 0, firefox : 0, konq : 0, opera : 0, chrome : 0, safari : 0, //具体的版本 ver : null }; //平台、设备和操作系统 var system = { win : false, mac : false, xll : false, //移动设备 iphone : false, ipod : false, nokiaN : false, winMobile : false, macMobile : false, //游戏系统 wii : false, ps : false }; //检测呈现引擎及浏览器 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); //确定是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"]; borwser.safari = parseFloat(browser.ver); } else { //近似的确定版本号 var safariVersion = 1; if (engine.webkit < 100 ){ safariVersion = 1; } else if (engine.webkit < 312){ safariVersoin = 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 = RegExp["$1"]; engine.khtml = 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; systemp.mac = p.indexOf("Mac") == 0; system.xll = (p.indexOf("Xll")) == 1 || (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; 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.nokiaN = ua.indexOf("NokiaN") > -1; system.winMobile = (system.win == "CE"); system.macMobile = (system.iphone || system.ipod); //游戏系统 system.wii = ua.indexOf("Wii") > -1; system.ps = /playstation/i.test(ua); //再次检测呈现引擎、平台和设备 return { engine : engine, browser : browser, system : system }; }();