JavaScript笔记:客户端检测

浏览器提供商虽然在实现公共接口方面投入了很多精力,但结果仍然是每一种浏览器都有各自的长处,也都有各自的缺点。

即使是那些跨平台的浏览器,虽然从技术上看版本相同,也照 样存在不一致性问题。

面对普遍存在的不一致性问题,开发人员要么采取迁就各方的“最小公分母”策略,要么(也是更常见的)就得利用各种客户端检测方法,来突破或者规避种种局限性。

迄今为止,客户端检测仍然是 Web 开发领域中一个饱受争议的话题。一谈到这个话题,人们总会不约而同地提到浏览器应该支持一组最常用的公共功能。

在理想状态下,确实应该如此。但是,在现实当中,浏览器之间的差异以及不同浏览器的“怪癖”(quirk),多得简直不胜枚举。因此,客户端检测除了是一种补救措施之外,更是一种行之有效的开发策略。

检测 Web 客户端的手段很多,而且各有利弊。

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

能力检测

最常用也最为人们广泛接受的客户端检测形式是能力检测(又称特性检测)。

能力检测的目标不是识别特定的浏览器,而是识别浏览器的能力。采用这种方式不必顾及特定的浏览器如何如何,只要确定浏览器支持特定的能力,就可以给出解决方案。

能力检测的基本模式如下:

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

举例来说,IE5.0之前的版本不支持 document.getElementById() 这个 DOM 方法。尽管可以使用非标准的 document.all 属性实现相同的目的。

于是,也就有了类似下面的能力检测代码:

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

要理解能力检测,首先必须理解两个重要的概念。

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

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

例如:

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

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

更可靠的能力检测

能力检测对于想知道某个特性是否会按照适当方式行事(而不仅仅是某个特性存在)非常有用。

上面的例子利用类型转换来确定某个对象成员是否存在,但这样你还是不知道该成员是不是你想要的。来看下面的函数,它用来确定一个对象是否支持排序:

//不要这样做!这不是能力检测——只检测了是否存在相应的方法 function isSortable(object){
   return !!object.sort;
}

这个函数通过检测对象是否存在 sort()方法,来确定对象是否支持排序。问题是,任何包含 sort 属性的对象也会返回 true。

检测某个属性是否存在并不能确定对象是否支持排序。更好的方式是检测 sort 是不是一个函数。

//这样更好:检查 sort 是不是函数 
function isSortable(object){
    return typeof object.sort == "function";
}

在可能的情况下,要尽量使用 typeof 进行能力检测。

特别是,宿主对象没有义务让 typeof 返回合理的值。最令人发指的事儿就发生在 IE 中。大多数浏览器在检测到 document.createElement() 存在时,都会返回 true。

//在 IE8 及之前版本中不行 
function hasCreateElement(){
   return typeof document.createElement == "function";
}

在 IE8 及之前版本中,这个函数返回 false,因为 typeof document.createElement 返回的是 “object”,而不是”function”。
IE9 纠正了这个问题,对所有 DOM 方法都返回”function”。

在浏览器环境下测试任何对象的某个特性是否存在,可以使用下面这个函数:

function isHostMethod(object, property) {
    var t = typeof object[property];
    return t=='function' || (!!(t=='object' && object[property])) || t=='unknown';
 }

目前使用 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 的某个原型属性同名,那么该实例属性将不会出现在 for-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 以前版本会枚举被隐藏的属性。可以用下面的函数来检测该“怪癖”:

var hasEnumShadowsQuirk = 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 属性访问。

在服务器端,通过检测用户代理字符串来确定用户使用的浏览器是一种常用而且广为接受的做法。而在客户端,用户代理检测一般被当作一种万不得已才用的做法,其优先级排在能力检测和(或)怪癖检测之后。

提到与用户代理字符串有关的争议,就不得不提到电子欺骗(spoofing)。所谓电子欺骗,就是指浏览器通过在自己的用户代理字符串加入一些错误或误导性信息,来达到欺骗服务器的目的。

要弄清楚这 个问题的来龙去脉,必须从 Web 问世初期用户代理字符串的发展讲起。

用户代理字符串的历史

HTTP 规范(包括 1.0 和 1.1 版)明确规定,浏览器应该发送简短的用户代理字符串,指明浏览器的名称和版本号。RFC 2616(即 HTTP 1.1 协议规范)是这样描述用户代理字符串的:

“产品标识符常用于通信应用程序标识自身,由软件名和版本组成。使用产品标识符的大 多数领域也允许列出作为应用程序主要部分的子产品,由空格分隔。按照惯例,产品要按照相应的重要程度依次列出,以便标识应用程序。”

上述规范进一步规定,用户代理字符串应该以一组产品的形式给出,字符串格式为:标识符/产品版本号。

但是,现实中的用户代理字符串则绝没有如此简单。

例如,Chrome:

谷歌公司的 Chrome 浏览器以 WebKit 作为呈现引擎,但使用了不同的 JavaScript 引擎。在 Chrome 0.2 这个最初的 beta 版中,用户代理字符串完全取自 WebKit,只添加了一段表示 Chrome 版本号的信息,格式如下:


Mozilla/5.0 ( 平台; 加密类型; 操作系统或 CPU; 语言) AppleWebKit/AppleWebKit 版本号 (KHTML, like Gecko) Chrome/ Chrome 版本号 Safari/ Safari 版本

Chrome 7 的完整的用户代理字符串如:

Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML,like Gecko) Chrome/7.0.517.44 Safari/534.7

其中,WebKit 版本与 Safari 版本看起来似乎始终会保持一致,尽管没有十分的把握。

用户代理字符串检测技术

考虑到历史原因以及现代浏览器中用户代理字符串的使用方式,通过用户代理字符串来检测特定的浏览器并不是一件轻松的事。

因此,首先要确定的往往是你需要多么具体的浏览器信息。一般情况下, 知道呈现引擎和最低限度的版本就足以决定正确的操作方法了。

例如,我们不推荐使用下列代码:

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

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

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

识别呈现引擎:

确切知道浏览器的名字和版本号不如确切知道它使用的是什么呈现引擎。

如果 Firefox、 Camino 和 Netscape 都使用相同版本的 Gecko,那它们一定支持相同的特性。类似地,不管是什么浏览器,只要它跟 Safari 3 使用的是同一个版本的 WebKit,那么该浏览器也就跟 Safari 3 具备同样的功能。 因此,我们要编写的脚本将主要检测五大呈现引擎: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
    }; 
}();

要识别 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);
}

应该放在第二位检测的呈现引擎是 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);
}

接下来要测试的呈现引擎是 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);
}

在排除了 WebKit 和 KHTML 之后,就可以准确地检测 Gecko 了。

但是,在用户代理字符串中,Gecko 的版本号不会出现在字符串”Gecko”的后面,而是会出现在字符串”rv:”的后面。这样,我们就必须使用一个比前面复杂一些的正则表达式,如下所示:

...继续上面的代码...前面代码省略...
} else if (/rv:([^\)]+)\) Gecko\/\d{8}/.test(ua)){
    engine.ver = RegExp["$1"];
    engine.gecko = parseFloat(engine.ver);
}

最后一个要检测的呈现引擎就是 IE 了。IE 的版本号位于字符串”MSIE”的后面、一个分号的前面,因此相应的正则表达式非常简单,如下所示:

...继续上面的代码...前面代码省略...
} else if (/MSIE ([^;]+)/.test(ua)){
    engine.ver = RegExp["$1"];
    engine.ie = parseFloat(engine.ver);
}

识别浏览器:

大多数情况下,识别了浏览器的呈现引擎就足以为我们采取正确的操作提供依据了。

可是,只有呈现引擎还不能说明存在所需的 JavaScript 功能。苹果公司的 Safari 浏览器和谷歌公司的 Chrome 浏览器都使用 WebKit 作为呈现引擎,但它们的 JavaScript 引擎却不一样。

在这两款浏览器中,client.webkit 都会返回非 0 值,但仅知道这一点恐怕还不够。对于它们,有必要像下面这样为 client 对象再添加一些新的属性。

var browser = {
    //浏览器
    ie: 0, 
    firefox: 0, 
    safari: 0, konq: 0,
    opera: 0, chrome: 0,
    //具体的版本
    ver: null 
};

return {
    engine: engine,
    browser: browser
};
//检测呈现引擎及浏览器
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 还是 Safari
    if (/Chrome\/(\S+)/.test(ua)){
        browser.ver = RegExp["$1"];
        var safariVersion = 1; 13
        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.konq = 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);
}

识别平台:

很多时候,只要知道呈现引擎就足以编写出适当的代码了。但在某些条件下,平台可能是必须关注的问题。那些具有各种平台版本的浏览器(如 Safari、Firefox 和 Opera)在不同的平台下可能会有不同的问题。目前的三大主流平台是 Windows、Mac 和 Unix(包括各种 Linux)。

为了检测这些平台,还需要像下面这样再添加一个新对象。

var system = {
    win: false,
    mac: false,
    x11: false 
};

return {
    engine: engine,
    browser: browser,
    system: system
};

在确定平台时,检测 navigator.platform 要比检测用户代理字符串更简单,后者在不同浏览器中会给出不同的平台信息。

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);

识别 Windows 操作系统

识别移动设备

识别游戏系统

使用方法

我们在前面已经强调过了,用户代理检测是客户端检测的最后一个选择。只要可能,都应该优先采用能力检测和怪癖检测。用户代理检测一般适用于下列情形。

不能直接准确地使用能力检测或怪癖检测。例如,某些浏览器实现了为将来功能预留的存根 (stub)函数。在这种情况下,仅测试相应的函数是否存在还得不到足够的信息。

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值