笑问客从何处来--浏览器检测vs.特性检测
之所以说浏览器类型检测比较尴尬,是因为有了一个打对台的东东,但是又不可能完全被替代,就是粒度更小的浏览器特性检测。
所以现在就成了两者共存的情况,虽然根据特性来判断更为准确,但是浏览器类型往往又是开发者判断的首选,而且较为简单明了。
还是和以前一样,站在巨人的肩膀上,利用各个js的框架源码,逐一分析比较一下。
注:采用的框架版本:prototype-1.6.1, mootools-1.2.4, jquery-1.4.2, ext-3.2.0, yui-3.1.0, dojo-1.4.2
prototype:
Browser : (function() { var ua = navigator.userAgent; var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'; return { IE : !!window.attachEvent && !isOpera, Opera : isOpera, WebKit : ua.indexOf('AppleWebKit/') > -1, Gecko : ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') === -1, MobileSafari : /Apple.*Mobile.*Safari/.test(ua) } })(), BrowserFeatures : { XPath : !!document.evaluate, SelectorsAPI : !!document.querySelector, ElementExtensions : (function() { var constructor = window.Element || window.HTMLElement; return !!(constructor && constructor.prototype); })(), SpecificElementExtensions : (function() { if (typeof window.HTMLDivElement !== 'undefined') return true; var div = document.createElement('div'); var form = document.createElement('form'); var isSupported = false; if (div['__proto__'] && (div['__proto__'] !== form['__proto__'])) { isSupported = true; } div = form = null; return isSupported; })() }
- 浏览器检测
- var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]',较常见的是var isOpera == !!window.opera,当然这个不够准确,因为我们可以在window下定义一个opera变量,所以采用前一种写法更加准确。
- window.attachEvent这个绑定事件的方法只有在ie及opera中支持,其余的都是采用addEventListener,因此可以用来判别是否是ie,不过稍稍有点奇怪的是,这里为什么就不怕用户自己加个attachEvent变量,如果采用约定俗成的话,那其实!!window.opera也足够了。
- webkit, gecko这些都是采用最常见的useragent字符串检测。
- 特性检测
mootools:
var Browser = $merge({ Engine : { name : 'unknown', version : 0 }, Platform : { name : (window.orientation != undefined) ? 'ipod' : (navigator.platform.match(/mac|win|linux/i) || ['other'])[0].toLowerCase() }, Features : { xpath : !!(document.evaluate), air : !!(window.runtime), query : !!(document.querySelector) }, Plugins : {}, Engines : { presto : function() { return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }, trident : function() { return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); }, webkit : function() { return (navigator.taintEnabled) ? false : ((Browser.Features.xpath) ? ((Browser.Features.query) ? 525 : 420) : 419); }, gecko : function() { return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19 : 18); } } }, Browser || {}); Browser.Platform[Browser.Platform.name] = true; Browser.detect = function() { for (var engine in this.Engines) { var version = this.Engines[engine](); if (version) { this.Engine = { name : engine, version : version }; this.Engine[engine] = this.Engine[engine + version] = true; break; } } return { name : engine, version : version }; }; Browser.detect(); Browser.Request = function() { return $try(function() { return new XMLHttpRequest(); }, function() { return new ActiveXObject('MSXML2.XMLHTTP'); }, function() { return new ActiveXObject('Microsoft.XMLHTTP'); }); }; Browser.Features.xhr = !!(Browser.Request()); Browser.Plugins.Flash = (function() { var version = ($try(function() { return navigator.plugins['Shockwave Flash'].description; }, function() { return new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); }) || '0 r0').match(/\d+/g); return { version : parseInt(version[0] || 0 + '.' + version[1], 10) || 0, build : parseInt(version[2], 10) || 0 }; })();
- 浏览器检测
- 多了平台的判断,这在某些情况下也还是有必要,因为不同平台的同一浏览器可能表现不一样,特别是手持终端平台。
- 这里与常见的浏览器类型版本不同的是判断的浏览器渲染引擎,presto是opera的专用,同样trident是ie家族的,使用webkit的就有很多,著名的如safari,chrome,gecko著名的是ff。但是用特征判断引擎的版本就不是很准确,因为版本在不断更新,如果需要判断引擎版本,还是推荐useragent分析,当然,实际上我们只关心引擎的大版本,如presto2,presto3,具体的版本号其实没有什么意义。
- 特性检测
- 特性里加了是否支持xhr,目前绝大多数浏览器,包括手机平台的,都支持xhr,所以个人觉得可有可无。
- 插件判断加入是否安装flash,两个不同函数分别对应非ie和ie,但是要注意的是ie下有navigator.plugins这个对象,只是是个空对象。
jquery:
jquery = { // ... uaMatch : function(ua) { var ret = { browser : "" }; ua = ua.toLowerCase(); if (/webkit/.test(ua)) { ret = { browser : "webkit", version : /webkit[\/ ]([\w.]+)/ }; } else if (/opera/.test(ua)) { ret = { browser : "opera", version : /version/.test(ua) ? /version[\/ ]([\w.]+)/ : /opera[\/ ]([\w.]+)/ }; } else if (/msie/.test(ua)) { ret = { browser : "msie", version : /msie ([\w.]+)/ }; } else if (/mozilla/.test(ua) && !/compatible/.test(ua)) { ret = { browser : "mozilla", version : /rv:([\w.]+)/ }; } ret.version = (ret.version && ret.version.exec(ua) || [0, "0"])[1]; return ret; }, browser : {} }; browserMatch = jQuery.uaMatch(userAgent); if (browserMatch.browser) { jQuery.browser[browserMatch.browser] = true; jQuery.browser.version = browserMatch.version; } // Deprecated, use jQuery.browser.webkit instead if (jQuery.browser.webkit) { jQuery.browser.safari = true; } // ... (function() { jQuery.support = {}; var root = document.documentElement, script = document.createElement("script"), div = document.createElement("div"), id = "script" + now(); div.style.display = "none"; div.innerHTML = '
- 浏览器检测
- 很传统的useragent字符串检测。
- 原来的safari被webkit代替,估计是因为chrome的强势出现。
- 特性检测
- leadingWhitespace,ie中使用innnerHTML会将头部的空格自动去除,注意:尾部不会。
- tbody,如果table中没有tbody,则自动插入。这本来是ie的特性,但现在ie8的出现让情况复杂了,ie8不会自动插入,所以这时候就体现出特性检测的优势了,因为拥有更细的粒度,也就更为准确。
- htmlSerialize,源码中的注释有些错误,其实不是包装元素的问题,而是ie下将link等同于头部空格处理了,只需要将link挪后面或者在前面加上点什么就可以。
- style,getAttribute("style")返回style的字符串,但ie下是返回一个object,要取字符串的话用style.cssText。ie8又额外跳了出来,大家已经适应了ie的特殊性,这时候ie开始慢慢向标准靠拢,反而有点不习惯。
- hrefNormalized,getAttribute("href"),ie会在前面加入访问的url。同时,没错,你猜对了,ie8又是例外,我们应该渐渐感受到微软的诚意,尽管这一天来得太晚。
- opacity,这个应该比较熟悉,ie下可用滤镜实现,其余的也可以用特定样式,如-webkit-opacity,-moz-opacity。
- cssFloat,有一些css属性在css中与js中的名称不完全一样,这个就是一例,css中的float,ie中用styleFloat对应,其余的用cssFloat对应,具体这一类的情况我们在以后的获取样式中还会讲到。
- checkOn,除webkit引擎之外的浏览器,checkbox的默认值为"on"。
- optSelected,除webkit及ie外的浏览器,option的selected默认值为true,目前测试mac下的chrome例外,估计新版的532.9的webkit修复了这个bug已经。
- parentNode,除ie外的浏览器,removeNode的parentNode为空。
- deleteExpando,除ie外的浏览器,delete一个未定义的属性返回true。
- checkClone,除webkit外的浏览器,调用fragment的cloneNode时,checkbox的状态并未克隆,目前测试mac下的chrome例外,估计新版的532.9的webkit修复了这个bug已经。
- scriptEval,除ie外的浏览器可以像操纵普通dom元素一样对script元素使用appendChild,ie用script.text代替。
- noCloneEvent,ie调用cloneNode会将事件响应函数也复制过去,不是很合理。
- boxModel,盒模型就不解释了。
- submitBubbles,changeBubbles,ff用添加onevent属性,检查是否函数,其余的用简单的in来检测元素是否支持该事件响应,这一妙招来自于Detecting event support without browser sniffing。
ext:
var ua = navigator.userAgent.toLowerCase(), check = function(r){ return r.test(ua); }, DOC = document, isStrict = DOC.compatMode == "CSS1Compat", isOpera = check(/opera/), isChrome = check(/\bchrome\b/), isWebKit = check(/webkit/), isSafari = !isChrome && check(/safari/), isSafari2 = isSafari && check(/applewebkit\/4/), // unique to Safari 2 isSafari3 = isSafari && check(/version\/3/), isSafari4 = isSafari && check(/version\/4/), isIE = !isOpera && check(/msie/), isIE7 = isIE && check(/msie 7/), isIE8 = isIE && check(/msie 8/), isIE6 = isIE && !isIE7 && !isIE8, isGecko = !isWebKit && check(/gecko/), isGecko2 = isGecko && check(/rv:1\.8/), isGecko3 = isGecko && check(/rv:1\.9/), isBorderBox = isIE && !isStrict, isWindows = check(/windows|win32/), isMac = check(/macintosh|mac os x/), isAir = check(/adobeair/), isLinux = check(/linux/), isSecure = /^https/i.test(window.location.protocol);
- 浏览器检测
- 代码只用变量,但比jquery,并无甚不同,yui亦如是。(浪花只开一时,但比千年石,并无甚不同,流云亦如此,庆余年中的句子,个人非常喜欢,借用一下)
- 特性检测
- isStrict,document.compatMode是否为CSS1Compat来判断是否是严格模式,但我在一篇文章中看到说这个判断并不准确,具体记不起来,留待日后找到再补。
- isSecure,采用http还是https访问。
yui:
Y.UA = function() { var numberify = function(s) { var c = 0; return parseFloat(s.replace(/\./g, function() { return (c++ == 1) ? '' : '.'; })); }, win = Y.config.win, nav = win && win.navigator, o = { ie : 0, opera : 0, gecko : 0, webkit : 0, mobile : null, air : 0, caja : nav && nav.cajaVersion, secure : false, os : null }, ua = nav && nav.userAgent, loc = win && win.location, href = loc && loc.href, m; o.secure = href && (href.toLowerCase().indexOf("https") === 0); if (ua) { if ((/windows|win32/i).test(ua)) { o.os = 'windows'; } else if ((/macintosh/i).test(ua)) { o.os = 'macintosh'; } else if ((/rhino/i).test(ua)) { o.os = 'rhino'; } if ((/KHTML/).test(ua)) { o.webkit = 1; } m = ua.match(/AppleWebKit\/([^\s]*)/); if (m && m[1]) { o.webkit = numberify(m[1]); if (/ Mobile\//.test(ua)) { o.mobile = "Apple"; } else { m = ua.match(/NokiaN[^\/]*|Android \d\.\d|webOS\/\d\.\d/); if (m) { o.mobile = m[0]; } } m=ua.match(/Chrome\/([^\s]*)/); if (m && m[1]) { o.chrome = numberify(m[1]); // Chrome } else { m = ua.match(/AdobeAIR\/([^\s]*)/); if (m) { o.air = m[0]; } } } if (!o.webkit) { m = ua.match(/Opera[\s\/]([^\s]*)/); if (m && m[1]) { o.opera = numberify(m[1]); m = ua.match(/Opera Mini[^;]*/); if (m) { o.mobile = m[0]; } } else { // not opera or webkit m = ua.match(/MSIE\s([^;]*)/); if (m && m[1]) { o.ie = numberify(m[1]); } else { // not opera, webkit, or ie m = ua.match(/Gecko\/([^\s]*)/); if (m) { o.gecko = 1; // Gecko detected, look for revision m = ua.match(/rv:([^\s\)]*)/); if (m && m[1]) { o.gecko = numberify(m[1]); } } } } } } return o; }();
- 浏览器检测
- 也是useragent字符串检测,不过变量的值即表示该浏览器的版本号,如果取不到版本号则默认为1,这样避免了如ie6,ie7,ie8之类多个变量,减少变量使用,更为清晰。
- 特性检测
- secure
dojo:
var d = dojo; var n = navigator; var dua = n.userAgent, dav = n.appVersion, tv = parseFloat(dav); if (dua.indexOf("Opera") >= 0) { d.isOpera = tv; } if (dua.indexOf("AdobeAIR") >= 0) { d.isAIR = 1; } d.isKhtml = (dav.indexOf("Konqueror") >= 0) ? tv : 0; d.isWebKit = parseFloat(dua.split("WebKit/")[1]) || undefined; d.isChrome = parseFloat(dua.split("Chrome/")[1]) || undefined; d.isMac = dav.indexOf("Macintosh") >= 0; var index = Math.max(dav.indexOf("WebKit"), dav.indexOf("Safari"), 0); if (index && !dojo.isChrome) { d.isSafari = parseFloat(dav.split("Version/")[1]); if (!d.isSafari || parseFloat(dav.substr(index + 7)) <= 419.3) { d.isSafari = 2; } } if (dua.indexOf("Gecko") >= 0 && !d.isKhtml && !d.isWebKit) { d.isMozilla = d.isMoz = tv; } if (d.isMoz) { d.isFF = parseFloat(dua.split("Firefox/")[1] || dua.split("Minefield/")[1]) || undefined; } if (document.all && !d.isOpera) { d.isIE = parseFloat(dav.split("MSIE ")[1]) || undefined; var mode = document.documentMode; if (mode && mode != 5 && Math.floor(d.isIE) != mode) { d.isIE = mode; } } if (dojo.isIE && window.location.protocol === "file:") { dojo.config.ieForceActiveXXhr = true; } d.isQuirks = document.compatMode == "BackCompat"; d.locale = dojo.config.locale || (d.isIE ? n.userLanguage : n.language).toLowerCase();
- 浏览器检测
- useragent字符串检测。
- ie8的X-UA-Compatible引入带来了一定的麻烦,需要综合document.documentMode来考虑。
- 特性检测
- isQuirks。
- locale,在1.4.2中新加入了语言。
通过以上的代码,我们可以总结出以下几点:
- 浏览器与特性检测并不矛盾,可以同时存在。
- 有些框架着重于引擎版本,有些着重于浏览器版本,基本上每个引擎都对应于一个主要浏览器,webkit除外,有safari和chrome,我们就单独处理一下。
- 为了判断兼容性,一般都有平台信息,但缺少语言信息。
- 有各种特性检测,有对各种特性的支持判断,也有基于某些特殊浏览器bug的兼容判断。
- 对新的HTML5的一些特性检测好像都没有,可以考虑加入。
综合网上的一些代码得出的个人版本
var win = window, doc = win.document, nav = win.navigator, root = doc.documentElement, div = doc.createElement('div'), bs, frag, id, ua = navigator.userAgent, ots = Object.prototype.toString; function has(p, o) { o = o || win; if (p in o) { try { delete o[p]; } catch (e) { } return p in o; } }; function ver(split) { var s = ua.split(split)[1]; return s && (s = s.split('.')) && parseFloat(s.shift() + '.' + s.join('')) || 1; }; function hasEvent(e) { e = "on" + e; var has = (e in div); if (!has) { div.setAttribute(e, "return;"); has = typeof div[e] === "function"; } return has; }; // 为了保证特性检测的独立性,所以不依靠浏览器判断 div.innerHTML = '
- 浏览器检测
- 检测trident, webkit, gecko和presto这四大浏览器引擎及版本
- 检测chrome, safari等常用浏览器及版本
- trident属性和ie属性的区别,主要为了兼容ie8引入的X-UA-Compatible,trident指的是浏览器引擎版本,ie指的是当前浏览器渲染版本,通常以ie为比较标准。
- 检测win, mac, linux平台
- 加入当前浏览器使用语言
- 特性检测
- strict, 是否使用严格模式。
- https, 是否使用https协议。
- querySelector, 是否提供querySelector函数。
- domExtensible, dom元素是否允许扩展,0不允许,1只允许HTMLElement,2可扩展各个HTMLElement子类。
- msging, 是否支持跨文档通信。
- storage, 是否支持本地存储。
- db, 是否支持本地数据库。
- worker, 是否支持worker。
- geo, 是否支持geolocation。
- dragdrop, 是否支持原生拖放。
- offline, 是否支持离线检测。
- cssTable, 是否支持css的table布局。
- rgba, 是否支持rgba。
- blankTrimmed, innerHTML是否会去掉空格。
- hrefNormalized, href值是否保持不变。
- autoTbody, 是否自动插入tbody。
- scriptChild,除ie外的浏览器可以像操纵普通dom元素一样对script元素使用appendChild,ie用script.text代替。
- parentRemoved,除ie外的浏览器,removeNode的parentNode为空。
- optSelected,除webkit及ie外的浏览器,option的selected默认值为true,目前测试mac下的chrome例外,估计新版的532.9的webkit修复了这个bug已经。
- chkOn,除webkit引擎之外的浏览器,checkbox的默认值为"on"。
- chkCloned,除webkit外的浏览器,调用fragment的cloneNode时,checkbox的状态并未克隆,目前测试mac下的chrome例外,估计新版的532.9的webkit修复了这个bug已经。
- eventCloned,ie调用cloneNode会将事件响应函数也复制过去,不是很合理。
通过对高手的代码学习解析,能从中提高一星半点对js的掌握。
个人评价:
ext:★
yui,dojo:★☆
prototype:★★
mootools:★★☆
jquery:★★★★