想起几年 Dean Edwards 大神发布Base2时那句注释:
You know, writing a javascript library is awfully time consuming.
虽然javascript看似容易入门,但想深入是极其困难,因为它与java这些古典式的语言非常不同,最可恨的是那狗屎一般的DOM实现——API千差万别,bugs层出不同。因此光凭javascript是无能为力,DOM API的地位是相当重要。我们不需要hash,堆这样数据结构,加之IE6也承载不了这样复杂的东西,只需要能对DOM进行强大的处理就行了,随之很多东西都建立于其上。也基于这个原因,我的框架就直接叫dom了(终于被我想到一个与closure一样酷的名字了,泪目)。虽然有人不同意对原生对象进行扩展,但既然javascript提供了如此强大的入侵式语法,放弃它太可惜了,因此本框架还是走Prototypejs与mootools的走路。原型的充分利用能让我们真正实现那句所谓的:"Write Less, Do More!"
由于我的东西是定位于"框架",非"类库",因此涉及的东西也非常多。由于战线过长,漫长的时间中发生各种事,导致有些模块被反复修改许多次,内部版本已达到3.0以上,有时模块则还没有完工。不过不管了,先放出来吧,长期的闭门造车也是个问题。
为了开发这东西,我很久没有上来冒泡了,憋环了,唠叨了这么多。好了,先介绍一下我的核心模块。由于加载系统还没有选型好,现在但求轻松选用最简单的AJAX同步阻塞,也就是我原来的第一版实现。其次是浏览器的特性侦测,浏览器检测,对集合的操作等一些常用函数。最后还有domReady,本来想让它独立为一个模块的,不过如果颗粒过细就意味着请求很多,对服务器的压力太大了。当然,我最后放出来还是一个JS文件。我没有服务器,不能像jQuery,mootools那样提供合并模块的服务。不过,为了方便大家合并,它的设计也是非常良好,直接剪切到核心模块文件就行了。每一个模块都是一个自执行函数,不会让那些变量什么的跑出去。
下面是源码,我会逐一讲解它们的用法,如果你们有什么好的实现也请不吝赐救!
/*dom Framework version 1.0 /*dom Framework version 1.0 Copyright 2010 Dual licensed under the MIT or GPL Version 2 licenses. author: <ruby> <rb>司徒正美<rp>(zhongqincheng)</rp></rb><rt>しとぅなさみ</rt></ruby> http://www.cnblogs.com/rubylouvre/ */ (function(){ var window = this, dom = function (selector,context) { dom.require("node"); return (this instanceof dom) ? this.init.apply(this,arguments) : new dom(selector,context) }, //几个简写 fn = "prototype", co = "constructor", has = "hasOwnProperty", tags = "getElementsByTagName", to_s = Object[fn].toString, //使用eval大法防止IE的条件编译在压缩时被删掉 ie = eval("''+/*@cc_on"+" @_jscript_version@*/-0")*1, //把别人的库保存到一个临时变量中 _dom = window.dom, //永久性命名空间,如果这个名字被其他库占用就没救了 namespace = escape(document.URL.split("#")[0]), //判定原生对象与基本类型,第二个参数为字符串,大小写敏感 is = function (obj,type) { return (type === "Object" && obj === Object(obj)) || (type === "Number" && obj === +obj ) || (type === "Null" && obj === null) || (type === "Undefined" && obj === void 0 ) || to_s.call(obj).slice(8,-1) === type; }, //========================================= // 判定是否为纯净的对象, // 指以{},{aa:1,bb:1}或new Object(不带参数,见ecma262v5 15.2.2.1)形式得到的对象实例,用于深拷贝 //========================================= isPureObject = function(obj){ return !!(obj && is(obj,"Object") && obj[co] === Object) }, dontEnum = true; for (var i in { toString: 1 }) dontEnum = null;//只处理这三个关键的不遍历属性 if (dontEnum) dontEnum = ["constructor", "toString", "valueOf"]; //========================================= // 特征侦探 //========================================== dom.env = new function(){ var div = dom.parser = document.createElement("div"), root = document.documentElement,sliceNodes = true; div.innerHTML = ' <link/><a href="/nasami" name="'+namespace+'" style="float:left;opacity:.25;"></a>'+ '<input type="radio" name="n" checked="checked"/><object><param/></object><table></table>'; var a = div[tags]("a")[0],s = a.style; a.expando = true; try{ Array[fn].slice.call(div.childNodes) }catch(e){ sliceNodes = false; } var box = div.cloneNode(true); box.style.width = box.style.paddingLeft = "1px"; root.insertBefore(box, root.firstChild); var w3cBox = box.offsetWidth === 2, mixupsName = document.getElementById(namespace) === box[tags]("a")[0], method = a.matchesSelector || a.webkitMatchesSelector || a.mozMatchesSelector root.removeChild(box); return { //某些浏览器的innerHTML会自动去掉标签外的空白 removeBlank: div.innerHTML.charAt(0) === "<", //某些浏览器会自动为table添加tbody insertTbody: !!div[tags]("tbody").length, sliceNodes:sliceNodes, //IE67会混淆id与name mixupsName:mixupsName, //某些浏览器会自动补全路径 convertUrl: a.getAttribute("href") !== "/nasami", //某些浏览器使用document.getElementByTagName("*")遍历Object元素下的param元素 traverseAllElements: !!div[tags]("param").length, //http://www.cnblogs.com/rubylouvre/archive/2010/01/09/1642978.html traverseAllProperties: !dontEnum, //https://prototype.lighthouseapp.com/projects/8886/tickets/264-ie-can-t-create-link-elements-from-html-literals //某些浏览器不能通过innerHTML序列化link,style,script等元素 serializeAll: !!div[tags]("link").length, //IE的cloneNode才是真正意义的复制,能复制动态添加的自定义属性与事件 cloneAll: !!a.cloneNode(true).expando, //http://www.cnblogs.com/rubylouvre/archive/2010/05/16/1736711.html //在safari下,指定了name属性的radio是无法复制checked属性的 cloneChecked: div[tags]("input")[0].cloneNode(true).checked, //IE67是没有style特性(特性的值的类型为文本),只有el.style(CSSStyleDeclaration) hasStyleAttribute:a.getAttribute("style") !== s, //http://www.cnblogs.com/rubylouvre/archive/2010/05/16/1736535.html //IE8返回".25" ,IE9pp2返回0.25,chrome等返回"0.25" w3cOpacity: s.opacity == "0.25", //某些浏览器不支持w3c的cssFloat属性来获取浮动样式,而是使用独家的styleFloat属性 w3cFloat: !!s.cssFloat, //某些浏览器存在怪异模式,此时盒子模型的宽高所围成的矩形等于border围成的矩形 //就像开发商计算建筑面积时用的是IE6模型 , 业主计算套内面积时用的是W3C模型 w3cBox: w3cBox, //IE8等支持W3C的selector APIs querySelector:!!(document.querySelectorAll && div.querySelectorAll ), matchesSelector: method && method.name,//返回对应的方法名,没有为undefined //除safari与IE外默认新添加的option为选中状态 optSelected: document.createElement("select").appendChild( document.createElement("option") ).selected, //http://oreilly.com/catalog/jscript4/chapter/ch17.html w3cRange:!!(document.implementation && document.implementation.hasFeature("Range","2.0")) } }; //========================================= // 数组化 //========================================== dom.slice = function(){ var item = arguments[0]||[], method = Array[fn].slice; //IE中不能slice节点集合,它们是基于COM的,并非Object的实例,需要转换为原生数组 if(!dom.env.sliceNodes && !(item instanceof Object)){ var i = item.length,ret = []; while(i--){ ret[i] = item[i] } item = ret; } return method.apply(item, method.call(arguments, 1)); } //========================================== // 混入方法 //========================================== dom.mixin = function() { var queue = dom.slice(arguments),deep = false,tk,sk; if(typeof queue[0] === "boolean"){ deep = queue.shift(); } var target = queue[0], source = queue[1] if(queue.length===1){ target = this; source = queue[0] } if (target && source ){ for(var key in source){ if(source[has](key)){ tk = target[key]; sk = source[key]; if(target === tk) continue;//如window.window === window,会陷入死循环, //如果是深拷贝,则检测source的当前属性是否为纯对象或数组,是则特殊处理它, //对于其他类型的属性,它们的处理同浅拷贝 if ( deep && sk && ( dom.isPureObject(sk) || dom.isArray(sk) ) ) { var clone = tk && ( dom.isPureObject(tk) || dom.isArray(tk) ) ? tk : dom.isArray(tk) ? [] : {}; target[ key ] = dom.mixin(deep, clone, sk ); //属性值绝对不能为undefined } else if ( sk !== undefined ) { target[ key ] = sk; } } } if(!dom.env.traverseAllProperties && source[has]){ var d = 3; while ((key = dontEnum[--d])) { source[has](key) && (target[key] = source[key]); } } } if(queue.length > 2 ){ var others = queue.slice(2);//dom.slice(arguments,2); for(var i=0,n=others.length;i<n;i++){ target = arguments.callee(deep,target,others[i]); } } return target; }; dom.include = function(obj){ dom.require("node") dom.mixin(dom.prototype,obj) }; dom.mixin({ //========================================= // 浏览器嗅探 //========================================== //除了另无它法,肯定不使用navigator.userAgent来判定浏览器。因为在第一次浏览器大战初期, //Netscape占绝对统计地位,大部分人们不愿意兼容其他浏览器,并通过检测其UA让他们的网站 //只允许Netscape访问,这就逼使其他浏览器(包括IE)修改自己的UA伪装成Netscape来通过那 //些自以为是的脚本,于是出现每个人都声称自己是别人的局面,即使最新的IE9的UA也是这样 //Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/5.0) ie: !!ie,//内核为trident ie5: ie === 5.5, ie6: ie === 5.6, ie7: ie === 5.7, //指IE8以下的版本或IE8运行于兼容模式下 ie67 :!-[1,] && dom.env.querySelector === false, ie8: ie === 5.8, ie9: ie === 5.9, firefox: !!top.crypto,//内核Gecko opera: is(top.opera,"Opera"),//内核 Presto 9.5为Kestrel 10为Carakan chrome: !!(top.google && top.chrome) ,//内核V8 safari: /apple/i.test(navigator.vendor),// 内核 WebCore quirk: !dom.env.w3cBox, //========================================= // 获取特殊节点 //========================================== //参数都是可选,除了script方法要求可选参数为普通对象,其他为DOM对象 document : function (obj) { if(obj){ //IE5.5中,HTML元素不存在ownerDocument属性 return obj.documentElement ? obj : (obj.ownerDocument || obj.document); }else{ return window.document; } }, root:function(obj){ return dom.document(obj).documentElement; }, head:function(obj){ return dom.document(obj)[tags]("HEAD")[0] ; }, body: function(obj){ return dom.document(obj).body }, script:function(obj){ //注意,charset属性当且仅当同时指定了src属性才有效 //注意,IE下用innerHTML序列化script标签存在bug,script标签前面要有其他节点 dom.parser.innerHTML = '<br><script type="text/javascript" charset="utf-8" ><\/script>' return dom.mixin(dom.parser.childNodes[1],obj); }, //========================================= // 各种判定 //========================================== is:is, isPureObject:isPureObject, //判断是否为XML文档 //http://www.cnblogs.com/rubylouvre/archive/2010/03/14/1685360.html isXML: function(el){ var doc = dom.document(el); return (!!doc.xmlVersion) || (!!doc.xml) || is(doc,"XMLDocument") || (!doc.body); }, //是否这元素节点,如果传入两个参数,则判定元素的标签类型 isElement : function (obj,tag) { if(arguments.length === 2){ tag = dom.isXML(obj) ? tag : tag.toUpperCase() return obj.nodeName === tag ; } return !! (obj && obj.nodeType === 1) ; }, isFormElement : function(obj){ return !!(obj.tagName && "name" in obj && "form" in obj); }, isNative : function(obj){//判定是否为原生方法 return !! obj && (/\{\s*\[native code\]\s*\}/.test(String(obj)) || /\{\s*\/\* source code not available \*\/\s*\}/.test(String(obj))); }, //包括Array,Arguments,NodeList,HTMLCollection,IXMLDOMNodeList与自定义类数组对象 //select.options集合(它们两个都有item与length属性) isArrayLike : function (obj) { if(!obj || obj.document || obj.nodeType || is(obj,"Function")) return false; return isFinite(obj.length) ; }, //检测是否为空对象,只检测本地属性 isEmptyObject: function(obj ) { for ( var key in obj ) if(obj[has] && obj[has](key)) return false; return true; }, isInDomTree : function(node,context){ var root = (context || document).documentElement; return node === root || dom.contains(node,root); }, contains :function(el, root) {//分别为子节点与父节点 if (el.compareDocumentPosition) return (el.compareDocumentPosition(root) & 8) === 8; if (root.contains && el.nodeType === 1){ return root.contains(el) && root !== el; } while ((el = el.parentNode)) if (el === root) return true; return false; }, //========================================= // 各种处理集合的方法 //========================================== keys : function(obj){ var result = [],ri = 0; if(!dom.env.traverseAllProperties && obj[has]){ obj[has]("constructor") && (result[ri++] = "constructor"); obj[has]("toString") && (result[ri++] = "toString"); obj[has]("valueOf") && (result[ri++]= "valueOf"); } for(var key in obj) if(obj[has] && obj[has](key)) result[ri++] = key; return result; }, each: function (obj, fn, bind ) { if (dom.isArrayLike(obj)) { for (var i = 0, n = obj.length ; i < n; i++) { if ( fn.call(bind || obj[i], obj[i], i, obj) === false ) {//绑定作用域 break; } } }else if(obj[has]){ for(var prop in obj){ if(obj[has](prop)){//value,key,obj,绑定对象默认为value if ( fn.call(bind || obj[prop], obj[prop], prop, obj) === false ) { break; } } } if(!dom.env.traverseAllProperties){ var d = 3; while ((prop = dontEnum[--d])) { if(obj[has](prop)){ if ( fn.call(bind || obj[prop], obj[prop], prop, obj) === false ) { break; } } } } } return obj }, map:function(array, fn, bind){ var result = [],ri = 0, value for (var i = 0,n = array.length; i < n; i++){ value = fn.call(bind || array[i],array[i],i,array) if(value != null){ result[ri++] = value; } } return result; }, filter:function(array, fn, bind){ var result = [],ri = 0; for (var i = 0,n = array.length; i < n; i++){ if(fn.call(bind || array[i],array[i],i,array)){ result[ ri++] = array[i]; } } return result; }, //类似python中的range()函数 range : function() { var a = dom.slice(arguments); var solo = a.length <= 1; var start = solo ? 0 : a[0], stop = solo ? a[0] : a[1], step = a[2] || 1; var len = Math.ceil((stop - start) / step); if (len <= 0) return []; var range = []; for (var i = start, ri = 0; true; i += step) { if ((step > 0 ? i - stop : stop - i) >= 0) return range; range[ri++] = i; } }, merge : function(array,args) {//合并集合 array = dom.slice(array); var arrayLength = array.length, length = args.length; while (length--) array[arrayLength + length] = args[length]; return array; }, inArray : function(el,arr){ if(arr.indexOf) return arr.indexOf(el) !== -1; for (var i = 0, n = arr.length; i < n; i++) if (arr[i] === el) return true; return false; }, now: Date.now || function(){ return new Date().valueOf(); }, random : function(min, max, exact){ var range = min + (Math.random()*(max - min)); return exact === void(0) ? Math.round(range) : range.toFixed(exact); }, //var a = {length:4,0:1,1:2,2:3,3:4}; //dom.console.log(dom.toArray(a) toArray : function (obj) { return obj != null ? dom.isArrayLike(obj) ? dom.slice(obj): [obj] :[] }, alias : function(newName) { //如果不指定新名,则随机生成一个,换言之,则进入忍者模式,需要用一个变量来接受它 newName = newName || "__dom__"+dom.now(); window.dom = _dom; return window[namespace] = window[newName] = dom; }, //生成一个值都相同的对象,用于if语句进行过滤 oneObject : function(array,val){ var result = {},value = val !== undefined ? val :1; for(var i=0,n=array.length;i<n;i++) result[array[i]] = value; return result; }, globalEval: function( code ) { //IE中,window.eval()和eval()一样只在当前作用域生效。 //Firefox,Safari,Opera中,直接调用eval()为当前作用域,window.eval()调用为全局作用域。 if ( code && /\S/.test(code) ) { var method = window.execScript ? "execScript" : "eval" try{ window[method](code); }catch(e){} } }, uuid:1, expando : "dom" + (new Date-0),//设在元素上的自定义属性 noop:function(){}, cacher : function(fn, bind, post) { return function (){ var self = arguments.callee, array = dom.slice(arguments), args = array.join("\u25ba"),//►,一个黑色的三角形 cache = self.cache = self.cache || {}, count = self.count = self.count || []; if (cache.hasOwnProperty(args)) { return post ? post(cache[args]) : cache[args]; } count.length >= 1e3 && delete cache[count.shift()]; count.push(args); cache[args] = fn.apply(bind, array); return post ? post(cache[args]) : cache[args]; } }, //此方法只对FF浏览器有效,调试用 //https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Object/noSuchMethod __noSuchMethod__: function(name, args) { dom.require("console") dom.console.error("尝试用以下参数("+args+")执行当前对象的'" + name + "'方法遭遇失败!"); }, ready: function( fn ) {//domReady var self = arguments.callee; self.init(); if ( self.status ) { fn(); } else if ( self.list) { self.list.push( fn ); } } }); dom.mixin(dom.ready,{ list:[], status:false, init: function(){ if (arguments.callee.used ) { return; } arguments.callee.used = true; var fire = dom.ready.fire //用于window.onload的内部 if ( document.readyState === "complete" ) { return fire(); } if (-[1,]) {//Safari3.1+,Chrome,Firefox2+ ,Opera9+,听说IE9也支持DOMContentLoaded //https://developer.mozilla.org/en/Gecko-Specific_DOM_Events document.addEventListener( "DOMContentLoaded", function() { document.removeEventListener( "DOMContentLoaded", arguments.callee , false ); fire(); }, false ); } else {//IE5 IE6 IE7 IE8 //http://dev.jquery.com/ticket/2614 //当页面包含图片时,onreadystatechange事件会触发在window.onload之后, //换言之,它只能正确地执行于页面不包含二进制资源或非常少或者被缓存时 document.attachEvent("onreadystatechange", function(e) { if ( document.readyState == "complete" ) { document.detachEvent("onreadystatechange", arguments.callee ); fire(); } }); //doScroll方法通常只会正确执行一个全新的页面 (function(){ if ( dom.ready.status ) { return; } //doScroll存在于所有标签而不管其是否支持滚动条 //当DOM树时我们就可以调用其doSroll方法 //若用document.documentElement.doScroll(),我们需要判定其是否位于顶层document //http://msdn.microsoft.com/en-us/library/ms536414(VS.85).aspx var node = new Image try { node.doScroll(); node = null//防止IE内存泄漏 } catch( e ) { //javascrpt最短时钟间隔为16ms,这里取其倍数 //http://blog.csdn.net/aimingoo/archive/2006/12/21/1451556.aspx setTimeout( arguments.callee, 64 ); return; } fire(); }); } }, fire: function() { if ( !dom.ready.status ) { if ( !document.body ) { return setTimeout(arguments.callee, 16 ); } dom.ready.status = true; if ( dom.ready.list ) { for(var i=0, fn;fn = dom.ready.list[i++];) fn(); delete dom.ready.list; } } } }); //添加更多见词明义的判定 dom.each(["Array","Function","Number","String","Undefined","Null"],function(name){ dom["is"+name] = function(obj){ return is(obj,name); } }); // ECMA-5 15.4.3.2 if(dom.isNative(Array.isArray)){ dom.isArray = Array.isArray; } // ECMA-5 15.2.3.14 if(dom.isNative(Object.keys)){ dom.keys = Object.keys; } (function(){ //游览器环境不能使用Msxml2.XMLHTTP.5.0与Msxml2.XMLHTTP.4.0 //http://blogs.msdn.com/xmlteam/archive/2006/10/23/using-the-right-version-of-msxml-in-internet-explorer.aspx var s = ["XMLHttpRequest", "ActiveXObject('Msxml2.XMLHTTP.6.0')", "ActiveXObject('Msxml2.XMLHTTP.3.0')", "ActiveXObject('Msxml2.XMLHTTP')", "ActiveXObject('Microsoft.XMLHTTP')"]; if(dom.ie7 && location.protocol === "file:"){ s.shift(); } for(var i = 0 ,el;el=s[i++];){ try{ if(eval("new "+el)){ dom.xhr = new Function( "return new "+el) break; } }catch(e){} } })(); //========================================= // 核心模块 模块加载系统 //========================================== //dom有三个重要的信息储存区, //一个是env用于储存与浏览器有关的东西 //一个是lib用于储存与框架有关的东西 //一个是cache用于储存运行收集的东西 dom.lib = { loaded:{}, //获取核心模块所在的JS文件所在的文件夹路径 baseUrl :(function(){ var result; try{ throw "" }catch(e){ result = e.fileName || e.sourceURL; } if(!result){ var scripts = document[tags]('script'), script = scripts[scripts.length - 1]; result = script.src; } return result.substr( 0, result.lastIndexOf('/')); })() } //name为模块名,create不存在此属性是否创建一个空对象 dom.mixin({ //创建一个命名空间 namespace:function(name,create,context){ var parts=name.split("."),obj = context || window; for(var i=0, p; obj && (p=parts[i]); i++){ if(i == 0 && this[p]){ p = this[p]; } obj = (p in obj ? obj[p] : (create ? obj[p]={} : undefined)); } return obj; }, //同步加载模块 require : function(name,timeout){ name = name.indexOf("dom.") === 0 ? name.slice(4):name timeout = timeout || 2000 var module = "dom."+name,url; if(dom.lib.loaded[name]) return //处理dom.node(http://www.cnblogs.com/rubylouvre/dom/node.js)的情形 var _url = module.match(/\(([^)]+)\)/); url = _url && _url[1] ? _url[1] : dom.lib.baseUrl+"/"+ module.replace(/\./g, "/") + ".js"; var xhr = dom.xhr(); xhr.open("GET",url,false); xhr.setRequestHeader("If-Modified-Since","0"); xhr.send(null); dom.globalEval( xhr.responseText|| "") setTimeout(function(){ try{ xhr.abort() }catch(e){} },timeout); }, //提供命名空间,标识此模块已经加载过 provide : function(name){ name = name.indexOf("dom.") === 0 ? name.slice(4):name dom.lib.loaded[name] = true; dom.namespace("dom."+name,true) } }); //====================添加其他模块====================== window[namespace] = window.dom = dom; })();
许多代码我以前都放过出来了,应该没有什么难度,我也将会在下一篇讲解它们的。如果到时能把chm文档搞出来就最好不过了,现在姑且先放出让大家瞧瞧。最后还是那句老话,如果某某方法有什么好的实现,请不吝赐救!
下载回来后对着文件点右键-->属性-->解除锁定。