本系列内容由ZouStrong整理收录
整理自《JavaScript权威指南(第六版)》,《JavaScript高级程序设计(第三版)》
浏览器之间的差异以及不同浏览器之间的“怪癖“,多的简直不胜枚举,面对浏览器间普遍存在的不一致问题,要么采取迁就各方的“最小公分母”策略,要么(也是最常见的)就是利用客户端检测,来突破或规避种种局限性
检测客户端的方法有很多种,但是需要知道的是,不到万不得已,就不要使用客户端检测,因为只要能找到更通用的方法,就应该优先采用更通用的的方法(先设计最通用的方案,然后再使用特定于浏览器的技术增强该方案)
最常用的客户端检测形式是能力检测(又称特性检测)
能力检测的目标不是识别特定的浏览器,而是识别浏览器的能力
能力检测的基本模式如下
if (object.aaa){
//使用object.aaa
}
就像这样(没有实际价值,重在领会精神)
function getEleById(id){
if (document.getElementById){
return document.getElementById(id);
} else if (document.all){
return document.all[id]; //for IE5
} else {
throw new Error("No way to retrieve element!");
}
}
总结起来,能力检测有两个地方需要注意
第一就是先检测达成目的的最常用的特性(可以保证代码最优化,在多数情况下都可以避免测试多个条件)
第二就是必须检测实际要用到的特性:一个特性存在,不一定意味着另一个特性也存在
function getWindowWidth(){
if (document.all){ //假设是IE,但也可能是opera
return document.documentElement.clientWidth; //错误的用法!!!
} else {
return window.innerWidth;
}
}
但问题是document.all存在也不一定表示浏览器就是IE
能力检测对于想知道某个特性是否会按照适当方式行事(而不仅仅是某个特性存在)非常有用
来看下面的函数,它用来确定一个对象是否支持排序
//不要这样做!这不是能力检测——只检测了是否存在相应的方法
function isSortable(object){
return !!object.sort;
}
问题是,任何包含sort属性的对象也会返回true
var result = isSortable({ sort: true });
检测某个属性是否存在并不能确定对象是否支持排序。更好的方式是检测sort是不是一个函数。
//这样更好:检查sort是不是函数
function isSortable(object){
return typeof object.sort === "function";
}
因此,在可能的情况下,要尽量使用typeof进行能力检测
检测某个或某几个特性并不能够确定浏览器
//错误!还不够具体
var isFirefox = !!(navigator.vendor && navigator.vendorSub);
//错误!假设过头了
var isIE = !!(document.all && document.uniqueID);
这两行代码代表了对能力检测的典型误用
以前,确实可以通过检测navigator.vendor 和navigator.vendorSub来确定Firefox浏览器。但是,Safari也依葫芦画瓢地实现了相同的属性。于是,这段代码就会导致人们作出错误的判断
为检测IE,代码测试了document.all 和document. uniqueID。这就相当于假设IE将来的版本中仍然会继续存在这两个属性,同时还假设其他浏览器都不会实现这两个属性
最后,这两个检测都使用了双逻辑非操作符来得到布尔值(比先存储后访问的效果更好)
注:在实际开发中,应该将能力检测作为确定下一步解决方案的依据,而不是用它来判断用户使用的是什么浏览器
与能力检测类似,怪癖检测(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;
}();
另一个经常需要检测的“怪癖”是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。在相关浏览器的新版本中,这些问题可能会也可能不会被修复。由于检测“怪癖”涉及运行代码,因此我们建议仅检测那些对你有直接影响的“怪癖”,而且最好在脚本一开始就执行此类检测,以便尽早解决问题
第三种客户端检测技术叫做用户代理检测,用户代理检测通过检测用户代理字符串来确定实际使用的浏览器
在每一次HTTP请求过程中,用户代理字符串是作为响应首部发送的,而且该字符串可以通过JavaScript的navigator.userAgent属性访问
在服务器端,通过检测用户代理字符串来确定用户使用的浏览器是一种常用而且广为接受的做法
而在客户端,用户代理检测一般被当作一种万不得已才用的做法,其优先级排在能力检测和(或)怪癖检测之后
提到与用户代理字符串有关的争议,就不得不提到电子欺骗(spoofing)。所谓电子欺骗,就是指浏览器通过在自己的用户代理字符串加入一些错误或误导性信息,来达到欺骗服务器的目的
HTTP规范(包括1.0和1.1版)明确规定,浏览器应该发送简短的用户代理字符串,指明浏览器的名称和版本号,然并卵
1993年,世界上第一款Web浏览器Mosaic的用户代理字符串非常简单
Mosaic/0.9
后来,Netscape浏览器将自己产品的代号定名为Mozilla
Mozilla/版本号 [语言] (平台; 加密类型)
1996年,Netscape 3发布,而用户代理字符串只作了一些小的改变
Mozilla/版本号(平台; 加密类型[; 操作系统或CPU说明])
那个时候,通过用户代理字符串中的产品名称,至少还能够轻易地确定用户使用的是什么浏览器
然而,微软的IE3出现了,由于Netscape浏览器在当时占绝对市场份额,许多服务器在提供网页之前都要专门检测该浏览器。如果用户通过IE 打不开相关网页,那么这个新生的浏览器很可能就会夭折,于是,微软决定将IE的用户代理字符串修改成兼容Netscape的形式
Mozilla/2.0 (compatible; MSIE 版本号; 操作系统)
由于当时的大多数浏览器嗅探程序只检测用户代理字符串中的产品名称部分,结果IE 就成功地将自己标识为Mozilla,从而伪装成Netscape Navigator。微软的这一做法招致了很多批评,因为它违反了浏览器标识的惯例。更不规范的是,IE将真正的浏览器版本号插入到了字符串的中间
1997年,Netscapte 4发布
Mozilla/版本号(平台; 加密类型[; 操作系统或CPU说明])
微软的IE4~7,顺便将用户代理字符串修改成了如下格式
Mozilla/4.0 (compatible; MSIE 版本号; 操作系统)
IE9对字符串格式做了一点调整
Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)
如果IE9运行在兼容模式下,字符串中的Mozilla版本号和MSIE版本号会恢复旧的值,但Trident的版本号仍然是5.0
Netscape 6中用户代理字符串的构成 如下
Mozilla/Mozilla版本号(平台; 加密类型; 操作系统或CPU; 语言; 预先发行版本) Gecko/Gecko版本号 应用程序或产品/应用程序或产品版本号
Firefox 4用户代理字符串变成了下面这个样子:
Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox 4.0.1
2003年,Apple公司宣布要发布自己的Web浏览器,名字定为Safari。Safari的呈现引擎叫WebKit,这款新浏览器和呈现引擎的开发人员也遇到了与 IE 3.0类似的问题:如何确保这款浏览器不被流行的站点拒之门外? 答案显而易见,将Safari标识为Mozilla,于是WebKit的用户代理字符串就具备了如下格式
Mozilla/5.0 (平台; 加密类型; 操作系统或CPU; 语言) AppleWebKit/AppleWebKit版本号(KHTML, like Gecko) Safari/Safari版本号
谷歌公司的Chrome浏览器以WebKit作为呈现引擎,但使用了不同的JavaScript引擎。在Chrome 0.2这个最初的beta版中,用户代理字符串完全取自WebKit,只添加了一段表示Chrome版本号的信息
Mozilla/5.0 ( 平台; 加密类型; 操作系统或CPU; 语言) AppleWebKit/AppleWebKit版本号(KHTML, like Gecko) Chrome/ Chrome版本号Safari/ Safari版本
仅就用户代理字符串而言,Opera应该是最有争议的一款浏览器了。Opera默认的用户代理字符串是所有现代浏览器中最合理的——正确地标识了自身及其版本号
Opera/ 版本号(操作系统或CPU; 加密类型; 语言)
Opera 10对代理字符串进行了修改。现在的格式是
Opera/9.80 (操作系统或CPU; 加密类型; 语言) Presto/Presto版本号Version/版本号
iOS和Android默认的浏览器都基于WebKit,而且都像它们的桌面版一样,共享相同的基本用户代理字符串格式
iOS设备的基本格式如下
Mozilla/5.0 (平台; 加密类型; 操作系统或CPU like Mac OS X; 语言) AppleWebKit/AppleWebKit版本号(KHTML, like Gecko) Version/浏览器版本号Mobile/移动版本号Safari/Safari版本号
看完了上面,会发现,通过用户代理字符串来检测特定的浏览器并不是一件轻松的事
首先要确定的就是你需要多么具体的浏览器信息。一般情况下,知道呈现引擎和最低限度的版本就足以决定正确的操作方法了。例如,不推荐使用下列代码
if (isIE6 || isIE7) { //不推荐!!!
//代码
}
如果是IE8怎么办呢?只要IE有新版本出来,就必须更新这些代码。不过,像下面这样使用相对版本号则可以避免此问题
if (ieVer >=6){
//代码
}
我们下面的浏览器检测脚本就将本着这种思路来编写
确切知道浏览器的名字和版本号不如确切知道它使用的是什么呈现引擎
因为,如果都使用相同版本的Gecko或者Webkit,那它们一定支持相同的特性
因此,我们要编写的脚本将主要检测五大呈现引擎
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"){
//针对这个版本执行某些操作
}
}
要正确地识别呈现引擎,关键是检测顺序要正确。由于用户代理字符串存在诸多不一致的地方,如果检测顺序不对,很可能会导致检测结果不正确