6

曾经,浏览器类型嗅探技术被戏称为javascript程序员的“股票交易”。倘若我们知道一些能够在IE5中生效但却无法在Netscape4运行的技术,我们首先做浏览器类型嗅探,然后根据不同的浏览器的兼容性来写代码。比如:

if(navigator.userAgent.indexOf('MSIE 5') != -1)
{
    //we think this browser is IE5
}

然而,各个浏览器厂家竞争日益激烈,他们在user-agent字段里增添额外的值,来确保该浏览器为该厂家拥有所有权。这是Mac版本Safari5的user-agent:

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.59.10 (KHTML, like Gecko) Version/5.1.9 Safari/534.59.10

这个字符串可以匹配“safari”、“webkit”与“KHTML”(Webkit构建基础——Konqueror codebase),但是它也能够匹配到“Gecko”(firefox的渲染引擎),还有“Mozilla”(由于历史原因,几乎所有浏览器都声明为Mozilla)

增加这些值的目的是为了规避浏览器嗅探技术。假如一个脚本声明只有firefox能够处理一个特别的功能,它也有可能在其他浏览器如safari能够运行。另外不要忘记用户自己也可以改变user-agent——比如我把我的浏览器设置成“ Googlebot/1.0”,所以我可以访问站长认为仅仅搜索引擎爬虫可以访问到的内容!

因此,一段时间之后,这种浏览器嗅探技术成为一种无法实现的困扰,已经大大地失去了作用,另一种更好的方法闪亮登场——特性检测

特性检测简单地检查我们需要用到的特性。举个栗子,如果我们需要getBoundingClientRect(获取元素相对浏览器可见视窗的位置),那么要做的重要事情就是浏览器是否支持这个属性;所以。我们不应该检测浏览器类型,而是做特性本身的检测:

if(typeof document.documentElement.getBoundingClientRect != "undefined")
{
    //the browser supports this function
}

不支持该特性功能的浏览器会返回“undefined”类型,所以不会进入条件。即使不通过我们在指定的浏览器测试这脚本,我们也知道它要么运行成功,要么静默失败。

或许我们还可以做...

但是,事实上——特性检测也不是完全可依赖的——他们有时候也会失败。让我们来看一些例子,来了解我们如何处理每个事例。

ActiveX object

或许,做特性检测失败最广为认知的事例是,在IE中检测ActiveXObject来做ajax请求

ActiveX 属于晚绑定对象,具体意思就是在尝试用ActiveX之前,你是无法确定
浏览器是否支持这个特性。所以,如果用户禁止了ActiveX,下面的代码会抛出错误:

if(typeof window.ActiveXObject != "undefined")
{
    var request = new ActiveXObject("Microsoft.XMLHTTP");
}

为了解决这个问题,我们需要做异常处理——根据不同情况尝试实例化对象,捕捉失败:

if(typeof window.ActiveXObject != "undefined")
{
  try
  {
    var request = new ActiveXObject("Microsoft.XMLHTTP");
  }
  catch(ex)
  {
    request = null;
  }
  if(request !== null)
  {
    //... we have a request object
  }
}

HTML属性( attributes )与DOM属性的映射(properties)

属性映射经常用来测试浏览器对HTML5 API属性的支持情况。比如,通过寻找draggable属性,检查带有[draggable="true"]的元素支持** Drag and Drop API**:

if("draggable" in element)
{
  //the browser supports drag and drop
}

问题是,IE8或者更早的版本,自动建立了所有HTML属性到DOM属性的映射关系。这就是getAttribute在老版本IE是如此糟糕的原因,因为它根本不返回HTML属性,而是返回一个DOM 属性。

这意味着,如果我们用已经存在属性的元素:

<div draggable="true"> ... </div>

这在IE8或更早的版本中,进行draggable检测会返回true,即使这些浏览器并不支持draggable属性。

HTML属性(attribute)可以是任意的:

<div nonsense="true"> ... </div>

在IE8或更早的版本,结果可想而知,返回为true

"nonsense" in element

解决方案,检测没有属性(attribute)的元素,最安全的方法就是创建新的元素:

if("draggable" in document.createElement("div"))
{
  //the browser really supports drag and drop
}

对用户行为的判断

你可能遇到过这样的代码用来检测可触控设备:

if("ontouchstart" in window)
{
  //this is a touch device
}

大多数触控设备在触发click事件之前故意设置了一个延迟(通常接近300ms),以便于元素能够触发double-tapped而不是进行点击。但是这样会使应用感到反应迟缓或者无响应。所以,开发者有时采用特性检测来管理不同事件:

if("ontouchstart" in window)
{
  element.addEventListener("touchstart", doSomething);
}
else
{
  element.addEventListener("click", doSomething);
}

然而,这个条件处理来自于一个错误的判断——因为设备只要支持touch,touch事件就会被采用。但是触控屏幕的笔记本该怎么办?用户可能触控屏幕,或者可能用鼠标或触控板。上边的代码无法处理这种情况,所以用鼠标点击根本没有作用。

解决方法就是根本不用检查浏览器对事件类型的支持——取而代之,绑定这些事件包括touchclick,然后采用preventDefault来阻止touch来产生click事件

element.addEventListener("touchstart", function(e)
{
  doSomething();

  e.preventDefault();

}, false);

element.addEventListener("click", function()
{
  doSomething();

}, false);

检测特性失效带来的灾难

这是可以容忍的痛苦事情,但是有时候这不是我们需要的特性的问题——某些浏览器声明支持一些特性,但实际却不能生效。最近的例子就是Opera12中的setDragImage()(可拖拽对象的方法dataTransfer

这里特性检测失败就是因为Opera12仅是对外声明支持,却无法真正的实现;即使异常处理也不会有作用,因为它不会抛出任何错误。它仅仅是不能运行:

//Opera 12 passes this condition, but the function does nothing
if("setDragImage" in e.dataTransfer)
{
  e.dataTransfer.setDragImage("ghost.png", -10, -10);
}

或者,某个浏览器开发了某个特性,但是却有BUG。

面对上述问题,我们不得不思考:什么才是检测浏览器最安全的方法?

我有两点建议:
1. 采用专有对象(proprietary object)测试 优先于 navigator信息
2. 采用排除法,即不包括该特性的浏览器来做特别处理,而不是囊括具有某特性的所有浏览器来做特别处理

比如: opera12或者更早的版本可以检测到window.opera对象,所以我们可以这样检测除了opera之外的其他浏览器对draggable的支持情况:

if(!window.opera && ("draggable" in document.createElement("div")))
{
  //the browser supports drag and drop but is not Opera 12
}

更好的方式,我们加上浏览器专有对象的检测:

if(window.opera)
{
  //Opera 12 or earlier, but not Opera 15 or later
}
if(document.uniqueID)
{
  //any version of Internet Explorer
}
if(window.InstallTrigger)
{
  //any version of Firefox
}

专有对象可以采用组合的方式:

if(document.uniqueID && window.JSON)
{
  //IE with JSON (which is IE8 or later)
}
if(document.uniqueID && !window.Intl)
{
  //IE without the Internationalization API (which is IE10 or earlier)
}

我们已经提过userAgent字符串是不可依赖的,但是vendor字段实际上还是可判断的,可以用来检测是chrome还是safari:

if(navigator.vendor == 'Google Inc.')
{
  //any version of Chrome
}
if(navigator.vendor == 'Apple Computer, Inc.')
{
  //any version of Safari (including iOS builds)
}

选择测试语法

之前,我喜欢测试检查对象与特性的各种写法。比如,下面的写法是最近比较常用的:

if("foo" in bar)
{
}

过去我们不能采用这种写法是因为IE5时代的浏览器会抛出语法错误;但是现在我们不在考虑这个问题,因为我们不需要在支持这些浏览器。

在本质上,它与下面的写法是等价的:

if(typeof bar.foo != "undefined")
{
}

因为JS可以进行隐式转换,所以我们还可以这样写:

if(foo.bar)
{
}

但这种写法也会有潜在的问题:foo.bar 为空字符串或者布尔值false或者数值0的时候,结果就与我们的预期有差距。举个例子,*style.maxWidth * 属性有时会被除了IE6的浏览器运用,我们应该这样检测:

if(typeof document.documentElement.style.maxWidth != "undefined")
{
}

maxWidth属性只会在浏览器支持并且作者设定了该值转换为true,所以我们这样写检测就会失败:

if(document.documentElement.style.maxWidth)
{
}

总结一下主要规则 : 自动类型转换相对对象与函数是安全可信赖的,但是针对字符串与数值却不值得信赖。

既然已经说到这:如果你能够安全利用它,那就去做吧。因为它在现代浏览器通常比较快。

更多内容,请关注:Automatic Type Conversion In The Real World.

原文地址: http://www.sitepoint.com/javascript-feature-detection-fails/


子非门
79 声望4 粉丝

身在GFW,say不由己.网络江湖一无名小卒而已,哈哈……


引用和评论

0 条评论