曾几何时,浏览器检测是JavaScript程序员的库存商品。 如果我们知道某些功能在IE5中有效 ,而在Netscape 4中则无效,那么我们将测试该浏览器并相应地存储代码。 像这样:
if(navigator.userAgent.indexOf('MSIE 5') != -1)
{
//we think this browser is IE5
}
但是当我第一次加入这个行业时,军备竞赛已经进行得很顺利! 供应商在用户代理字符串中添加了额外的值,因此它们似乎是竞争对手的浏览器,以及他们自己的浏览器。 例如,这是适用于Mac的Safari 5:
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代码库)的测试相匹配; 但它也匹配Gecko
(这是Firefox的渲染引擎),当然还有Mozilla
(由于历史原因 ,几乎每个浏览器都声称是Mozilla)。
添加所有这些值的目的是为了规避浏览器检测 。 如果脚本假定只有Firefox可以处理特定功能,则它可能会排除Safari,即使它可能会起作用。 并且不要忘记,用户自己可以更改其用户代理-众所周知,我将浏览器设置为Googlebot / 1.0
,因此我可以访问网站所有者认为仅可用于爬网的内容!
因此,随着时间的流逝,这种浏览器检测已成为一种不可能的纠结,并且在很大程度上已经不再使用,而被功能更好的东西所取代。
特征检测只是测试我们要使用的特征。 例如,如果我们需要getBoundingClientRect
(以获取元素相对于视口的位置),那么重要的是浏览器是否支持它 ,而不是哪种浏览器; 因此,我们没有测试支持的浏览器,而是测试了功能本身:
if(typeof document.documentElement.getBoundingClientRect != "undefined")
{
//the browser supports this function
}
不支持该功能的浏览器将返回"undefined"
类型,因此不会通过该条件。 无需我们在任何特定的浏览器中测试脚本,我们都知道该脚本将正常运行,或者无提示地失败。
还是我们……?
但问题是- 特征检测也不是完全可靠的 -有时它会失败。 因此,现在让我们看一些示例,看看如何解决每种情况。
ActiveX对象
功能检测失败的最著名示例可能是测试ActiveXObject
在Internet Explorer中发出Ajax请求 。
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属性映射到DOM属性
属性映射通常用于测试对HTML5属性附带的API的支持。 例如,通过查找draggable
属性,检查具有[draggable="true"]
的元素是否支持拖放API :
if("draggable" in element)
{
//the browser supports drag and drop
}
这里的问题是IE8或更早版本会自动将所有 HTML属性映射到DOM属性。 这就是为什么getAttribute
在这些旧版本中如此混乱的原因,因为它根本不返回任何属性,而返回DOM属性。
这意味着如果我们使用已经具有属性的元素:
<div draggable="true"> ... </div>
然后,即使他们不支持IE8或更早版本,可拖动测试也将返回true。
该属性可以是任何东西:
<div nonsense="true"> ... </div>
但是结果将是相同的-IE8或更早版本将为("nonsense" in element)
返回true。
在这种情况下,解决方案是使用不具有属性的元素进行测试 ,而最安全的方法是使用已创建的元素:
if("draggable" in document.createElement("div"))
{
//the browser really supports drag and drop
}
关于用户行为的假设
您可能已经看到了用于检测触摸设备的如下代码:
if("ontouchstart" in window)
{
//this is a touch device
}
大多数触摸设备会在触发click
事件之前实施人为延迟(通常约为300 ms ),这样一来,即使没有点击元素,也可以双击元素。 但是,这会使应用程序感觉迟钝和无响应,因此开发人员有时会使用该功能测试来分叉事件:
if("ontouchstart" in window)
{
element.addEventListener("touchstart", doSomething);
}
else
{
element.addEventListener("click", doSomething);
}
但是,此条件源自错误的假设 -由于设备支持触摸,因此将使用触摸。 但是,触摸屏笔记本电脑呢? 用户可能正在触摸屏幕,或者他们正在使用鼠标或触控板。 上面的代码无法解决这个问题,因此,用鼠标单击根本无法执行任何操作。
在这种情况下,解决方案是根本不测试事件的支持,而是立即绑定两个事件,然后使用preventDefault
来阻止触摸产生点击:
element.addEventListener("touchstart", function(e)
{
doSomething();
e.preventDefault();
}, false);
element.addEventListener("click", function()
{
doSomething();
}, false);
普通的东西不起作用
要承认这是一件很痛苦的事情,但是有时它不是我们需要测试的功能- 它是浏览器 -因为特定的浏览器声称支持某些无效的功能。 最近的一个示例是Opera 12中的setDragImage()
(这是拖放dataTransfer
对象的方法 )。
功能测试在这里失败了,因为Opera 12声称支持它。 异常处理也无济于事,因为它不会引发任何错误。 只是不起作用:
//Opera 12 passes this condition, but the function does nothing
if("setDragImage" in e.dataTransfer)
{
e.dataTransfer.setDragImage("ghost.png", -10, -10);
}
现在,如果您只想尝试添加自定义拖动图像,可能会很好,如果不支持该设置,则很乐意保留默认设置(这将会发生)。 但是,如果您的应用程序确实需要自定义图像,而在不支持该图像的浏览器中应提供完全不同的实现(即使用自定义JavaScript来实现所有拖动行为),该怎么办?
或者,如果浏览器实现了某些功能,但又呈现出无法避免的渲染错误,该怎么办? 有时,我们别无选择,只能明确检测出所涉及的浏览器 ,并将其排除在使用可能会尝试支持的功能之外。
因此,问题就变成了-实现浏览器检测的最安全方法是什么?
我有两个建议:
- 优先使用专有对象测试而不是
navigator
信息。 - 用它来排除浏览器而不是包括它们。
例如,可以使用window.opera
对象检测Opera 12或更早版本,因此我们可以使用该排除条件测试可拖动的支持:
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")
{
}
但是,通常根据自动类型转换来编写测试条件:
if(foo.bar)
{
}
我们在某些浏览器对象测试(例如,针对window.opera
的测试)中使用了较早的语法,这是安全的,因为对象是如何评估的-任何已定义的对象或函数都将始终评估为true
,而如果未定义,则始终为true
将评估为false
。
但是,我们可能正在测试可以有效返回null
或empty-string的东西,两者均评估为false
。 例如, style.maxWidth
属性有时用于排除IE6 :
if(typeof document.documentElement.style.maxWidth != "undefined")
{
}
如果maxWidth
属性受支持且具有作者定义的值,则其评估结果为true
,因此,如果我们这样编写测试,则它可能会失败:
if(document.documentElement.style.maxWidth)
{
}
一般规则是这样的:依赖自动类型转换对对象和函数是安全的 ,但对字符串和数字或可能为null的值不一定是安全的 。
话虽如此-如果您可以安全地使用它,那么请这样做,因为在现代浏览器中,它通常要快得多(大概是因为它们已针对这种情况进行了优化)。
有关此的更多信息,请参见: 现实世界中的自动类型转换 。
From: https://www.sitepoint.com/javascript-feature-detection-fails/