故事背景:由於最近太忙了,已經很久沒有寫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 $(function () { 2 // 說明:創建委托函數 3 // context:函數上下文 4 // params:參數【必須是數組形式】,可以為空 5 Function.prototype.delegate = function (context, params) { 6 var func = this; 7 return function () { 8 if (params == null) { 9 return func.apply(context); 10 } 11 return func.apply(context, params); 12 }; 13 }; 14 var menuCommon = { 15 coverObject: function (obj1, obj2) { 16 var o = this.cloneObject(obj1, false); 17 var name; 18 for (name in obj2) { 19 if (obj2.hasOwnProperty(name)) { 20 o[name] = obj2[name]; 21 } 22 } 23 return o; 24 }, 25 cloneObject: function (obj, deep) { 26 if (obj === null) { 27 return null; 28 } 29 var con = new obj.constructor(); 30 var name; 31 for (name in obj) { 32 if (!deep) { 33 con[name] = obj[name]; 34 } else { 35 if (typeof (obj[name]) == "object") { 36 con[name] = $.cloneObject(obj[name], deep); 37 } else { 38 con[name] = obj[name]; 39 } 40 } 41 } 42 return con; 43 }, 44 // 說明:實現委托 45 delegate: function (func, context, params) { 46 if ($.isFunction(func)) { 47 return func.delegate(context, params); 48 } else { 49 return $.noop; 50 } 51 }, 52 getParam: function (param) { 53 if (typeof (param) == "undefined") { 54 return ""; 55 } else { 56 return param; 57 } 58 } 59 }; 60 });
第二部分:定義導航默認參數,其中的data參數的格式我已經給出。
1 var menu = function () { 2 //參數定義 3 this.defaultParams = { 4 id: "", //導航容器ID 5 data: "", //數據 包含title、depth、recordId、parentId、children 6 //格式:[ 7 // {title: "第一級——1", 8 // depth:1, 9 // recordId:1, 10 // parentId:0, 11 // children:[ 12 // {title: "第二級", 13 // depth:2, 14 // recordId:3, 15 // parentId:1, 16 // children:[] 17 // }] 18 // }, 19 // {title: "第一級——2", 20 // depth:1, 21 // recordId:2, 22 // parentId:0, 23 // children:[ 24 // {title: "第二級", 25 // depth:2, 26 // recordId:4, 27 // parentId:2, 28 // children:[] 29 // }] 30 // }] 31 boolBreadCut: true, //是否要面包削 32 breadCutId: "", //面包削ID 33 navClickCallback: $.noop //導航點擊回調事件 34 }; 35 this.options = {}; 36 };
第三部分:定義屬性、方法,並代碼實現。這部分很重要,封裝了各種方法,包括我說的事件句柄、遞歸等思想都在這里體現。代碼中有注釋。其中createMenu、getNodeById getBreadCutNameList這個三個方法是用遞歸實現的。

1 menu.prototype = { 2 constructor: menu, 3 init: function (params) { 4 this.options = $.coverObject(this.defaultParams, params); 5 this._init(); 6 }, 7 _init: function () { 8 this._initMenu(); 9 }, 10 _initMenu: function () { 11 if (this.options.data == null) { 12 return; 13 } 14 if (this.options.data.length < 1) { 15 return; 16 } 17 var htmlStr = this.createMenu(this.options.data, this.options.id, ""); 18 $("#" + this.options.id).html(htmlStr); 19 //注冊導航事件 20 this._registeNavClick(); 21 }, 22 23 //生成菜單的Html元素 24 createMenu: function (data, id, htmlStr) { 25 $.each(data, function (i, item) { 26 var depth = item.depth; 27 var recordId = item.recordId; 28 var parentId = item.parentId; 29 var marginLeft = parseInt(item.depth) * 20; 30 31 htmlStr += "<div class='zwsMenu' depth='" + depth + "'>"; 32 33 if (depth === 1) { 34 htmlStr += "<div id='" + recordId + "' isShow='true' depth='" + depth + "' parentId='" + parentId + "' class='menu_depth_1' >"; 35 htmlStr += " <div class='menu_depth_1_icon' attrIcon='firstLevel' style ='margin-left:" + marginLeft + "px;'></div>";//第一級小圖標 36 htmlStr += " <div class ='meun_title' >" + item.title + "</div>";//標題 37 htmlStr += "</div>"; 38 } else { 39 htmlStr += "<div id='" + recordId + "' isShow='true' depth='" + depth + "' parentId='" + parentId + "' class='menu_depth_other' >"; 40 htmlStr += " <div class='menu_depth_other_icon' attrIcon='otherLevel' style ='margin-left:" + marginLeft + "px;'></div>";//其他級小圖標 41 htmlStr += " <div class ='meun_title' >" + item.title + "</div>";//標題 42 htmlStr += "</div>"; 43 } 44 45 if (item.children != null && item.children.length > 0) { 46 htmlStr += "<div class='meun_navArea' depth='" + item.children[0].depth + "' parentId='" + recordId + "' isShow='false' navArea=''>"; 47 48 htmlStr = menu.prototype.createMenu(item.children, id, htmlStr); 49 50 htmlStr += "</div>"; 51 } 52 53 htmlStr += "</div>"; 54 }); 55 56 return htmlStr; 57 }, 58 59 /*****************(注冊事件 begin)*****************/ 60 61 //說明: 62 // 注冊導航事件 63 _registeNavClick: function () { 64 var options = this.options; 65 66 $("div[depth][isShow='true']").each(function (i, item) { 67 var itemClick = $.delegate(menu.prototype._handleNavClick, this, [{ item: item }]);//樣式改變 68 var itemClickCallBack = $.delegate(options.navClickCallback, this);//回調事件 69 var itemShowBreadCut = $.delegate(menu.prototype.createBreadCut, this, [{ item: item, options: options }]);//面包削 70 71 $(item).click(itemClick); 72 $(item).click(itemClickCallBack); 73 $(item).click(itemShowBreadCut); 74 75 }); 76 }, 77 78 //說明: 79 // 注冊面包削事件 80 _registeBreadCutClick: function () { 81 $(".meun_breadCut_name").each(function (i, item) { 82 var breadCutId = $(this).attr("id"); 83 var itemClick = $.delegate(menu.prototype._handleBreadCutClick, this, [{ breadCutId: breadCutId }]);//樣式改變 84 $(item).click(itemClick); 85 }); 86 }, 87 88 /*****************(注冊事件 end)*****************/ 89 90 /*****************(事件句柄 begin)*****************/ 91 92 //說明: 93 // 導航事件句柄 94 // params:導航每行元素 95 _handleNavClick: function (params) { 96 var id = params.item.id; 97 98 var isShow = $("div[navArea][parentId='" + id + "']").attr("isShow"); 99 var depth = parseInt($("#" + id).attr("depth")); 100 var parentId = parseInt($("#" + id).attr("parentId")); 101 102 //當前深度級的導航 103 var currDepthLevel = $("div[parentId='" + parentId + "'][depth='" + depth + "']"); 104 105 //獲取下級導航區域 106 var navHide = currDepthLevel.next("div[depth='" + (depth + 1) + "'][navArea]"); 107 navHide.attr("isShow", "false").css("display", "none"); 108 109 //將所有導航都置成 未選中狀態 110 var navHideIconOtherLevel = $("div[attrIcon='otherLevel']"); 111 var meunTitle = $("div.meun_title"); 112 navHideIconOtherLevel.removeClass("menu_depth_other_icon_selected").addClass("menu_depth_other_icon"); 113 meunTitle.removeClass("meun_title_color"); 114 115 //當點擊第一級導航時候 116 if (depth === 1) { 117 var navHideIconFirstLevel = $("div[attrIcon='firstLevel']"); 118 navHideIconFirstLevel.removeClass("menu_depth_1_icon_selected").addClass("menu_depth_1_icon"); 119 } 120 121 //獲取第一級導航和其他級導航中圖標 122 var iconFirst = $("#" + id).find("div[attrIcon='firstLevel']"); 123 var iconOther = $("#" + id).find("div[attrIcon='otherLevel']"); 124 var currTitle = $("#" + id).find("div.meun_title"); 125 126 //控制當前點擊的導航的下級導航是否顯示 127 var navCurr = $("div[parentId='" + id + "']"); 128 if (isShow == "true") { 129 navCurr.attr("isShow", "false").css("display", "none"); 130 131 currTitle.removeClass("meun_title_color"); 132 iconFirst.removeClass("menu_depth_1_icon_selected").addClass("menu_depth_1_icon"); 133 iconOther.removeClass("menu_depth_other_icon_selected").addClass("menu_depth_other_icon"); 134 } 135 else { 136 navCurr.attr("isShow", "true").css("display", "block"); 137 138 currTitle.addClass("meun_title_color"); 139 iconFirst.removeClass("menu_depth_1_icon").addClass("menu_depth_1_icon_selected"); 140 iconOther.removeClass("menu_depth_other_icon").addClass("menu_depth_other_icon_selected"); 141 } 142 }, 143 144 //說明: 145 // 面包削事件句柄 146 _handleBreadCutClick: function (params) { 147 var breadCutId = params.breadCutId; 148 var navId = breadCutId.substr(9, breadCutId.length - 9); 149 $("#" + navId).click(); 150 }, 151 152 /*****************(事件句柄 end)*****************/ 153 154 //說明: 155 // 驗證面包削 156 validateBreadCut: function (options) { 157 if (!options.boolBreadCut) { 158 return false; 159 } 160 var breadCutObj = $("#" + options.breadCutId); //面包削區域 161 if (options.breadCutId == "" || breadCutObj.length < 1) { 162 return false; 163 } 164 return true; 165 }, 166 167 //說明: 168 // 創建面包削 169 createBreadCut: function (params) { 170 var item = params.item; 171 var itemId = item.id; 172 var options = params.options; 173 var optionData = options.data; 174 175 if (!menu.prototype.validateBreadCut(options)) { 176 return; 177 } 178 179 var depth = parseInt($("#" + itemId).attr("depth")); 180 var separator = "<div class='meun_breadCut_separator'> > </div>";//分隔符 181 var breadCutHtml = ""; 182 breadCutHtml += "<div class='meun_breadCut'>"; 183 184 var itemNode = menu.prototype.getNodeById(itemId, optionData); 185 186 var breadCutNodeList = menu.prototype.getBreadCutNodeList(itemNode, optionData, []); 187 188 for (var i = 1; i <= depth; i++) { 189 breadCutHtml += "<div id='breadCut_" + breadCutNodeList[depth - i].recordId + "' class='meun_breadCut_name'>"; 190 breadCutHtml += breadCutNodeList[depth - i].title; 191 breadCutHtml += "</div>"; 192 if (i != depth) { 193 breadCutHtml += separator; 194 } 195 } 196 breadCutHtml += "</div>"; 197 198 $("#" + options.breadCutId).html(breadCutHtml); 199 200 //注冊事件 201 menu.prototype._registeBreadCutClick(); 202 203 }, 204 205 //說明: 206 // 獲取面包削列表 207 // item:當前點擊的導航 208 // optionsData:數據源 209 // breadCutNameList:返回列表 210 getBreadCutNodeList: function (item, optionData, breadCutNameList) { 211 if (item != null && item.parentId >= 0) { 212 var node = menu.prototype.getNodeById(item.recordId, optionData); 213 breadCutNameList.push(node);//獲得列表 214 215 item = menu.prototype.getNodeById(item.parentId, optionData); 216 menu.prototype.getBreadCutNodeList(item, optionData, breadCutNameList); 217 } 218 return breadCutNameList; 219 }, 220 221 //說明: 222 // 根據ID獲取節點 223 // id:節點ID 224 // optionsData:數據源 225 getNodeById: function (id, optionsData) { 226 if (id < 1) { 227 return null; 228 } 229 $.each(optionsData, function (i, v) { 230 if (v.recordId == id) { 231 nodeTS = v; 232 return false; 233 } 234 if (v.children.length > 0) { 235 menu.prototype.getNodeById(id, v.children); 236 } 237 }); 238 return typeof (nodeTS) !== "undefined" ? nodeTS : null; 239 } 240 };
第四部分:這部分很簡單,就是對外封裝,一句話而已。
1 $.menu = new menu();
第五部分:這部分當然是調用啦^_^,具體的參數說明在定義默認參數的時候都用注釋,這里就不再累述。
1 $.menu.init({ 2 id: "leftMenu", 3 data: data,//注意格式 4 navClickCallback: function () { 5 }, 6 boolBreadCut: true, 7 breadCutId: "mbx" 8 });
第六部分:樣式表,本次樣式比較簡單、少,所以可以貼出來。
1 .menu_depth_1 { 2 width: 200px;cursor: pointer;height: 33px;line-height: 33px; 3 margin-top: 2px;background-image: url("../Images/MenuImg/bg.png"); 4 } 5 .menu_depth_1_icon { 6 width: 11px;height: 11px;background-image: url("../Images/MenuImg/right-depth1.png"); 7 margin-top: 10px;margin-right: 3px;float: left; 8 } 9 .menu_depth_1_icon_selected { 10 width: 11px;height: 11px;background-image: url("../Images/MenuImg/down-depth1.png"); 11 margin-top: 10px;margin-right: 3px;float: left; 12 } 13 .menu_depth_other { 14 width: 200px;cursor: pointer;background-color: #FFFFFF;height: 33px;line-height: 33px;display: none; 15 border-bottom: 1px dashed #E0E0E0; 16 } 17 .menu_depth_other_icon { 18 width: 7px;height: 7px;background-image: url("../Images/MenuImg/right-depth2-1.png"); 19 margin-top: 13px;margin-right: 3px;float: left; 20 } 21 .menu_depth_other_icon_selected { 22 width: 7px;height: 7px;background-image: url("../Images/MenuImg/right-depth2-2.png"); 23 margin-top: 13px;margin-right: 3px;float: left; 24 } 25 .meun_title { 26 width: auto;height: 100%;float: left; 27 } 28 .meun_title_color { 29 color: #FF6600; 30 } 31 .meun_navArea { 32 width: 100%;height: auto; 33 } 34 35 /*面包削*/ 36 .meun_breadCut { 37 width: 100%;height: 30px;line-height: 30px; 38 } 39 .meun_breadCut_name { 40 width: auto;height: 30px;line-height: 30px;cursor: pointer;float: left; 41 } 42 .meun_breadCut_separator { 43 width: auto;height: 30px;line-height: 30px;margin: 0px 5px;float: left; 44 }
第七部分:當然是最后測試了啊,測試很重要,要相信好的代碼是測出來的。哈哈。。。
第八部分:效果圖
本來是沒有添加這部分內容的,原因是當時我沒有好的圖片做效果(本人隨熟練布局,但是不會ps。。。),今天我請我公司的UI設計師給我簡單的畫了幾張圖,在此也表示很感謝我的那位同事。下面是我截的效果圖,效果圖上也有些說明。可能不是很好看,但是功能杠杠的,哈哈^_^
總結:其實本次開發比較急,按照我上兩篇文章,其實我應該在添加一個主題部分的,當然這里就是為什么說我要大家學習div+css的原因了,如果你會布局,你可以做出各種你喜歡的主題風格。這次我偷懶了,沒有加上,后期我會補上。文章比較長,很多知識點我也沒有寫詳細。如果有需要源碼的或者想共同探討的同仁,隨時聯系我,QQ:296319075 ,注明園友就好,同時也希望大家也能提出寶貴意見,不吝賜教。秉承共同探討、共同進步!如有轉載,請注明出處,謝謝!^_^