"你從哪裡來?” “你要到哪裡去?" 這是保全小哥經常會問的具有哲理性的問題。在互聯網的應用的開發中,也經常會用到有關地址的選擇設置。不管是物流的應用,還是外賣的應用,都會要求用戶設置用戶所在的位置。如果讓用戶來輸入完整的地址,一方面,輸入比較慢,體驗不好。另一方面,輸入的地址不規範,例如:"浙江省 ...
"你從哪裡來?”
“你要到哪裡去?"
這是保全小哥經常會問的具有哲理性的問題。在互聯網的應用的開發中,也經常會用到有關地址的選擇設置。不管是物流的應用,還是外賣的應用,都會要求用戶設置用戶所在的位置。如果讓用戶來輸入完整的地址,一方面,輸入比較慢,體驗不好。另一方面,輸入的地址不規範,例如:"浙江省溫州市永嘉縣",有些人會輸入"浙江溫州永嘉",有些人則會輸入"浙江省永嘉縣",這對服務端的數據處理、分析也帶來不便。
如何開發一個體驗良好的“地址選擇Web控制項”,如何優雅地回答保全小哥的哲理問題?估計是廣大Web應用開發人員一直在思考的問題。與其他一些標準Web控制項(例如:組合框、單選框等)不同,“地址選擇Web控制項”具有區域特征,功能也比較綜合,大部分標準的UI庫都不會提供,所以,搗鼓一下還是很有必要的。
先睹為快
閑話少說,我們先來看看今天我們研究的控制項的最終效果圖(參照天貓的送貨地址設置的效果):
“地址選擇Web控制項”的基本組成:
使用控制項舉例:
<!--需要載入和引用的文件--> <link rel="stylesheet" href="css/zlbox.css" type="text/css" media="screen" /> <script src="js/jquery-1.7.1.js"></script> <script src="js/jquery.zlbox.js"></script> <!--控制項的HTML代碼--> <div id="company_addr" class="zl_addressbox"> <div class="ab_showbar tip"> <div class="tip_info">請選擇省市區</div> <div class="value_info"> <!--結果顯示容器--> </div> </div> <span class="ab_btn"></span> <div class="selectaddr_box"> <div class="ab_bar"> <ul> <li class="sheng current">省份</li> <li class="shi">城市</li> <li class="qu">區縣</li> </ul> </div> <div class="ab_panel"> <dl> <dd class="sheng current"> <div class="ab_group"> <span class="ab_shengtitle">A-G</span> <ul class="ab_sheng ab_item"> <li>北京</li> <li>廣東</li> </ul> </div> <div class="ab_group"> <span class="ab_shengtitle">T-Z</span> <ul class="ab_sheng ab_item"> <li>浙江</li> </ul> </div> </dd> <dd class="shi"> <ul class="ab_shi ab_item"> <!--市區容器--> </ul> </dd> <dd class="qu"> <ul class="ab_qu ab_item"> <!--區縣容器--> </ul> </dd> </dl> </div> </div> </div> <!--End of ZLAddressBox--> <script type="text/javascript"> //初始化地址控制項 $('#company_addr').czl_addressbox({}); </script>
為了講解的方便,控制代碼的篇幅,這裡僅僅舉例了北京、廣東、浙江3個省市的“省份/城市/區縣”選擇,數據完整的“全國地址選擇Web控制項”還在搗鼓中,出貨後再向諸位彙報。
基本功能實現
【HTML代碼】
Web控制項涉及的HTML代碼如下:
<!--ZLAddressBox--> <div id="company_addr" class="zl_addressbox"> <div class="ab_showbar tip"> <div class="tip_info">請選擇省市區</div> <div class="value_info"> <!--結果顯示容器--> </div> </div> <span class="ab_btn"></span> <div class="selectaddr_box"> <div class="ab_bar"> <ul> <li class="sheng current">省份</li> <li class="shi">城市</li> <li class="qu">區縣</li> </ul> </div> <div class="ab_panel"> <dl> <dd class="sheng current"> <div class="ab_group"> <span class="ab_shengtitle">A-G</span> <ul class="ab_sheng ab_item"> <li>北京</li> <li>廣東</li> </ul> </div> <div class="ab_group"> <span class="ab_shengtitle">T-Z</span> <ul class="ab_sheng ab_item"> <li>浙江</li> </ul> </div> </dd> <dd class="shi"> <ul class="ab_shi ab_item"> <!--市區容器--> </ul> </dd> <dd class="qu"> <ul class="ab_qu ab_item"> <!--區縣容器--> </ul> </dd> </dl> </div> </div> </div> <!--End of ZLAddressBox-->
【CSS代碼】
Web控制項涉及到的CSS代碼(zlbox.css)如下:
1 /**全國地址選擇控制項**/ 2 .zl_addressbox{ 3 position:relative; 4 width:370px; 5 background-color:#F08; 6 } 7 .zl_addressbox .ab_showbar{ 8 width:100%; 9 height:30px; 10 line-height:30px; 11 border:#A9A9A9 1px solid; 12 background-color:#FFF; 13 } 14 .zl_addressbox .ab_btn{ 15 position:absolute; 16 right:0px; 17 top:1px; 18 width:30px; 19 height:30px; 20 background:#FFF url('img/cb_btn_down.png') no-repeat center center; 21 } 22 /**地址選擇面板**/ 23 .zl_addressbox .selectaddr_box{ 24 display:none; 25 position:absolute; 26 left:0px; 27 right:0px; 28 top:32px; 29 width:100%; 30 line-height:30px; 31 background-color:#FFF; 32 z-index:888; 33 } 34 .zl_addressbox .ab_bar{ 35 width:100%; 36 height:40px; 37 } 38 .zl_addressbox .ab_bar:after{ 39 clear:both; 40 content:''; 41 display:table; 42 } 43 .zl_addressbox .ab_bar li{ 44 float:left; 45 width:122px; /**這裡如果要調整容器大小,需要同步調整**/ 46 height:40px; 47 line-height:40px; 48 text-align:center; 49 background-color:#F0F0F0; 50 border-left:#CCC 1px solid; 51 border-bottom:transparent 1px solid ; 52 cursor:pointer; 53 } 54 .zl_addressbox .ab_bar li:first-child{ 55 border-left:none; 56 } 57 .zl_addressbox .ab_bar li.current{ 58 color:#009AFD; 59 background-color:#FFF; 60 cursor:none; 61 } 62 .zl_addressbox .ab_panel{ 63 position:relative; 64 width:100%; 65 } 66 .zl_addressbox .ab_panel dd{ 67 display:none; 68 position:relative; 69 width:100%; 70 } 71 .zl_addressbox .ab_panel dd.current{ 72 display:block; 73 } 74 /**省份面板中的分組**/ 75 .zl_addressbox .ab_panel dd .ab_group{ 76 position:relative; 77 width:100%; 78 margin-bottom:20px; 79 } 80 .zl_addressbox .ab_panel dd .ab_group span.ab_shengtitle{ 81 display:block; 82 position:absolute; 83 top:0px; 84 width:60px; 85 height:30px; 86 line-height:30px; 87 text-align:center; 88 font-size:0.8em; 89 color:#009AFD; 90 } 91 .zl_addressbox .ab_panel dd .ab_group .ab_sheng{ 92 margin-left:40px; 93 } 94 .zl_addressbox .ab_panel dd ul.ab_item{ 95 margin-top:10px; 96 margin-bottom:5px; 97 } 98 .zl_addressbox .ab_panel dd ul.ab_item:after{ 99 clear:both; 100 display:table; 101 content:''; 102 } 103 .zl_addressbox .ab_panel dd ul.ab_item li{ 104 float:left; 105 height:30px; 106 line-height:30px; 107 padding:0px 10px; 108 margin-left:10px; 109 cursor:pointer; 110 } 111 .zl_addressbox .ab_panel dd ul.ab_item li:hover{ 112 color:#009AFD; 113 } 114 .zl_addressbox .ab_panel dd ul.ab_item li.current{ 115 border-radius:6px; 116 color:#FFF; 117 background-color:#009AFD; 118 } 119 .zl_addressbox.selected .selectaddr_box{ 120 display:block; 121 border-left:1px #CCCCCC solid; 122 border-right:1px #CCCCCC solid; 123 border-bottom:1px #CCCCCC solid; 124 padding-bottom:10px; 125 } 126 .zl_addressbox.selected .ab_btn{ 127 background:#FFF url('img/cb_btn_up.png') no-repeat center center; 128 } 129 /**提示狀態**/ 130 .zl_addressbox .ab_showbar .tip_info{ 131 display:none; 132 width:90%; 133 padding-left:5px; 134 color:#CCCCCC; 135 } 136 .zl_addressbox .ab_showbar .value_info{ 137 display:block; 138 width:90%; 139 padding-left:5px; 140 } 141 .zl_addressbox .ab_showbar .value_info span.sep{ 142 color:#CCC; 143 font-size:0.8em; 144 } 145 146 .zl_addressbox .ab_showbar.tip .value_info{ 147 display:none; 148 } 149 .zl_addressbox .ab_showbar.tip .tip_info{ 150 display:block; 151 }
結合我們的功能訴求,一級前面介紹的控制項的組成,將HTML代碼與CSS代碼對照起來理解應該比較好理解,所以,關於控制項涉及到的佈局、樣式等細節,就不展開贅述,我們把重點放到JavaScript的代碼部分。開發Web控制項的基本套路與前一篇博文:ZLComboBox自定義控制項開發詳解 類似,本文就著重講解控制項的業務需求以及與ZLComboBox控制項開發不一樣的地方。
JavaScript--閉包實現
與ZLComboBox控制項不同,本文我們採用閉包的方式來構建我們的控制項對象,jQuery插件的基本代碼如下:
$.fn.czl_addressbox = function( options ) { this.each( function() { var instance = $.data( this , 'czl_addressbox' ); if( !instance ) { $.data( this, 'czl_addressbox' , $.createAddressBox( options , this ) ); });//end of each return this; //支持鏈式操作 };
控制項對象通過createAddressBox()來返回一個控制項對象,下麵我們重點來分析一下createAddressBox()的基本組成。
整個控制項對象基於地址的'三級'目錄樹,以id來進行各級行政單位的標識與處理。
整個控制項對外的介面:
1> 創建控制項時,可以傳入一個對象字面量(options),用於設置預設選中的地址。
2> 返回當前選中地址的對象字面量:get_addr_obj()。
3> 傳入一個地址對象字面量:set_addr_obj( new_addr_obj )。
業務流程分析:
1> 預設情況下,控制項顯示'請選擇省市區'的提示信息。
2> 單擊控制項'地址欄',顯示下拉的'地址選擇面板';再次單擊'地址欄',隱藏下拉的'地址選擇面板';下拉的'地址選擇面板'反映當前的選擇狀態。
3> 在'省份'面板中選中一個'省級單位'後,自動顯示選中省級單位下的'市級'行政區。
在'城市'面板中選中一個'市級單位'後,自動顯示選中市級單位下的'區級'行政區。
在'區縣'面板中選中一個'區級單位'後,自動隱藏'地址選擇面板'。
所有的選擇結果,都會實時在控制項'地址欄'中顯示出來。
4> 只有選中了'省級'單位之後,才能單擊'市級'單位的頁簽,
只有選中了'市級'單位之後,才能單擊'區級'單位的頁簽。
5> 當更改了某一級別的選擇結果之後,將它的下一級單位的選擇清空。
例如:已經選擇了"廣東/深圳",這時候單擊"省份"面板中的"浙江",則原來"城市面板"中的內容被清空,重新設置為"浙江"所包含的城市。
綜合業務需求,閉包中的成員可以包括:
私有屬性
- 保存了'三級地址信息'的對象字面量:addr_box_data
- 用於記錄選擇結果的對象字面量:addr_obj
- 一些HTML元素的引用等:例如;地址欄元素ab_showbar
私有函數
- 選中某個'省級單位','市級單位'和'區級單位'之後的響應函數。
_selectShengId( sheng_index )
_selectShiId( shi_index )
_selectQuId( qu_index )
- 更新地址欄信息的函數:_updateAddrValue( )
- 創建控制項時,要執行的初始化函數:_init( options )
- 相關控制項的註冊函數:_loadEvents( ),這個函數應該在_init()中被調用。
因為整個架構都是基於id來創建,而從界面呈現來看,我們更習慣於'地址名稱',
例如:"浙江省"的id是多少?不去翻閱addr_box_data,我們是回答不上來的。
所以,增加一個輔助函數:getShengIDFromName( sheng_name )
控制項對外提供的介面函數
- 獲得當前選中的地址信息的對象字面量:get_addr_obj()
- 設置預設選中的地址信息:set_addr_obj:function( a_obj )
控制項提供的HTML介面
每次更新addr_obj的時候,同步將地址的信息更新到 HTML 元素的data屬性中,名稱為:addr_value。
例如:如果依次選中了:'浙江>杭州>濱江',那麼,addr_value的值為:'浙江_杭州_濱江'。
這種方式的好處是:在應用中直接用$('.zl_addressbox').czl_addressbox({}); 就把所有的addressbox控制項都初始化了。後續要取值時,直接從對應元素的data屬性中獲取即可,而不用去分析如何調用控制項對象。
綜合以上分析,createAddressBox()代碼的基本框架如下:
$.createAddressBox = function( options , element ){ //私有成員聲明 var addr_box = $( element ); //相關的HTML組件 //...... //省份的選擇控制項 var ab_sheng_item = addr_box.find( '.ab_sheng>li' ); //保存了'三級地址信息'的對象字面量 var addr_box_data = {} ; //用於記錄選擇結果的對象字面量 var addr_obj = {}; //私有函數聲明 var _init = function( options ){ //載入事件註冊 _loadEvents();
addr_box.data( 'addr_value' , '' );
//... }; //依據a_obj的值初始化控制項的狀態 var _init_addr_obj = function( a_obj ){ //...... } //更新地址欄中的值 var _updateAddrValue = function(){ //...... }; //選擇序號為sheng_id的省份之後的響應事件 var _selectShengId = function( sheng_index ){ //...... }; //選擇序號為shi_index的城市之後的響應事件 var _selectShiId = function( shi_index ){ //...... }; //選擇序號為qu_index的區縣之後的響應事件 var _selectQuId = function( qu_index ){ //...... }; //根據省份的名稱,選擇對應的id var getShengIDFromName = function( sheng_name ){ //...... } //註冊事件 var _loadEvents = function(){ //...... }; //執行初始化函數 _init( options ); //創建對象 var that = { get_addr_obj:function(){ return addr_obj; }, set_addr_obj:function( a_obj ){ _init_addr_obj( a_obj ); return ; } }; //返回對象 return that ; }
JavaScript--事件冒泡
重點看一下'市級'面板和'區級'面板相關選項的單擊事件處理。根據前面的分析,我們發現這兩個面板的選項,是動態變化的,那我們應該如何處理呢?
這讓我們想到了事件的'冒泡'機制,只要註冊相關容器的單擊事件,當選中某個選項時,自然會冒泡到'容器'的處理函數那裡。不難,直接上代碼:
//註冊事件 var _loadEvents = function(){ //...... //單擊'市'下麵的選項的響應事件 ab_shi_databox.on( 'click' , function( event ){ //阻止事件冒泡 event.stopPropagation(); var $target = $( event.target ); if( $target.attr( 'tagName' ) === 'li' ){ var shi_item = $target ; //如果當前的'市'選項就是選中的選項,則不響應事件,直接返回 if( shi_item.hasClass('current') ){ return ; } //獲得省份對應的id的值 var shi_id = shi_item.index(); //響應選中城市之後的響應函數 _selectShiId( shi_id ); //標記當前選中的"城市" shi_item.addClass('current').siblings().removeClass( 'current' ); }//如果是子元素li上觸發的事件 return ; }); //單擊'區'下麵的選項的響應事件 ab_qu_databox.on( 'click' , function( event ){ //阻止事件冒泡 event.stopPropagation(); var $target = $( event.target ); if( $target.attr( 'tagName' ) === 'li' ){ var qu_item = $target ; if( qu_item.hasClass('current') ){ return ; } //獲得省份對應的id的值 var qu_id = qu_item.index(); //響應選中城市之後的響應函數 _selectQuId( qu_id ); qu_item.addClass('current').siblings().removeClass( 'current' ); return ; } }); };
優化控制項:
到目前為止,我們的控制項已經能夠工作了,但是,還有一些可以優化地方,我們一起來分析一下。
JavaScript--單例模式
每次我們使用控制項的時候,都要執行createAddressBox()創建一個對象,根據閉包的特性,每次創建時,閉包中的私有屬性成員都會被單獨創建,作為一個完整的工作區占據記憶體空間。 我們註意到包含"三級地址信息"的addr_box_data也是一個'私有屬性',而這個對象字面量所占的記憶體空間非常大,如果每次調用createAddressBox()都占用一段大記憶體空間,從性能上來看,顯然不是一個好的設計。
因為addr_box_data的內容是固定的,所以,可以把"三級地址信息"保存到一個單獨的全局空間中,
而僅僅在createAddressBox()中引用這個全局空間即可。
優化方式如下:
a. 創建一個全局變數:$.zl_addr_box_data。
b. 在createAddressBox中引用這個全局變數。
相關的代碼示例如下:
$.createAddressBox = function( options , element ){ //...... var addr_box_data = $.zl_addr_box_data; //引用的全局變數 //...... } //保留有全國地址信息的全局變數 $.zl_addr_box_data = (function( ){ var that = { '0':['北京','浙江','廣東'], '0_0':['北京'], '0_0_0':['東城','西城','崇文','宣武','朝陽','丰台','石景山','海澱','門頭溝','房山','通州','順義','昌平','大興','懷柔','平谷','密雲','延慶'], '0_1':['杭州','寧波','溫州','嘉興','湖州','紹興','金華','衢州','舟山','台州','麗水'], '0_1_0':['上城','下城','江干','拱墅','西湖','濱江','蕭山','餘杭','桐廬','淳安','建德','富陽','臨安'], '0_1_1':['海曙','江東','江北','北侖','鎮海','鄞州','象山','寧海','餘姚','慈溪','奉化'], '0_1_2':['鹿城','龍灣','甌海','洞頭','永嘉','平陽','蒼南','文成','泰順','瑞安','樂清'], '0_1_3':['秀城','嘉善','海鹽','海寧','平湖','桐鄉'], '0_1_4':['吳興','南潯','德清','長興','安吉'], '0_1_5':['越城','紹興','新昌','諸暨','上虞','嵊州'], '0_1_6':['婺城','金東','武義','浦江','磐安','蘭溪','義烏','東陽','永康'], '0_1_7':['柯城','衢江','常山','開化','龍游','江山'], '0_1_8':['定海','普陀','岱山','嵊泗'], '0_1_9':['椒江','黃岩','路橋','玉環','三門','天台','仙居','溫嶺','臨海'], '0_1_10':['蓮都','青田','縉雲','遂昌','松陽','雲和','慶元','景寧','龍泉'], '0_2':['廣州','韶關','深圳','珠海','汕頭','佛山','江門','湛江','茂名','肇慶','惠州','梅州','汕尾','河源','陽江','清遠','東莞','中山','潮州','揭陽','雲浮'], '0_2_0':['荔灣','越秀','海珠','天河','白雲','黃埔','番禺','花都','南沙','蘿崗','增城','從化'], '0_2_1':['武江','湞江','曲江','始興','仁化','翁源','乳源','新豐','樂昌','南雄'], '0_2_2':['羅湖','福田','南山','寶安','龍崗','鹽田','光明新區','坪山新區','龍華新區','大鵬新區'], '0_2_3':['香洲','斗門','金灣'], '0_2_4':['龍湖','金平','濠江','潮陽','潮南','澄海','南澳'], '0_2_5':['禪城','南海','順德','三水','高明'], '0_2_6':['蓬江','江海','新會','台山','開平','鶴山','恩平'], '0_2_7':['赤坎','霞山','坡頭','麻章','遂溪','徐聞','廉江','雷州','吳