【引言】距离上一回写博客已经有一些时日了,之前的爱莲iLinkIT系列主要是讲解了如何用NodeJS来实现一个简单的“文件传送”软件,属于JavaScript中在服务器端的应用。
今天,我们就回归到JavaScript的主战场 ---- 前端设计,一起来聊聊如何开发一个“自定义”的Web控件(基于jQuery),属于基础级别,高手请轻拍,还望不吝赐教,先谢过。
我们先假设这样一个场景,BOSS下达了命令,要求开发一款“幼儿早教”的应用,方便小朋友进行选择操作,而你则负责设计一个“下拉框”的Web控件,方便团队中其他弟兄使用,设计师的MM已经把效果图设计好了,如下:
总的来说,这是一个类似“下拉框”的Web控件:
a. 用户单击控件之后,显示下拉列表。
b. 选中下拉列表中的某项(例如:桃子),会将选中项的图片显示出来。
接下来,就让我们一步一步来实现这样的功能,我们先对整个讲解过程做一个整体的规划,以便我们心中有谱:
1. 实现控件的外观部分:包括HTML代码的组织,CSS样式设计。
2. 实现控件的基本框架:用JavaScript实现控件的交互设计。
3. 优化控件的可扩展性:让控件可以接受JavaScript回调函数。
4. 优化控件的可用性:可以动态传入选项的数据,达到自定义选项列表的目的。
最后,我们来做一个简单的回顾和总结。单击此处下载演示包,解压缩后打开index.html页面体验。
实现控件的外观部分
我们先来分析一下整个控件的几个组成部分,然后组织我们的HTML代码,为了后面设计CSS样式表的方便,在组织HTML代码时,就应该遵照相关规范(例如:公司的样式表命名规范),完成class的名称设计。
控件的组成
控件的最外层是一个大的容器,包含控件的各个组成部分,其中的组件包括:
1. 结果显示框
可以将选中的选项显示在结果显示框中。
2. 下拉框按钮
控制下拉框的显示与隐藏,单击按钮,显示下拉列表,再次单击时,隐藏下拉框,不同的状态下,箭头的方向不一样。
3. 下拉框
下拉框中包含多个可供用户选择的“项”。
4. 下拉框中的项
选项包含图片和文字两个部分,当前被选中的选项,应该呈现不同的外观。
HTML代码
根据前面的组成分析,我们可以实现如下的HTML代码(index.html):
1 <div class="zl_combobox"> 2 <div class="cb_input"><img src="images/f_apple.png" /><span>苹果</span></div> 3 <span class="cb_btn"></span> 4 <ul class="item_list"> 5 <li><img src="images/f_apple.png" /><span>苹果</span></li> 6 <li><img src="images/f_lemon.png" /><span>柠檬</span></li> 7 <li><img src="images/f_peach.png" /><span>桃子</span></li> 8 <li><img src="images/f_watermelon.png" /><span>西瓜</span></li> 9 </ul> 10 </div>
其中:
- zl_combobox:代表整个Web控件。
- cb_input:结果显示框。
- cb_btn: 下拉框按钮。
- item_list:下拉框。下拉框中的“项”用<li>元素来表示。
CSS样式表设计
接下来我们要根据控件最终的效果图和前面设计的HTML代码,来完成样式表的设计。在设计样式表时,一般分为两个部分:
- 基础外观:包括各个子部件的部件、关联关系、呈现外观等。
- 状态样式:要考虑到控件在交互过程中的状态变化,通过‘开关’来实现不同状态下的外观,方便后续的JavaScript的控制。
基础外观的相关CSS代码(zlbox.css)如下:
1 .zl_combobox{ 2 position:relative; 3 width:180px; 4 font-size:1.5em; 5 margin-left:5px; 6 color:#323232; 7 } 8 .zl_combobox .cb_input{ 9 width:170px; 10 height:60px; 11 padding:0px 5px; 12 line-height:60px; 13 border:1px #A9A9A9 solid; 14 background-color:#F2F2F2; 15 cursor:pointer; 16 } 17 .zl_combobox .cb_input:after, 18 .zl_combobox .item_list li:after{ 19 content:''; 20 clear:both; 21 display:table; 22 } 23 .zl_combobox .cb_input span, 24 .zl_combobox .item_list li span{ 25 display:block; 26 padding-left:10px; 27 height:60px; 28 line-height:60px; 29 } 30 .zl_combobox .cb_input img, 31 .zl_combobox .item_list li img{ 32 display:block; 33 width:50px; 34 height:50px; 35 } 36 .zl_combobox .cb_input>img, 37 .zl_combobox .cb_input>span, 38 .zl_combobox .item_list li>img, 39 .zl_combobox .item_list li>span 40 { 41 float:left; 42 } 43 .zl_combobox .cb_btn{ 44 position:absolute; 45 right:0px; 46 top:1px; 47 width:60px; 48 height:60px; 49 background:url('img/cb_btn_down.png') no-repeat center center; 50 cursor:pointer; 51 } 52 .zl_combobox .item_list{ 53 display:none; 54 position:absolute; 55 right:0px; 56 top:62px; 57 width:180px; 58 line-height:60px; 59 background-color:#FEFEFE; 60 z-index:999; 61 } 62 .zl_combobox .item_list li{ 63 padding-left:10px; 64 border-bottom:2px #CCCCCC dotted; 65 cursor:pointer; 66 }
因为本文重点在于介绍Web控件的设计过程,关于CSS设计的细节,就不展开细讲,如有疑问,咱们在评论中交流讨论。
那么,整个控件在交互过程中,会有哪些状态需要考虑呢?
1. 默认的状态:不显示下拉框,仅仅显示“结果显示框”和“下拉框按钮”。
2. 下拉框展开状态:显示下拉框,并且下拉框按钮的箭头变为向上。
3. 选项的鼠标悬停状态:当鼠标悬停到选项时,改变不同的样式。
(当然,如果你确定你的控件只是在手机/Pad上使用,可以不考虑鼠标悬停)。
4. 被选中的选项的状态:这个可以根据需要设计。
状态相关的样式的CSS代码(zlbox.css)如下:
1 .zl_combobox.selected .item_list{ 2 display:block; 3 } 4 .zl_combobox.selected .cb_btn{ 5 background:url('img/cb_btn_up.png') no-repeat center center; 6 } 7 .zl_combobox .item_list li:hover{ 8 color:#5281F8; 9 }
注意到,我们通过判断控件主体.zl_combobox是否有selected的class来控制下拉框的显示状态。
至此,控件的外观部分设计已经完成,我们可以看到,Web控件的外观设计有以下几个特点:
a. 布局属性:Web控件可能在各种场合中使用,所以,我们一般不对控件主体的布局属性进行设置(举例中我们仅仅设置了margin-left:5px,约定控件在使用过程中,距离它前面的控件为5px)。
b. 基础属性:像字体大小、字体颜色这样基础属性,在控件主体中尽量设置一下,确保控件的风格可以得到保证。比如:常规情况下,背景色是白色的,而字体的颜色是黑色,这时候,黑色的字体在白色背景的下拉框中可以显示出来。假设你没有设置控件主体的字体颜色,而在某个使用环境中,背景变成了黑色,字体颜色设置为白色,由于样式具有继承的特性,这时候就会变成在白色下拉框中的字体颜色是白色的。所以,在控件主体中设置基础属性,可以有效避免外部环境的“入侵”,让控件的风格自成一体。
实现控件的基本框架
工程文件组织
尽管前面我们已经讲解了控件的外观设计,也写了相关的HTML代码和CSS代码,但其实有一个本来应该先做的事情没有完成,就是工程文件组织,如下所示:
控件主文件夹的说明如下:
- css:保存项目用到的CSS相关的文件。
- images:保存需要在项目中直接引用的图片资源。
- js:保存项目用到的JavaScript相关的文件。
- index.html:HTML主体文件。
CSS文件夹下的内容:
- img:保存在css样式表中要引用的背景图片资源。
- index.css:与工程相关的样式表,比如:页面布局等。
- reset.css:一个重置样式表,将HTML中元素的标准特性重置。
- zlbox.css:与控件相关的样式表。
JS文件夹下的内容:
- jquery.zlbox.js:与控件相关的JavaScript文件,因为我们用到了jQuery库,所以以jquery为前缀。
- jquery.1.7.1.js:jQuery库文件。
本来应该还有一个index.js,用于保存工程相关的js代码,考虑到我们的演示demo的内容比较简单,工程相关的js代码调用就直接放到index.html文件中。
关于工程文件的组织和命名,各个公司应该有各自的规范标准,实际使用过程中,请遵照公司的规范。本文的论述将按以上的组织来进行。
JavaScript代码设计
总算到了我们Web控件设计的核心环节,我们的控件是基于jQuery库来设计的。jQuery插件的设计方法有很多种,我们依据我们的业务特点,选用了下面的基本框架模式,核心代码如下:
1 $.fn.czl_combobox = function( options ) 2 { 3 this.each( function() 4 { 5 var instance = $.data( this , 'czl_combobox' ); 6 if( !instance ) 7 { 8 $.data( this, 'czl_combobox' , new $.ZLComboBox( options , this ) ); 9 } 10 11 });//end of each 12 13 return this; 14 }; 15 $.ZLComboBox = function( options , element ) 16 { 17 this.$el= $( element ); 18 this._init( options ); 19 }; 20 $.ZLComboBox.defaults = { 21 22 }; 23 $.ZLComboBox.prototype = { 24 _init : function( options ) { 25 //初始化函数 26 }, 27 _loadEvents:function( ){ 28 //控件相关的事件注册 29 } 30 };
我们先来看一下调用这个Web控件的代码,然后我们对照起来分析:
$('.zl_combobox').czl_combobox( {} );
让前端的HTML元素和后端的JavaScript代码关联起来,czl_combobox 这个函数是关键,所以,我们就从$.fn.czl_combobox这个函数入手,来看看它到底是如何关联的,先看代码:
1 $.fn.czl_combobox = function( options ) 2 { 3 this.each( function() 4 { 5 var instance = $.data( this , 'czl_combobox' ); 6 if( !instance ) 7 { 8 $.data( this, 'czl_combobox' , new $.ZLComboBox( options , this ) ); 9 } 10 11 });//end of each 12 13 return this; 14 };
1. czl_combobox是$.fn的成员,意味着凡是jQuery对象都可以调用它。
2. 通过$.data()获取jQuery对象(对应一个HTML元素)的名称为czl_combobox的数据对象。
如果不存在,那么,就通过$.data()新建一个ZLComboBox 的对象。
如果已存在,那么,就不重复创建对象。
3. 创建对象时,把当前jQuery对象和参数options传入。
经过分析,我们发现$.fn.czl_combobox也仅仅是桥梁,还不是控件的核心,真正的核心是ZLComboBox对象,这个对象可以通过对应的HTML元素访问到。
现在,我们依据控件的业务特征,先来预估一下ZLComboBox对象能做什么:
1. 既然将HTML元素对应的DOM对象作为参数传给ZLComboBox对象,那么,ZLComboBox对象就能对相关的HTML元素以及它的子元素进行操作。
2. 同时将参数options传递给ZLComboBox对象,意味着,可以根据业务需要对ZLComboBox对象进行一些”定制”操作,包括:属性和行为。
下面我们再来看一下ZLComboBox的核心代码:
1. 在它的构造函数中,将传入的HTML DOM对象保存到自己的一个属性成员中,并且将参数options传递给自己的一个初始化方法(_init())。
2. 在它的原型中,定义了_init() 和 _loadEvents()两个方法。
我们的目的是要让控件能够响应’单击’事件,并且对控件的各个子部件进行状态的改变,所以我们可以这样操作:
1. 在_init中,通过控件主体的 DOM对象,获得它的各个子部件的jQuery对象。
2. 在_loadEvents中,对子部件的jQuery对象绑定响应事件。
3. 增加一个属性selected_index,记录当前选中的项的序号。
4. 定义一个方法_show_itemlist,用来操作下拉框的显示和隐藏。
完整的JS代码(zlbox.js)如下,可对照注释进行理解:
1 (function($){ 2 $.fn.czl_combobox = function( options ) 3 { 4 this.each( function() 5 { 6 var instance = $.data( this , 'czl_combobox' ); 7 if( !instance ) 8 { 9 //主体功能通过一个对象实现 10 $.data( this, 'czl_combobox' , new $.ZLComboBox( options , this ) ); 11 } 12 });//end of each 13 14 return this; 15 }; 16 $.ZLComboBox = function( options , element ) 17 { 18 this.$el= $( element ); 19 this._init( options ); 20 }; 21 22 $.ZLComboBox.prototype = { 23 _init : function( options ) 24 { 25 //相关的控件 26 this.comboBox = this.$el ; 27 28 //相关的HTML控件 29 this.cb_btn = this.comboBox.children( '.cb_btn' ).eq(0); 30 this.cb_input = this.comboBox.children( '.cb_input' ).eq(0); 31 this.cb_item = this.comboBox.find( 'li' ); 32 33 //控件的状态,标记是否显示下拉列表 34 this.cb_showitem_status = false ; 35 36 //初始化默认的值,默认选中最后一个选项 37 this.selected_index = this.cb_item.length-1 ; 38 this.cb_input.html( this.cb_item.eq(this.selected_index).html() ); 39 40 //注册响应事件 41 this._loadEvents(); 42 }, 43 _show_itemlist:function(){ 44 if( this.cb_showitem_status === false ){ 45 this.comboBox.addClass( 'selected' ); 46 this.cb_showitem_status = true; 47 } 48 else{ 49 this.comboBox.removeClass( 'selected' ); 50 this.cb_showitem_status = false; 51 } 52 return ; 53 }, 54 _loadEvents:function(){ 55 var _self = this; 56 //1_单击下拉箭头 57 this.cb_btn.on( 'click' , function( event ){ 58 _self._show_itemlist( ); 59 return ; 60 }); 61 //2_单击编辑框,也同样进行下拉框的状态切换 62 this.cb_input.on( 'click' , function( event ){ 63 _self._show_itemlist( ); 64 return ; 65 }); 66 //3_单击选项 67 this.cb_item.on( 'click' , function( event ){ 68 var index = $(this).index(); 69 if( _self.selected_index !== index ){ 70 //设置选项的值 71 _self.cb_input.html( $(this).html() ); 72 73 //设置当前选中的项 74 _self.selected_index = index ; 75 76 //隐藏下拉框 77 _self._show_itemlist(); 78 } 79 return ; 80 }); 81 } 82 }; 83 }(jQuery));
插件验证:
在index.html中,增加对插件调用的代码:
<script type="text/javascript"> $('.zl_combobox').czl_combobox( {} ); </script>
打开index.html网页,我们发现自定义的控件已经可以响应用户的单击事件,效果如下:
优化控件的可扩展性
有了前面的基础,接下来的理解就会比较简单。前面我们在分析控件的调用过程中,有一个参数options传到ZLComboBox对象中,但是,在最后的_init和_loadEvents方法中都没有用到过,那么,这个options的意义在哪里呢?
我们还是从业务的需求出发,然后再来考虑我们如何实现:
- 如果将控件用在“幼儿教学”的应用中,用户选中一种水果之后,可以播放与这种水果相关的介绍视频。
- 如果将控件用在“卖水果”的应用中,用户选中一种水果,可以显示这种水果的价格、产地等信息。
这就意味着,在不同的应用场景中,用户做出“选择水果”的操作时,触发的后续动作是不一样的,我们的控件应该支持这种操作才对。
如果在初始化控件的时候,传入一个“回调函数”,当事件触发时,调用一下这个传入的回调函数,那不就达到我们的目的了吗?这样,不同的业务场景,我们只要传入不同的回调函数即可。
回调函数是JavaScript的最拿手的,现在,我们就来优化我们的控件,让它支持回调函数。
这里也先假设一个场景:
当用户选中一个选项之后,我们就将选项中水果的图片显示出来,这时候,调用这个控件的方式变成这样:
1 $('#fruit_box').czl_combobox( { 2 selectItemEvent:function( index ){ 3 var html_content = $( '.item_list li img' ).get( index ).outerHTML; 4 $('#result_box').html( html_content ); 5 } 6 } );
注意到两点:
1. #fruit_box 为控件. zl_combobox对应的id,在同一个应用中可能存在多个zl_combobox控件,当要传入回调函数时,一般就用id去引用对应的控件元素,因为相同的控件,在不同的应用场景,行为是不同的,传入的回调函数也应该不同,所以不建议通过$(‘. zl_combobox’)一次性对所有的控件进行初始化。
2.传入的回调函数的名称是selectItemEvent,带有一个index 的参数。
现在,我们来优化控件的核心对象ZLComboBox中的内容:
1. 新增一个options成员,用来保存传入的参数对象:
_init:function( options ){ //… this. options = options ; }
2. 在下拉列表框中的选项单击事件中,增加对回调函数的调用:
//…. //隐藏下拉框 _self._show_itemlist(); //调用回调函数 _self.options.selectItemEvent( index );
然后,我们再来验证一下效果,打开index.html,从下拉框中选中一个选项,效果如下:
至此,我们最初设定的目的是已经达成了。为了完整性,再补充一下控件“扩展性”的另外一个特征:默认值设定。
还是以之前的业务场景为例:如果用户没有传入回调函数,我们希望控件就把选中项的序号通过提示框显示出来。如果有传入会调用函数,就调用用户传入的回调函数。也就是说,给控件增加默认的行为,当调用时没有传入指定参数,就调用默认的值。
现在我们就来实现控件的默认值设定:
1. 给ZLComboBox对象增加defaults成员,代码如下:
1 //默认值设置 2 $.ZLComboBox.defaults = { 3 selectItemEvent:function( index ){ 4 alert( index ); 5 return ; 6 } 7 };
2. 在_init方法中,保存传入的options对象采用如下的方式:
this.options = $.extend( true , {} , $.ZLComboBox.defaults , options );
这样,如果options中没有指定相关的成员,就调用defaults中的成员。这是jQuery插件处理传入参数的一种方式,$.extend为jQuery库定义的方法。
优化控件的可用性
从理解语言特性来看,这部分内容与上一部分没有什么差异,我们只是从业务角度来看,对实现方式做一些区分。
回到我们控件的场景,本文实现的控件我们取名为ZLComboBox,ZL为前缀,显然,它的行为特征与标准的组合框控件是类似的,组合框控件最常见的使用方式就是在表单(Form)中,那么,我们的控件也应该支持在表单(Form)中使用。
如果ZLComboBox是作为表单的一个组件,那么,就需要在用户提交表单数据时,能够获取到用户到底选了哪个选项。从目前来看,似乎只能判断.cb_input容器中的内容,而这个内容是这个样子:
<img src="images/f_apple.png" /><span>苹果</span>
显然,用户处理起来非常不方便。
也许你已经想到了,可以通过传入回调函数的方式,当用户选中一个选项之后,就将这个序号值(index)更新到某个隐藏的<input>元素中,表单提交数据时,提交这个隐藏的<input>中的值即可,这当然是一种处理方式。
其实,我们可以给控件的核心对象ZLComboBox增加一个方法getSelectedIndex,用来获取当前用户选中的项的序号,代码如下:
1 _loadEvents:function(){ 2 //… 3 }, 4 getSelectedIndex:function(){ 5 return this.selected_index; 6 }
这时候,控件的调用方式变成:
1 $('#fruit_box').czl_combobox( { 2 selectItemEvent:function( index ){ 3 var html_content = $( '.item_list li img' ).get( index ).outerHTML; 4 $('#result_box').html( html_content ); 5 } 6 } ); 7 8 //在表单提交前的数据处理中,取得用户选中项的序号 9 var index = $('#fruit_box').data( ‘czl_combobox’ ). getSelectedIndex() ;
重点在第9行。
当然,你也许会觉得,这种方式还不如之前动态更新隐藏<input>的方式自然,这里的主要目的是给大家一个特性解释,具体情况当然依据业务需要来确定。
另外,也许你已经发现了,每次调用我们自定义的控件,在HTML代码中都要写入一大堆内容,能不能在使用时HTML代码中仅仅定义<div class=” zl_combobox”></div>?然后将选项通过参数options传入,在ZLComboBox的_init方法中动态生产子部件呢?答案是:当然可以,就留作练习吧。
总结
通过前面的内容介绍,我们大致理解了自定义控件的意义,以及开发一个Web自定义控件的大致过程,希望能给大家带来一些启发。当然,我们的举例,实现方式都仅仅是为了讲解整个过程,在实际的开发过程中,除了业务诉求之外,还需要考虑其他方面的要求,比如性能方面:将整个控件库中CSS样式用到的背景图片优化成"雪碧"图,减少加载时间。或者安全性的优化:将某些插件的框架由'伪类'(new)调整为'闭包'模式,增强控件的安全性。
完整的示例代码,请单击此处下载。
感谢诸位捧场^_^~~