你的位置:首页 > Java教程

[Java教程]地址选择控件开发


"你从哪里来?”

“你要到哪里去?"

这是保安小哥经常会问的具有哲理性的问题。在互联网的应用的开发中,也经常会用到有关地址的选择设置。不管是物流的应用,还是外卖的应用,都会要求用户设置用户所在的位置。如果让用户来输入完整的地址,一方面,输入比较慢,体验不好。另一方面,输入的地址不规范,例如:"浙江省温州市永嘉县",有些人会输入"浙江温州永嘉",有些人则会输入"浙江省永嘉县",这对服务端的数据处理、分析也带来不便。

如何开发一个体验良好的“地址选择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':['赤坎','霞山','坡头','麻章','遂溪','徐闻','廉江','雷州','吴川'],'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 >/</span><span>'+addr_obj.qu+'</span>' );          }
} //...... } }

小节:

本文所论述的是地址控件的业务原理,在真正的生产环境中使用时,也许还需要结合控件的使用场景,

公司的业务规范来进行优化,比如思考以下几个方面:

a. 安全性:我们当前的设计,是将'三级地址信息'直接放到JS脚本中,在实际的业务场景中,你或许需要将这个信息放到'云端',用到时才从'云端'动态加载。

这样做的另外一个好处就是,当信息发生变化时(例如:行政区域发生变更),只要更新'云端'的数据就可以了。

b. 移动化:我们当前的设计,是将'地址选择面板'进行'悬挂式'设计,即:显示'地址选择面板'时,并不会影响原来的布局流。这个在某些使用场景时可能会出现问题,例如:如果这个控件放置在页面的底部,就不能完整的显示整个'地址选择面板',这个在'移动化'应用设计中会遇到比较多,这时候,也许将它修改成'抽屉式'设计更合理一些。

'悬挂式'设计和'抽屉式'设计的示意图如下:

 

抽屉式'设计的样式如何写?就留作练习,让大家练练手吧。

c. 兼容性:在前端设计中,因为您可能会遇到各种浏览器,所以兼容性也是需要大家考量的问题,例如事件的注册、事件对象访问等,不过由于我们采用了jQuery库,很多兼容性问题jQuery库已经帮我们解决了。

不提供源代码的控件分析都是耍流氓,单击'这里',下载讲解的示例代码。

完整的全国'三级地址',小生正在整理中,诸位大哥如果想要一起研究,共同完善,请留下邮箱,待小生整理完毕之后,会即时向诸位汇报。

感谢诸位捧场。