先睹為快
閑話少說,我們先來看看今天我們研究的控件的最終效果圖(參照天貓的送貨地址設置的效果):

“地址選擇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':['赤坎','霞山','坡頭','麻章','遂溪','徐聞','廉江','雷州','吳川'],
'0_2_8':['茂南','茂港','電白','高州','化州','信宜'],
'0_2_9':['端州','鼎湖','廣寧','懷集','封開','德慶','高要','四會'],
'0_2_10':['惠城','惠陽','博羅','惠東','龍門'],
'0_2_11':['梅江','梅縣','大埔','豐順','五華','平遠','蕉嶺','興寧'],
'0_2_12':['城區','海豐','陸河','陸豐'],
'0_2_13':['源城','紫金','龍川','連平','和平','東源'],
'0_2_14':['江城','陽西','陽東','陽春'],
'0_2_15':['清城','佛岡','陽山','連山','連南','清新','英德','連州'],
'0_2_16':['東莞'],
'0_2_17':['中山'],
'0_2_18':['湘橋','潮安','饒平'],
'0_2_19':['榕城','揭東','揭西','惠來','普寧'],
'0_2_20':['雲城','新興','郁南','雲安','羅定'],
};
return that ;
})();
特殊情況處理:
我們注意到,像北京、上海、天津這些直轄市,雖然是省級單位,但是它的下面直接就是包含了各個區的"區級單位",為了保證'三級地址結構'的一致性,我們給它補上'市級單位',名稱也是北京市。但是,在用戶選擇的時候,如果還要按"北京 > 北京 > 海淀"這樣的方式,就顯得太麻煩了,所以,我們做如下的優化:
a. 當用戶選中一個'省級單位'時,如果它下面只有一個'市級單位',就直接跳過這個'市級單位'。
比如:用戶選中'北京'之后,就直接顯示'朝陽'、'海淀'等'區級單位'
b. 優化顯示結果:
如果發現用戶選中的'省級單位'與'市級單位'的名稱一樣,就只顯示省級單位。
例如:北京/石景山
優化的相關代碼如下:
$.createAddressBox = function( options , element ){
//......
//選擇序號為sheng_id的省份之后的響應事件
var _selectShengId = function( sheng_index ){
//......
//判斷是否是'北京'、'上海'、天津、重慶等直轄市
var shi_list_id = '0_'+sheng_index;
//這里處理需要跳過'市'這一級別的處理
if( addr_box_data[shi_list_id].length === 1 ){
//更新地址的值
addr_obj.sheng = sheng_name;
addr_obj.shi = '' + addr_box_data['0_'+sheng_index+'_0'] ;
addr_obj.qu = '' ;
addr_obj.sheng_id = ''+sheng_index;
addr_obj.shi_id = '0' ;
addr_obj.qu_id = '' ;
//更新地址欄中的值
_updateAddrValue( );
//觸發選中了區的選項卡
_selectShiId( 0 );
}
//......
}
//更新地址欄中的值
var _updateAddrValue = function(){
//......
if( addr_obj.shi !== '' && addr_obj.shi === addr_obj.sheng){
if( addr_obj.qu !== '' ){
ab_vauleinfo.append( '<span class="sep">/</span><span>'+addr_obj.qu+'</span>' );
}
}
//......
}
}
小節:
本文所論述的是地址控件的業務原理,在真正的生產環境中使用時,也許還需要結合控件的使用場景,
公司的業務規范來進行優化,比如思考以下幾個方面:
a. 安全性:我們當前的設計,是將'三級地址信息'直接放到JS腳本中,在實際的業務場景中,你或許需要將這個信息放到'雲端',用到時才從'雲端'動態加載。
這樣做的另外一個好處就是,當信息發生變化時(例如:行政區域發生變更),只要更新'雲端'的數據就可以了。
b. 移動化:我們當前的設計,是將'地址選擇面板'進行'懸掛式'設計,即:顯示'地址選擇面板'時,並不會影響原來的布局流。這個在某些使用場景時可能會出現問題,例如:如果這個控件放置在頁面的底部,就不能完整的顯示整個'地址選擇面板',這個在'移動化'應用設計中會遇到比較多,這時候,也許將它修改成'抽屜式'設計更合理一些。
'懸掛式'設計和'抽屜式'設計的示意圖如下:



抽屜式'設計的樣式如何寫?就留作練習,讓大家練練手吧。
c. 兼容性:在前端設計中,因為您可能會遇到各種瀏覽器,所以兼容性也是需要大家考量的問題,例如事件的注冊、事件對象訪問等,不過由於我們采用了jQuery庫,很多兼容性問題jQuery庫已經幫我們解決了。
不提供源代碼的控件分析都是耍流氓,單擊'這里',下載講解的示例代碼。
完整的全國'三級地址',小生正在整理中,諸位大哥如果想要一起研究,共同完善,請留下郵箱,待小生整理完畢之后,會即時向諸位匯報。
感謝諸位捧場。

