- JavaScript实战-JavaScript、jQuery、HTML5、Node.js实例大全(第2版)
- 张泽娜
- 1873字
- 2025-02-24 19:10:52
6.4 复杂的树视图
制作用户控件的目的是提高代码复用率和移植,也因此方便了代码的维护。
在6.2节里实现了一个最简单的树视图,但是这个树视图对于移植和复用都很不方便。如何对它进行控件化呢?
6.4.1 闭包隔离变量污染
众所周知,jQuery强大但是入口单一,没有变量污染,它是如何做到的呢?
(function( window, undefined ) { var jQuery=... ....内部代码... window.jQuery = window.$ = jQuery; })( window );
下面将树控件起名为T,挂载到window对象下,这样才能保证在网页中被自由调用。
(function(window){ window.T = window.T || function(){}; })(window);
6.4.2 省去new关键字调用控件
想一想jQuery,似乎大家在调用的时候就没有用new关键字。我们也来实现一个,另外还需要让控件接受一些配置参数,那么就需要改造一下构造函数:
window.T = window.T || function(cfg){ if (! (this instanceof T)) { return new T(cfg) }; //省略new 关键字调用 this.SET = cfg; //存储起来,让内部可以自由使用 this.ROOT = null; //记录根节点 };
6.4.3 丰富控件方法
在编写代码前,应该先打个草稿。由于篇幅限制,我们只完成window资源管理器的模拟,即控件可以自由展开、收缩,并且能发出每个节点的单击事件。其效果如图6-3所示。

图6-3 树控件效果图
其中“本书目录”的第二和第三节点是收缩的,“控件说明”的第三节点是展开的。因为代码是复用的,为了便于移植,笔者把前面base.js中积累的常用方法挂载到控件里。
/*静态方法*/ T.extend = function(){/*合并对象*/ var len = arguments.length ,obj = arguments[0] ,tmp if(! obj || typeof obj === "number" || obj.constructor ! == Object){ obj = {}; } for (var i = 1; i < len; i++){ tmp = arguments[i]; if(tmp){ for (var o in tmp){ obj[o] = tmp[o]; } } } return obj; }; T.$ = function(id){//取得DOM 元素 return document.getElementById(id); } T.hasClass = function(el, cls){//判断是否包含某个class return el.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)')); }; T.addClass = function(el, cls){//增加class if (! this.hasClass(el, cls)) el.className += " "+cls; }; T.removeClass = function(el, cls) {//移除某个class if (this.hasClass(el, cls)) { var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)'); el.className = el.className.replace(reg, ' '); } }; T.find = function(el, target){//根据ClassName, tagName, ID 查找 var target = target.replace(/#|\./g, ""); var cd = el.children; //获取元素子元素集合 for(var i=0; i<cd.length; i++){ var p = cd[i]; if(p.tagName.toLowerCase() === target.toLowerCase() || p.id === target || T.hasClass(p, target)) return p; } return null; }; T.addListener = function(target, type, handler){ //绑定事件 if(target.addEventListener){ target.addEventListener(type, handler, false); }else if(target.attachEvent){ target.attachEvent("on"+type, handler); }else{ target["on"+type]=handler; } };
同时把它们作为静态方法,无须实例化就可以调用,也便于其他控件使用。
作为一个控件总会有一些独有的动态方法,也就是实例化后才可使用的,将这样的方法挂载到prototype属性下:
var P = T.prototype;
通过转接,将省略不少字节,代码也便于阅读。
控件常见的一个方法就是init()初始化方法:
P.init = function(cfg){//模板和配置文件的处理 T.extend(this.SET, cfg||{}); var set = this.SET, dic = set.data for(var i in dic){//用来处理所属关系 if(dic[i].pid ! ==undefined){ //判断是指定的pid 才处理 var pid = dic[i].pid; if(dic[pid]){ //判断父类是否存在 dic[pid].child || (dic[pid].child = []); //判断父类有无child,无则初始化 dic[pid].child.push(i); //登记到父类child 中 } } } this.addNode(T.$(this.SET.id), -1); };
在init()里首先用静态方法extend()合并处理配置信息,并且格式化JSON数据,最后调用添加节点函数addNode()。
P.addNode = function(el, pid){ //在某个父节点下增加子节点 if(this.ROOT === null) this.ROOT = pid; //记录根id var ul = document.createElement("ul"); //创建一个ul 元素 var dic = this.SET.data; for(var i in dic){//遍历数据 if(dic[i].pid == pid){ //判断节点是否都是同一个父节点,即是否是当前需要显示的节点 var dl = dic[i]; //取得一个节点的信息 var child = dl.child && dl.child.length>0; //判断是否还有子类 var li = document.createElement("li"); //创建一个li 元素 li.innerHTML = '<span id="s'+i+'"></span><a href="'+ dl.url+'">'+dl.cn+'</a>'; //拼接html if(child){ this.addNode(li, i); //递归下去 this.setParentNodeEvent(li); //设置父节点事件 } this.setNodeClass(li, pid, child); //设置节点样式 this.setNodeEvent(li); //设置节点事件 ul.appendChild(li); //把拼装好的li 追加到ul 中去 }else{ continue; //继续下一个循环 } } el.appendChild(ul); //插入到给定的元素中 };
addNode()方法是主要的方法,这里需要增加一些更丰富的操作。比如设置父节点的展开和关闭事件、设置节点样式、设置节点事件等,而且每个节点除<a>标签之外还有<span>标签。
P.setNodeClass = function(el, pid, child){ var cls = "page"; //默认子节点样式 if(this.ROOT === pid){ cls = "root"; //设置根节点样式 }else if(child>0){ cls = "open"; //设置父节点样式 } T.addClass(el, cls); };
根节点的样式和其他节点样式均不同:
P.setParentNodeEvent = function(el){ var span = el.firstChild; //找到第一个子元素 T.addListener(span, "click", function(){ if(T.hasClass(el, "open")){ T.removeClass(el, "open"); T.addClass(el, "close"); }else{ T.removeClass(el, "close"); T.addClass(el, "open"); } }); };
父节点的展开和关闭都是靠单击事件触发的:
P.setNodeEvent = function(el){ var a = T.find(el, "a"); var self = this; //存储this 对象 T.addListener(a, "click", function(event){ if(typeof self.SET.onclick === "function"){ self.SET.onclick(event.srcElement||this); //这里的this 和上面的this 指向不同的对象 } }); };
对节点单击的处理也是绑定在单击事件上的,只是元素不同。这里还接受配置参数里传递过来的onclick回调函数,由于function在JavaScript里可以作为参数传递,因此配置灵活度会得到极大的提高,不过这些回调函数都需要在内部作为验证并调用,包括回调函数能使用的参数都可以控制。
在网页中如何设置回调函数呢?请看下面的代码。
var myTree = T({id:"mytree", data:dic ,onclick:function(node){ alert(node.text); //弹出节点文本 } }); myTree.init(); var myTree2 = T({id:"mytree2", data:dic2}); myTree2.init();
回调函数就是一个参数而已。整个HTML结构如【范例6-3】所示。
【范例6-3 控件的HTML结构及其调用】
1. <! DOCTYPE html> 2. <html> 3. <head> 4. <title>javascript tree</title> 5. <link rel="stylesheet" href="T.css" type="text/css" /> 6. </head> 7. <body> 8. <div id="mytree" class="T"></div> 9. <div id="mytree2" class="T"></div> 10. </body> 11. </html> 12. <script src="base.js"></script> 13. <script src="z3fTree.js"></script> 14. <script> 15. var dic = { 16. "0" : {pid:-1, cn:’本书目录’, url:'/'} 17. , "1" : {pid:0, cn:’第1 章 JavaScript 概述’, url:'/01'} 18. , "2" : {pid:0, cn:’第2 章 用JavaScript 验证表单’, url:'/02'} 19. , "11" : {pid:1, cn:'1.1 认识JavaScript', url:'javascript:; '} 20. , "12" : {pid:1, cn:'1.2 配置JavaScript 开发环境’, url: 'javascript:; '} 21. , "3" : {pid:0, cn:’第3 章 JavaScript 实现的照片展示’, url:'/03'} 22. , "21" : {pid:2, cn:'2.1 最简单的表单验证 - 禁止空白的必填项目’, url:'javascript:; '} 23. , "22" : {pid:2, cn:'2.2 处理各种类型的表单元素’, url: 'javascript:; '} 24. , "23" : {pid:2, cn:'2.3 输入的邮箱地址正确吗?用正则来校验复杂的 格式要求’, url:'javascript:; '} 25. , "24" : {pid:2, cn:'2.4 改善用户体验’, url:'javascript:; '} 26. , "31" : {pid:3, cn:'3.1 功能设计’, url:'javascript:; '} 27. , "32" : {pid:3, cn:'3.2 照片加载与定位’, url:'javascript:; '} 28. , "33" : {pid:3, cn:'3.3 响应鼠标动作’, url:'javascript:; '} 29. }; 30. var dic2 = { 31. "0" : {pid:-1, cn:’控件说明’, url:'/'} 32. , "1" : {pid:0, cn:’构造器’, url:'/01'} 33. , "11" : {pid:1, cn:’参数:cfg', url:'javascript:; '} 34. , "2" : {pid:0, cn:’静态方法’, url:'/02'} 35. , "21" : {pid:2, cn:'extend(obj[, obj]...[, obj])', url:'javascript:; '} 36. , "22" : {pid:2, cn:'$(id)', url:'javascript:; '} 37. , "23" : {pid:2, cn:'hasClass(el, cls)', url:'javascript:; '} 38. , "24" : {pid:2, cn:'addClass(el, cls)', url:'javascript:; '} 39. , "25" : {pid:2, cn:'removeClass(el, cls)', url:'javascript:; '} 40. , "26" : {pid:2, cn:'addListener(target, type, handler)', url:'javascript:; '} 41. , "3" : {pid:0, cn:’动态方法’, url:'/03'} 42. , "31" : {pid:3, cn:'init()', url:'javascript:; '} 43. , "32" : {pid:3, cn:'addNode(el, pid)', url:'javascript:; '} 44. , "33" : {pid:3, cn:'setNodeClass(el, pid, child)', url:'javascript:; '} 45. , "34" : {pid:3, cn:'setNodeEvent(el)', url:'javascript:; '} 46. }; 47. var myTree = T({id:"mytree", data:dic 48. , onclick:function(node){ 49. alert(node.innerText||node.text); //输出文字(兼容IE 和其他浏览器) 50. } 51. }); 52. myTree.init(); 53. var myTree2 = T({id:"mytree2", data:dic2}); 54. myTree2.init(); 55. </script>
读者可能注意到了,一个漂亮的控件不可能完全没有CSS, 【范例6-3】中的T.css代码如下:
ul, li{ list-style: none; }/*去掉自带的样式*/ .T .root{ background: url("img/base.gif") no-repeat scroll 0 0 transparent; padding-left: 20px; /*把图标用背景的方式显示,不重复,然后内容向右位移*/ } .T .open{ background: url("img/folderopen.gif") no-repeat scroll 0 0 transparent; } .T .page{ background: url("img/page.gif") no-repeat scroll 0 0 transparent; padding-left: 20px; } .T .open span{display:inline-block; width:20px; height:20px; } .T .page span{display:none; } .T .close{ background: url("img/folder.gif") no-repeat scroll 0 0 transparent; } .T .close span{display:inline-block; width:20px; height:20px; } .T .close ul{display:none; }/*当父节点切换到关闭状态时,其子节点自动隐藏*/
到此为止,基本上完成了一个相对6.2节更为复杂的树视图,当然应用于实际项目时,还需要补充一些更为丰富的操作。本例仅仅讲了基本的流程和步骤,以便引导读者入门,相信读者能够以此举一反三,编写出更多优秀的控件。
由于兼容性问题,类似这样甚至更为复杂的用户控件都是基于jQuery的,这样做一方面是因为节省开发成本,另一方面是因为jQuery的广泛应用。