之所以说浏览器类型检测比较尴尬,是因为有了一个打对台的东东,但是又不可能完全被替代,就是粒度更小的浏览器特性检测。
所以现在就成了两者共存的情况,虽然根据特性来判断更为准确,但是浏览器类型往往又是开发者判断的首选,而且较为简单明了。
还是和以前一样,站在巨人的肩膀上,利用各个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字符串检测。
特性检测
XPath,document.evaluate会根据传入的path返回XPathResult对象,XPath的语法详见这里 ,除了ie外的主流浏览器基本都支持。
SelectorsAPI,众多js selector,sizzle ,peppy 之类的终结者,新一代的浏览器,ff3.5,saf4,chrome,op10,嗯,还有ie8,都已经支持。
ElementExtensions,大家对Object.prototype应该很熟悉了,这就是在DOM中是否允许扩展prototype,毫不意外,出局的又是ie。
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 =
'
a ';
var all = div.getElementsByTagName("*"), a = div.getElementsByTagName("a")[0];
if (!all || !all.length || !a) {
return;
}
jQuery.support = {
leadingWhitespace : div.firstChild.nodeType === 3,
tbody : !div.getElementsByTagName("tbody").length,
htmlSerialize : !!div.getElementsByTagName("link").length,
style : /red/.test(a.getAttribute("style")),
hrefNormalized : a.getAttribute("href") === "/a",
opacity : /^0.55$/.test(a.style.opacity),
cssFloat : !!a.style.cssFloat,
checkOn : div.getElementsByTagName("input")[0].value === "on",
optSelected : document.createElement("select").appendChild(document.createElement("option")).selected,
parentNode : div.removeChild(div.appendChild(document.createElement("div"))).parentNode === null,
deleteExpando : true,
checkClone : false,
scriptEval : false,
noCloneEvent : true,
boxModel : null
};
script.type = "text/javascript";
try {
script.appendChild(document.createTextNode("window." + id + "=1;"));
} catch (e) {
}
root.insertBefore(script, root.firstChild);
if (window[id]) {
jQuery.support.scriptEval = true;
delete window[id];
}
try {
delete script.test;
} catch (e) {
jQuery.support.deleteExpando = false;
}
root.removeChild(script);
if (div.attachEvent && div.fireEvent) {
div.attachEvent("onclick", function click() {
jQuery.support.noCloneEvent = false;
div.detachEvent("onclick", click);
});
div.cloneNode(true).fireEvent("onclick");
}
div = document.createElement("div");
div.innerHTML = '
';
var fragment = document.createDocumentFragment();
fragment.appendChild(div.firstChild);
jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked;
jQuery(function() {
var div = document.createElement("div");
div.style.width = div.style.paddingLeft = "1px";
document.body.appendChild(div);
jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2;
document.body.removeChild(div).style.display = 'none';
div = null;
});
var eventSupported = function(eventName) {
var el = document.createElement("div");
eventName = "on" + eventName;
var isSupported = (eventName in el);
if (!isSupported) {
el.setAttribute(eventName, "return;");
isSupported = typeof el[eventName] === "function";
}
el = null;
return isSupported;
};
jQuery.support.submitBubbles = eventSupported("submit");
jQuery.support.changeBubbles = eventSupported("change");
// release memory in IE
root = script = div = all = a = null;
})();
浏览器检测
很传统的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 =
'
';
bs = {
trident : -[1,] ? 0 : ScriptEngineMinorVersion(), // 考虑像mootools那样用window.ActiveXObject来判断,但实质上这和判断userAgent一样的,都是可以由用户更改的,所以取了这个最短判断,至少目前可用
webkit : !has('taintEnabled', nav) ? ver('WebKit/') : 0,
gecko : win.crypto && ots.call(win.crypto) === '[object Crypto]' ? ver('rv:') : 0,
presto : win.opera && ots.call(win.opera) === '[object Opera]' ? ver('Presto/') : 0,
gchrome : has('chrome') ? ver('Chrome/') : 0,
safari : /a/.__proto__ == '//' ? ver('Version/') : 0, // mac,win下ok
ie : document.documentMode,
platform : (nav.platform.toLowerCase().match(/mac|win|linux/) || [''])[0],
lang : (nav.language || nav.browserLanguage).toLowerCase(),
// feature detect
strict : doc.compatMode === 'CSS1Compat',
https : /^https/i.test(win.location.protocol),
// xpath: !!doc.evaluate
// query: !!doc.querySelector,
// style 没有太大意义,应该使用通用的a.style.cssText
querySelector : has('querySelector', doc),
// in opera HTMLElement.prototype will lead an error
domExtensible : win.HTMLDivElement && ('prototype' in win.HTMLDivElement) ? 2 : win.HTMLElement
&& ('prototype' in win.HTMLElement) ? 1 : 0,
msging : has('postMessage'),
storage : win.sessionStorage && ots.call(win.sessionStorage) === '[object Storage]',
db : has('openDatabase'),
appCache : has('applicationCache'),
worker : win.Worker && ots.call(win.Worker) === '[object Worker]',
geo : nav.geolocation && ots.call(nav.geolocation) === '[object Geolocation]',
dragdrop : hasEvent('drag'),
offline : has('onLine', nav),
cssTable : 0,
rgba : 0,
blankTrimmed : div.firstChild.nodeName === 'A',
hrefNormalized : div.getElementsByTagName('a')[0].getAttribute('href') === '/a',
autoTbody : div.getElementsByTagName('tbody').length,
scriptChild : 0,
parentRemoved : !div.removeChild(div.firstChild).parentNode,
optSeleted : div.getElementsByTagName('select')[0].appendChild(document.createElement('option')).selected,
chkOn : div.lastChild.value === 'on',
chkCloned : 1,
eventCloned : 0
// deleteExpando
// boxModel:,
// submitBubbles, changeBubbles
};
if (div.attachEvent && div.fireEvent) {
div.attachEvent("onclick", function click() {
bs.eventCloned = 1;
div.detachEvent("onclick", click);
});
div.cloneNode(true).fireEvent("onclick");
}
frag = doc.createDocumentFragment();
div.innerHTML = '
';
frag.appendChild(div.lastChild);
bs.chkClone = frag.cloneNode(true).lastChild.checked;
frag = doc.createElement('script');
frag.type = "text/javascript";
id = '_' + new Date().getTime();
try {
frag.appendChild(doc.createTextNode('window.' + id + '=1;'));
} catch (e) {
}
root.insertBefore(frag, root.firstChild);
if (win[id]) {
bs.scriptEval = 1;
delete win[id];
}
root.removeChild(frag);
try {
div.style.display = 'table';
bs.cssTable = div.style.display === 'table';
} catch(e) {
}
try {
div.style.color = 'rgba(0,0,0,1)';
bs.rgba = !div.style.color.indexOf('rgba');
} catch(e) {
}
div = flag = null;
浏览器检测
检测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:★★★★