JQUERY 插件开发——MENU(导航菜单)
故事背景:由于最近太忙了,已经很久没有写jquery插件开发系列了。但是凭着自己对这方面的爱好,我还是抽了一些时间来过一下插件瘾的。今天的主题是导航菜单,这个我相信不管做B/S还是做C/S都非常熟悉一个功能模块。其实大家有没有发现,我们开发插件的目的是为了重用,既然是需要重用的肯定也是开发中常用的,所以说白了,我们开发插件的需求来自开发中常用的功能。只要你想,你仔细分析,相信绝大部分常用功能都可以分装出来做插件的。额。。。有种秀智商的赶脚啊,呵呵,不好意思,想到哪里就说道哪里了。相信大家还是能清楚啥时需要开发插件的。本篇文章其实需求来源是来源于我现在做的一个项目,但是后期我又做了优化,和原有需求不同。当然,我改的这个版本的样式就没有那么炫了。但是代码肯定优化了。
还是我一直提到的,你开发插件,你肯定要清楚该插件是做啥的,啥时用。也就是需求分析要做好。相信有人会说又装13了,其实这不是装,因为menu是大家所熟知的,但是我也相信就算大家熟知的事情你也不一定就了解它的所有功能。开发的插件是根据业务来的,不同的业务需求对导航菜单的要求也不同。不管是样式还是功能。例如面包削,这个就是菜单的“附赠”品,很多网站需要有,但是也有很多网站不需要。所以,请大家也不要装,除非你真的是大牛,可以目空一切。但是一般大牛都好像很谦虚的,很深奥的样子,至少我看到的都不错,^_^
对了,其实有一句好我很想说的就是,如果你喜欢或者有意向开发jquery插件的,请你熟悉一下div+css页面布局,如果你这方面不熟悉,其实是苦恼的。相信开发过的人都知道。很多人会说我们公司有前端专门做样式的,但是我想说的是,多学点没什么坏处。这样方便你开发,能提高自己写的代码质量。
好了,感觉一扯就像吃了炫迈似的,根本停不下来,其实也就是说开发需要扯。。。^_^。。。我是想看文章的人也很累,让大家轻松一点。
故事主题:jquery插件开发——Menu,导航菜单开发。
正常的menu功能:1、实现菜单的切换 2、实现切换内容的加载 3、控制菜单的收缩 4、控制样式变更
附加功能:面包削导航
本次开发用了大量的递归思想,其实好的递归可以为你节省很多很多代码,但是说实话,复杂的递归在错误排查上还是很繁琐的。所以我们要量力而行,当然还是希望大家能熟练运用递归,毕竟你将来是要成为牛X的猿,所以你就必须会各种算法。
当然本次和上次开发的插件想必又添加了委托思想 和 事件句柄。当然这个我也得感谢我的一个同事,是在他的提醒下,我添加的,这样写的确实现了元素和事件间的解耦。当然这个也是模仿面向对象思想中的开发了。
其实当你真正去多次开发插件时候,你就会发现,其实开发插件就分三步走。
第一步:定义插件和参数 var menu = function () {this.defaultParams = {};};
第二步:定义插件属性、方法 menu.prototype = {constructor: menu,init:function (params){}};
第三步:对外分装 $.menu = new menu();
其实就是这三步,然后写好每一步实现就好了。很简单吧。^_^我感觉这三步就像一个系统的架构一样,大的方向定下来,下面就是向框架中填充东西,实现功能即可。当然,开发中你要把公共部分先剥离出来,下面具体讲解开发的代码。分为以下几个部分。
第一部分:这部分是公共部分,比上一次写的多了delegate,这个下面注册事件的时候会用到,理解就像面向对象语言中理解一样。如果对委托不是很清楚的可以百度看看,相信这种思想已经为大部分人所知了。
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
$( function () { // 说明:创建委托函数 // context:函数上下文 // params:参数【必须是数组形式】,可以为空 Function.prototype.delegate = function (context, params) { var func = this ; return function () { if (params == null ) { return func.apply(context); } return func.apply(context, params); }; }; var menuCommon = { coverObject: function (obj1, obj2) { var o = this .cloneObject(obj1, false ); var name; for (name in obj2) { if (obj2.hasOwnProperty(name)) { o[name] = obj2[name]; } } return o; }, cloneObject: function (obj, deep) { if (obj === null ) { return null ; } var con = new obj.constructor(); var name; for (name in obj) { if (!deep) { con[name] = obj[name]; } else { if ( typeof (obj[name]) == "object" ) { con[name] = $.cloneObject(obj[name], deep); } else { con[name] = obj[name]; } } } return con; }, // 说明:实现委托 delegate: function (func, context, params) { if ($.isFunction(func)) { return func.delegate(context, params); } else { return $.noop; } }, getParam: function (param) { if ( typeof (param) == "undefined" ) { return "" ; } else { return param; } } }; }); |
第二部分:定义导航默认参数,其中的data参数的格式我已经给出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
var menu = function () { //参数定义 this .defaultParams = { id: "" , //导航容器ID data: "" , //数据 包含title、depth、recordId、parentId、children //格式:[ // {title: "第一级——1", // depth:1, // recordId:1, // parentId:0, // children:[ // {title: "第二级", // depth:2, // recordId:3, // parentId:1, // children:[] // }] // }, // {title: "第一级——2", // depth:1, // recordId:2, // parentId:0, // children:[ // {title: "第二级", // depth:2, // recordId:4, // parentId:2, // children:[] // }] // }] boolBreadCut: true , //是否要面包削 breadCutId: "" , //面包削ID navClickCallback: $.noop //导航点击回调事件 }; this .options = {}; }; |
第三部分:定义属性、方法,并代码实现。这部分很重要,封装了各种方法,包括我说的事件句柄、递归等思想都在这里体现。代码中有注释。其中createMenu、getNodeById getBreadCutNameList这个三个方法是用递归实现的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
menu.prototype = { constructor: menu, init: function (params) { this .options = menuCommon.coverObject( this .defaultParams, params); this ._init(); }, _init: function () { if ( this .options.data == null ) { return ; } if ( this .options.data.length < 1) { return ; } var data = this .options.data; var id = this .options.id; var htmlStr = this .createMenu(data, id, "" ); $( "#" + id).html(htmlStr); this ._registeNavClick(); }, //生成菜单的Html元素 createMenu: function (data, id, htmlStr) { $.each(data, function (i, item) { var depth = item.depth; var recordId = item.recordId; var parentId = item.parentId; var marginLeft = parseInt(item.depth) * 20; htmlStr += "<div class='zws' depth='" + depth + "'>" ; htmlStr += " <div id='" + recordId + "' isShow='true' depth='" + depth + "' parentId='" + parentId + "' " ; if (depth === 1) { htmlStr += "class='menu_depth_1' >" ; } else { htmlStr += "class='menu_depth_other' >" ; } htmlStr += "<div style ='width: 16px;height: 16px;margin-top: 11px;margin-left:" + marginLeft; htmlStr += "px;float: left;'></div>" ; //小图标 htmlStr += "<div class ='meun_title'>" + item.title + "</div>" ; //标题 htmlStr += "</div>" ; if (item.children != null && item.children.length > 0) { htmlStr += "<div class='meun_navArea' depth='" + item.children[0].depth + "' parentId='" + recordId + "'" ; htmlStr += " isShow='false' navArea=''>" ; //递归实现 htmlStr = menu.prototype.createMenu(item.children, id, htmlStr); htmlStr += "</div>" ; } htmlStr += "</div>" ; }); return htmlStr; }, //注册事件 _registeNavClick: function () { var options = this .options; $( "div[depth][isShow='true']" ).each( function (i, item) { var itemClick = menuCommon.delegate(menu.prototype._handleNavClick, this , [{ item: item }]); //样式改变 var itemClickCallBack = menuCommon.delegate(options.navClickCallback, this ); //回调事件 var itemShowBreadCut = menuCommon.delegate(menu.prototype.createBreadCut, this , [{ item: item, options: options }]); //面包削 $(item).click(itemClick); $(item).click(itemClickCallBack); $(item).click(itemShowBreadCut); }); }, //事件句柄 _handleNavClick: function (params) { var id = params.item.id; var isShow = $( "div[navArea][parentId='" + id + "']" ).attr( "isShow" ); var depth = parseInt($( "#" + id).attr( "depth" )); var parentId = parseInt($( "#" + id).attr( "parentId" )); var navHide = $( "div[parentId='" + parentId + "'][depth='" + depth + "']~div[depth='" + (depth + 1) + "'][navArea]" ); navHide.attr( "isShow" , "false" ); navHide.css( "display" , "none" ); var navCurr = $( "div[parentId='" + id + "']" ); if (isShow == "true" ) { navCurr.attr( "isShow" , "false" ); navCurr.css( "display" , "none" ); } else { navCurr.attr( "isShow" , "true" ); navCurr.css( "display" , "block" ); } }, //说明: // 验证面包削 validateBreadCut: function (options) { if (!options.boolBreadCut) { return false ; } var breadCutObj = $( "#" + options.breadCutId); //面包削区域 if (options.breadCutId == "" || breadCutObj.length < 1) { return false ; } return true ; }, //说明: // 创建面包削 createBreadCut: function (params) { var item = params.item; var itemId = item.id; var options = params.options; var optionData = options.data; if (!menu.prototype.validateBreadCut(options)) { return ; } var depth = params.item.depth; var separator = "<div class='meun_breadCut_separator'> > </div>" ; //分隔符 var breadCutHtml = "" ; breadCutHtml += "<div class='meun_breadCut'>" ; var itemNode = menu.prototype.getNodeById(itemId, optionData); var breadCutNameList = menu.prototype.getBreadCutNameList(itemNode, optionData, []); for ( var i = 1; i <= depth; i++) { breadCutHtml += "<div class='meun_breadCut_name'>" ; breadCutHtml += breadCutNameList[depth - i]; breadCutHtml += "</div>" ; if (i != depth) { breadCutHtml += separator; } } breadCutHtml += "</div>" ; $( "#" + options.breadCutId).html(breadCutHtml); }, //说明: // 获取面包削名称列表 // item:当前点击的导航 // optionsData:数据源 // breadCutNameList:返回列表 getBreadCutNameList: function (item, optionData, breadCutNameList) { if (item != null && item.parentId >= 0) { var title = menu.prototype.getNodeById(item.recordId, optionData).title; breadCutNameList.push(title); //获得列表 item = menu.prototype.getNodeById(item.parentId, optionData); //递归实现 menu.prototype.getBreadCutNameList(item, optionData, breadCutNameList); } return breadCutNameList; }, //说明: // 根据ID获取节点 // id:节点ID // optionsData:数据源 getNodeById: function (id, optionsData) { if (id < 1) { return null ; } $.each(optionsData, function (i, v) { if (v.recordId == id) { nodeTS = v; return false ; } if (v.children.length > 0) { //递归实现 menu.prototype.getNodeById(id, v.children, nodeTS); } }); return nodeTS; } }; |
第四部分:这部分很简单,就是对外封装,一句话而已。
1
|
$.menu = new menu(); |
第五部分:这部分当然是调用啦^_^,具体的参数说明在定义默认参数的时候都用注释,这里就不再累述。
1
2
3
4
5
6
7
8
9
|
$.menu.init({ id: "leftMenu" , data: data, navClickCallback: function () { //这里是点击导航的回调事件,一般用于加载页面 }, boolBreadCut: true , breadCutId: "mbx" }); |
第六部分:样式表,本次样式比较简单、少,所以可以贴出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
.menu_depth_1{ width: 200px;cursor:pointer; border-top-left-radius: 0px !important; border-top-right-radius: 0px !important; border-bottom-right-radius: 0px !important; border-bottom-left-radius: 0px !important; border: 0px !important; bottom: auto !important; float: none !important; height: auto !important; left: auto !important; line-height: 1.8em !important; margin: 0px !important; outline: rgb(0, 0, 0) !important; overflow: visible !important; padding: 0px !important; position: static !important; right: auto !important; top: auto !important; vertical-align: baseline !important; width: auto !important; box-sizing: content-box !important; font-family: Consolas, 'Bitstream Vera Sans Mono', 'Courier New', Courier, monospace !important; min-height: inherit !important; color: gray !important; background: none !important;">#FFD3D2;height: 38px;line-height: 38px;margin-top:2px; } .menu_depth_other{ width: 200px;cursor:pointer; border-top-left-radius: 0px !important; border-top-right-radius: 0px !important; border-bottom-right-radius: 0px !important; border-bottom-left-radius: 0px !important; border: 0px !important; bottom: auto !important; float: none !important; height: auto !important; left: auto !important; line-height: 1.8em !important; margin: 0px !important; outline: rgb(0, 0, 0) !important; overflow: visible !important; padding: 0px !important; position: static !important; right: auto !important; top: auto !important; vertical-align: baseline !important; width: auto !important; box-sizing: content-box !important; font-family: Consolas, 'Bitstream Vera Sans Mono', 'Courier New', Courier, monospace !important; min-height: inherit !important; color: gray !important; background: none !important;">#FFD3D2;height: 38px;line-height: 38px;margin-top:2px;display:none } .meun_title{ width: auto;height: 100%;float: left; } .meun_navArea{ width:100%;height:auto; } /*面包削*/ .meun_breadCut { width:100%;height:30px;line-height:30px; } .meun_breadCut_name{ width:auto;height:30px;line-height:30px;float:left } .meun_breadCut_separator { width:auto;height:30px;line-height:30px;margin: 0px 5px;float:left } |
第七部分:当然是最后测试了啊,测试很重要,要相信好的代码是测出来的。哈哈。。。
总结:其实本次开发比较急,按照我上两篇文章,其实我应该在添加一个主题部分的,当然这里就是为什么说我要大家学习div+css的原因了,如果你会布局,你可以做出各种你喜欢的主题风格。这次我偷懒了,没有加上,后期我会补上。文章比较长,很多知识点我也没有写详细。如果有需要源码的或者想共同探讨的同仁,随时联系我,QQ:296319075 ,注明园友就好,同时也希望大家也能提出宝贵意见,不吝赐教。秉承共同探讨、共同进步!如有转载,请注明出处,谢谢!^_^