编写更加通用的 JavaScript

红薯 发布于 2011/02/10 09:03
阅读 749
收藏 12

当你准备好抽象出一个 Web 组件的时候, JavaScript 能帮你做到,融入组件的 JavaScript 变得优雅利索;此时它像一把瑞士军刀,轻松帮你应对各种场景。但当你急于交付时,JavaScript 也能帮你做到,它会散落在页面各处,代码重复而臃肿,维护起来像噩梦;短快之后是长痛。 作者以网页中常见的下拉列表的实现为例,介绍了一种让 JavaScript 变得更加通用的方法:编写 JavaScript 组件。

必要性分析

找一个开源 JS 库像 jQuery, 是一个提高工作效率的好办法,它能帮你以简洁的代码快速操作或遍历 DOM 集,而且不必担心跨浏览器兼容问题;但 jQuery 不是万能钥匙,如果你要完成某些公共的功能(如组件,公共 JS 模块),需要自己写插件,你也可以偷懒,从网上找到开源的 jQuery 插件,获得对源码的修改许可后还可以根据实际使用场景进行修改或改进。但使用或修改别人的插件,这中间的 bug 风险和维护成本首先得考虑进去。那自己写一个吧。如果你写的是 jQuery 插件,那么你未来的插件必须与大小至少是 150k 的 jQuery 库(如:v1.4.2 未压缩开发版)产生依赖;大概评估一下,如果你的插件里有约 30% 的代码能通过 jQuery 帮你精简下来,并且写 JS 兼容性代码让你感到有压力,那么即使有依赖也是值得的,否则我宁愿自己写一个独立的 JavaScript 组件。

虽然暂时会辛苦一点,但从它以后给你带来的回报来看,你的辛苦是很值得的:在以后的项目中你无需再为依赖版本,代码冲突发愁,因为你的 JavaScript 组件更加迷你灵活。

应用场景举例

从下拉列表中选单位,允许输入,同时根据输入的内容过滤下拉列表中的内容。我们也可以称之为自动完成功能 (Auto Complete)。如图 1 输入“单位”时,下拉列表中出现与单位相关的条目供输入时选择。

图 1. 下拉列表示例
下拉列表示例

实现步骤

确定设计目标

为输入框(包括 Input,TextArea)提供下拉框选择,辅助输入,提高输入准确性和效率

需求明细整理

  1. 对下拉列表中匹配关键字的部分亮显 (如图中“单位”)
  2. 鼠标滑入下拉项时改变背景色为焦点色,滑出时恢复显示先前背景色
  3. 单击某一项时,将该项的值设为输入框的值
  4. 点击下拉框和输入框之外的位置时,收起下拉框
  5. 点击输入框时,如果已收起,则显示;如果已显示,则收起
  6. 键盘方向键控制下拉项当前位置,默认没有选中项,向上时定位到最后一项,向下时定位到第一项;当按下向右,向下键时当定位到下一项;当按下向左,向上键时定位到上一项;到在第一项上按上移键时,定位到最后一项;当在最后一项上按下移键时,定位到第一项
  7. 按下回车时键时,将选中项的值设为输入框的值
  8. 允许输入在下拉框中不存在的值
  9. 允许同一页面出现多个下拉框实例
  10. 允许连续多次选择(如 email 收件人地址,输入每个收件人时都有自动提示)

主要细节分析

  • 组件组成部分

    输入框(Input,TextArea),下拉选择框(Div),数据加载时的动画图标(GIF)。

    输入框在构建组件时,通过构造参数传入,下拉选择框 DIV 动态生成,由事件控制隐藏和显示,我们能想到的事件如输入控件 click 事件,输入框中输入 BackSpace 导致内容为空或达到最低触发长度等。

    • 输入框

      提供用户输入的地方,输入包括:字符输入,方向控制和选择控制(如回车和退出),输入框中的值在用户切换并选中条目时发生变化。

    • 下拉选择框

      下拉框初次构建好后,以后在事件中控制隐藏和显示 下拉框出现的位置,高和宽:位置为绝对位置并与输入框对齐;宽等于输入框的宽,高根据参数确定 下拉框中的内容由事件触发装载,装载数据源类型分远程和本地,事件类型由参数指定,如:click(普通下拉), keyup(autocomplete)等;为每类事件定制特定的参数,如 keyup,为避免加载频繁,可以设定 keyup 事件触发后延迟 300ms 执行数据装载;click,为避免每次 click 事件触发数据装载,设定只装载一次(初始化时)的参数。

    • 数据加载动画

      当加载远程数据时,在输入框右侧显示 Ajax 加载动画(GIF 图片)。

  • 事件设计

    • 内部事件(private)

      输入框:按键:Up(Left),Down(Right):控制条目聚焦,Enter 键选中,Escape 键退出,并清空选择下拉条目:在每次装载完下拉数据时设置下拉框中每个条目的事件。如:条目的焦点移动,鼠标移入,移出,点击。

    • 对外事件(public)

      用户可以实现的对外事件有:beforeDropdown:(可选)下拉前开始装载数据时触发,afterDropdown:(可 选)下拉完数据装载完毕时触发,onSelect:(必须)选中时触发,默认实现会提示用户需要实现此事件。onEmpty:(可选)输入为空时触发。 onNewInput:(可选)输入的值在下拉框条目中不存在时触发。getFilter:加载下拉内容时的过滤条件的值;默认实现:返回输入框的值,此 设计满足了特殊输入的要求,如连续多输入的情况(多个 Email 收件人),这种情况下,用户需要重写 getFilter 返回最后的分隔字符串作为下拉过滤条件值,同时用户在 onSelect 的实现里需要将选中的值作为当前分隔段的值。

      对外事件,以 配置项的方式提供。

  • 配置项设计

    • 主要配置项



      配置项名称 配置项含义 用法 作用
      className 风格类名 样式名将作用到下拉框最外层对象,样式由用户在使用组件前定义好 让你的下拉框根据样式名融入不同的网页风格
      dataFields 描述数据源的元数据: 列名,数据类型,如字符串,数值等 用 JSON 对象数组格式描述: [{'name':'xx', 'type':'tpXX'},{'name':'xx', 'type':'tpXX'}] 决定用户能操作哪些数据,如作为值,标题或其它; dataFields为 数据提供者提供返回数据的规范
      dataKeyField 决定 dataFields中哪一列作为值列 用 JSON 对象格式描述:{'name' : 'id', 'type' : 'int'} 由用户灵活指定值列
      dropdownTrigger 指定哪些事件能触发下拉下拉列表展开并从数据源装载内容 用 JSON 对象数组格式描述:[{eventName:'keyup', cacheable:true, triggerWhenEmpty: false, triggerMinLength:1}, {eventName:'click', loadAllForOnce:true}] 能适用不同的使用场景: 既可以作为纯下拉列表也可以用作自动填充 (AutoComplete)
      beforeDropdown 供用户实现的事件: 下拉并装载数据前触发 用户可选择实现 如下拉前对输入内容进行检查,修改或替换
      afterDropdown 供用户实现的事件: 下拉并装载数据后触发 用户可选择实现 如对选中的值进行检查,修改或替换
      onSelect 供用户实现的事件: 选中(点击下拉项目,或在选中的项上回车确认) 用户必须实现 为输入框赋值
      onNewInput 供用户实现的事件: 当输入的值在下拉中不存在时触发 用户可选择实现 用户不需要知道输入的值是否已经存在时不需要实现此事件
      onEmpty 供用户实现的事件: 当输入的值为空时触发 用户可选择实现 如:输入为空时需要给用户提醒
      loadingImgPath 远程数据加载时,提供动画的名称(含路径) 指定一个 GIF 图片名称字符串 灵活指定加载动画
      height 下拉框最高值(pix) 指定高度值 规定下拉框最大高度
      footHtml 指定下拉框页脚 html 标记字符串 下拉框能适用更多的场合
      footHeight 页脚高度 html 标记字符串 限定页脚高度
      colorOfhighlightWords 与输入框内容匹配的高亮字符串颜色 颜色值字符串,如黑色 : 'black' 动态指定高亮字串的颜色,适用不同的要求
      bgColorOfItem 指定下拉列表中交错显示不同的背景颜色 JSON 数组:['#ffffff','#F3F2FF', '#6666FF'],第一位:奇数行,第二位:偶数行,第三位:焦点行 能为不同的应用场合指定配色, 此项不受 className 对应的子样式影响

代码清单

  • 主代码
    • 公共调用部分

      这部分代码能从主代码中抽出来作为独立的文件,如 utils.js。 dropDownList.js 中用到这里面的 ajaxGet, positionOf 等公共方法。 此处代码比较简单,只提供方法接口,不一一提供实现,仅说明主代码中要用到的常用方法。



      清单 1. utils.js
      								
       /* 处理 Ajax GET:
       url: 包含服务和参数的 URL 
       callBack: 异步时,需要用户实现的回调,回调参数为 data,格式根据 resultType 指定
       resultType: 返回类型,如 JSON, XML, text 
       asynch: true 异步, false 同步
       progressCallback 调用过程的回调,可选,如果用户需要知道进度可以实现
       */      
       function ajaxGet(url, callBack, resultType, asynch, progressCallback) {} 
      
       /* 获取对象位置 */ 
       function positionOf(obj){ 
          var curleft = curtop = 0; 
          do { 
            curleft += obj.offsetLeft; 
            curtop += obj.offsetTop; 
          } while (obj = obj.offsetParent); 
          return {'left':curleft,'top':curtop}; 
       } 
      
       /* 空格裁剪 */ 
       function trim(src) {} 
       String.prototype.trim = function(){ 
          return trim(this); 
       } 
       /* 进度条对象 */ 
       function LoadingBar(posConfig) {} 
      
       /*javascript 对象 extend 实现 */ 
       Object.extend = function(destination, source) { 
          if(source) { 
              for (var property in source) { 
                  destination[property] = source[property]; 
              } 
          } 
          return destination; 
       } 
       /* 事件绑定的浏览器兼容实现 */ 
       function attachEventX(target,eventName,handlerName){ 
          if ( target.addEventListener ) 
              target.addEventListener(eventName, handlerName, false); 
          else if ( target.attachEvent ) 
              target.attachEvent("on" + eventName, handlerName); 
          else 
              target["on" + eventName] = handlerName; 
       } 
       /* 编码字符串:让 javascript 赋值时不会认为是特殊字符而报错 */ 
       String.prototype.escape = function(){}   
       /* 恢复字符串,按原样显示 */ 
       String.prototype.unescape = function(){}   
           
    • 主体部分

      为了便于理解控件整体实现思路,主体代码只保留了代码总体结构,省去了具体实现细节,用户可以在文章结尾部分下载完整代码查看具体实现。



      清单 2. 代码概览
      								
       function DropDownList(targetObj, dataProvider, config) { 
          this.conf = Object.extend( 
              { 
                  /* 属性和事件的默认配置 */ 
              }, 
              config/* 用户自定义配置 */ 
          ); 
          this.target = targetObj;// 组件的 DOM 组成部分
          this.target.setAttribute("autocomplete", "off");// 关闭浏览器的自动提示
          this.dataService = dataProvider;// 数据来源: 1, 远程 URL 链接 2, 本地 JSON 对象数组
          this.targetPos = positionOf(this.target);// 初始化组件的位置
          this.loadingBar = new CLoadingBar();// 初始化 AJAX-Loading 的进度子控件
          this.ajaxCall = ajaxGet;//ajax 请求方法
          this.currObj;// 记录当前选中条目
          this.data = [];// 缓存的下拉列表数据
          this.evtCache = {}; 
          var self = this; 
          this.hide = function(){}// 隐藏下拉列表
          this.show = function(){}// 显示下拉列表
          this.toggleDisplay = function(){} 
          this.goDown = function(){}// 往下移动当前条目
          this.goUp = function(){}// 往上移动当前条目
          
          this.getItemIndex = function(item){}// 获取当前条目的次序
          
          this.doSel = function(item){}// 选中事件
          this.isOptItem = function(obj){}// 判断是否是下拉框中的条目
          
          this.isDirectionKeyEntered = function(evt){}// 判断是否是方向键,Enter 键或是 Escape 键
          this.doMsOver = function(item){}// 鼠标移到下拉条目上时
          this.doMsOut = function(item){}// 鼠标移出下拉条目时
          /* 根据事件类型返回对应事件的配置信息 */ 
          this.getEventConfig = function(eventType){} 
          /* 为输入控件绑定事件 */ 
          for(var e = 0; e < this.conf.dropdownTrigger.length; e++) { 
              attachEventX(this.target, 
              this.conf.dropdownTrigger[e].eventName, 
              function(event){}); 
          } 
          /* 动态创建下拉列表 DIV*/ 
          this.createPopup = function(){} 
          this.createPopup(); 
          /* 为 body 绑定 click 事件:当点击 body 时,能处理下拉框的隐藏 */ 
          attachEventX(document.body, 'click', function(event){}); 
          /* 下拉框下拉并完成内容装载 */ 
          this.dropdownCustomerList = function(evt, e){} 
          /* 下拉框渲染:处理下拉数据并根据处理后的数据生成下拉条目 */ 
          this.processResult = function(data, content) {} 
       } 
           

      同前面我们详细分析控件需求时的结果一样, 主代码由配置项,子组件(输入框,下拉 DIV,加载动画)和事件组成。



      清单 3. 配置项相关代码
      								
       function DropDownList(targetObj, dataProvider, config) { 
          this.conf = Object.extend( 
              { 
                  'className' : '',// 下拉框外框的样式名
                  /* 数据源须提供的数据字段格式,包括名称和类型 */ 
                  'dataFields':[{'name' : 'name', 'type' : 'string'}], 
                  /* 数据源须提供的作为值列的字段,如 ID*/ 
                  'dataKeyField':{'name' : 'name', 'type' : 'string'}, 
                  /* 数据源须提供的作为显示列的字段,如 Name*/ 
                  'dataDescField':{'name' : 'name', 'type' : 'string'}, 
                  /* 触发下拉的事件 */ 
                  'dropdownTrigger': 
                  [ 
      	            { 
      		            eventName:'keyup', cacheable:true, 
      		            triggerWhenEmpty: false, 
      		            triggerMinLength:1 
      	            } 
      	            , 
      	            {eventName:'click', loadAllForOnce:true} 
                  ], 
                  /* 可外部扩展事件: 下拉触发前 */ 
                  'beforeDropdown': function(){}, 
                  /* 可外部扩展事件: 下拉触发后 */ 
                  'afterDropdown': function(){}, 
                  /* 可外部扩展事件: 选中时 */ 
                  'onSelect': function(data){
                  alert('onSelect event is not implemented!');}, 
                  /* 可外部扩展事件: 输入值是新值,即不在下拉项中 */ 
                  'onNewInput': function(){}, 
                  /* 可外部扩展事件: 输入空时 */ 
                  'onEmpty': function(){}, 
                  /* 下拉前将过滤后的输入框内容作为下拉过滤条件 */ 
                  'getFiltered': function(){}, 
                  /* 远程加载数据时的等待图片 */ 
                  'loadingImgPath':'', 
                  /* 默认下拉框显示高度 */ 
                  'height':200, 
                  /* 默认下拉框显示宽度,0: 与输入框宽度相同 */ 
                  'width':0, 
                  /* 默认下拉页脚内容:HTML 格式 */ 
                  'footHtml':'', 
                  /* 默认页脚高度 */ 
                  'footHeight':0, 
                  /* 下拉项中与输入框内容匹配的部分的颜色 */ 
                  'colorOfhighlightWords':'blue', 
                  /*0: 间隔偶数项背景色 1:间隔奇数想背景色 2:选中或焦点项背景色 */ 
                  'bgColorOfItem':['#ffffff','#F3F2FF', '#6666FF'] 
              }, 
              config 
          ); 
         

      上面的代码详细展示了 配置项数据结构。用户通过在构造参数中指定 config来覆盖组件的默认属性和行为。



      清单 4. 将用户配置的事件绑定到输入控件
      								
          for(var e = 0; e < this.conf.dropdownTrigger.length; e++) { 
              attachEventX(this.target, 
              this.conf.dropdownTrigger[e].eventName, function(event){ 
                if(window.event) { 
                    event.cancelBubble=true; 
                } else { 
                    event.stopPropagation(); 
                } 
                var param = self.getEventConfig(event.type); 
                if(!self.isDirectionKeyEntered(event) 
                && param && typeof(param.delay) != 'undefined' 
                && param.delay > 0) { 
                   var to = eval('window.timeout_'+ self.target.getAttribute("id")); 
                   if(to) { 
                       clearTimeout(to); 
                   } 
                   to = setTimeout( 
                      function(){ 
                        eval('window.timeout_'+ 
                        self.target.getAttribute("id") + ' = null;'); 
                        self.dropdownCustomerList( 
                            event, 
                            param 
                          ); 
                        }, 
                       param.delay 
                  ); 
                  eval('window.timeout_'+ self.target.getAttribute("id") + ' = to;'); 
               }else 
                  self.dropdownCustomerList( 
                        event, 
                        param 
                        ); 
              }); 
          } 
      

      上面的代码让控件能适用不同的应用场景, 如单击鼠标时下拉和击键时下拉 (AutoComplete),或是它们的组合。取决于用户在配置项 dropdownTrigger中的设置。



      清单 5. 下拉框渲染
      								
          this.dropdownCustomerList = function(evt, e){ 
              this.conf.beforeDropdown(); 
              if(this.target.value == '' || this.target.value.length == 0){ 
                  this.hide(); 
                  this.conf.onEmpty(); 
              } 
              if(evt) 
                  kc = window.event?window.event.keyCode : evt.which; 
              /* 按键时的处理 */ 
              if(kc == 38 || kc == 37){ 
                  this.goUp(); 
                  //Down OR Right Arrow key 
              }else if(kc == 40 || kc == 39){ 
                  this.goDown(); 
              }else if(kc == 27){//Escape Key 
                  this.data.length = 0; 
                  this.target.value = ''; 
                  this.hide(); 
                  this.conf.onEmpty(); 
              }else if(kc == 13){//Enter key 
                  if(!this.currObj)//Did not download completely 
                      return false; 
                  this.doSel(this.currObj); 
              }else { 
                /* 处理下拉框渲染部分的代码从略,感兴趣的朋友可以在本文结尾处下载代码 */ 
              } 
          } 
       } 
           

      上面的方法由用户配置的 dropdownTrigger中的事件驱动,如 keyup 事件触发时会调用本方法。通过触发此方法,控制下拉框的隐藏或显示,内容渲染。

应用举例

到此,我们已经分析了 JavaScript 控件需求,组成和实现思路,并列举了关键代码说明了实现要点。以下我们以本地数据源为例列举两类最常用的下拉选择的应用场景。

  • 下拉框选择 ( 本地数据源 )
    • 代码

      清单 6. 本地数据源的下拉例子
      									
       <html>           
        <body> 
           <input type="text" value="" id="my_input" > 
        </body> 
       <script src="dropDownList.js"></script> 
       <script> 
            var dropDownList = new DropDownList( 
      	        document.getElementById("my_input"), 
      	        [{name:'Beijing'},{name:'Beihai'}, 
      	         {name:'Beida'},{name:'Shanghai'},{name:'wuhan'}], 
      	        { 
      	                dropdownTrigger: [{eventName:'click'}], 
      	                  onSelect:function(data){ 
      	                   document.getElementById("
      	                   my_input").value = data['name']; 
      	                } 
      	         } 
              ); 
       </script>       
       </html> 
                
    • 效果图

      图 2. 下拉列表示例
      下拉列表示例

  • 自动填充 ( 本地数据源 )
    • 代码

      清单 7. 本地数据源的自动填充例子
      									
       <html>           
        <body> 
           <input type="text" value="" id="my_input" > 
        </body> 
       <script src="dropDownList.js"></script> 
       <script> 
            var dropDownList = new DropDownList( 
                 document.getElementById("my_input"), 
      	       [{name:'Beijing'},{name:'Beihai'},{name:'Beida'}, 
      	       {name:'Shanghai'},{name:'wuhan'}], 
      	       { 
      	                dropdownTrigger: [{eventName:'keyup'}], 
      	                  onSelect:function(data){ 
      	                   document.getElementById("my_input").value = 
      	                   data['name']; 
      	                } 
                 } 
              ); 
       </script>       
       </html> 
                
    • 效果图

      图 3. 自动填充示例
      自动填充示例

  • 其他

    远程数据源的两种实际应用场景,如果大家感兴趣,不妨试试,有问题可以给我 Email。另外你还可以覆盖默认 配置项中 getFiltered 方法,以让你的控件适用多输入的情况,如 Email 多个收件人的场景 , 以及 TextArea 中语法提示的场景。 关于语法提示,我能想起这样一个具体应用: web 页面上对查询结果集的筛选。如高级查询,我们可以提供一个带语法提示的 TextArea,让用户在一个编辑框中一次输入所有的过滤条件,而不用逐项挨个选择或输入。

总结

本文以日常工作中常见的下拉列表为例,分析了自己编写 JavaScript 控件给日常开发工作带来的好处,带领读者从整理实际的应用需求着手,区分不同的应用场景,分析组件工作的细节,并用代码逐一说明了实现这一个控件的主要步 骤,最后给大家列举了这一控件在常见的几类场景中的应用。

本文也通过实例说明了编写自己的 JavaScript 控件也是提高工作效率的一种方法,一次编写,多处收益,而且维护起来很方便。项目中大家经常能听到埋怨编写代码的时间太短的抱怨声,但回头想想,我们手上 有没有足够好的"武器"可用呢?高质量和通用灵活的控件就是你的武器,那你平常积攒了多少呢?当你积攒了够好,够多的武器时,面对开发时间紧缩的情况,你 的眉宇可能更舒展一些吧。在此我也希望读者是一个工作中的有心人,当反复碰到某一类应用场景时,留意一下现在的代码能否封装为公共代码,做一些积累。

说到效率,有人又说,“没那么多时间专门写控件啊,我用复制粘贴一样高效快速”。分时候,复制粘贴对于长单词,变量,函数名等短的内容,可以 减少因键盘输入带来的低级错误,当你的 IDE 的代码检查和代码提示不能覆盖上面的情况时,复制粘贴是可取的。但对于大段代码复制粘贴的情况,我个人比较反对(这里的情况跟 License 有区分,违反 License 的情况属道德和法律约束的范围),这很可能是一种危险的信号,预示你的代码将会很臃肿,或很凌乱,很难维护。这时你可能需要重新整理思路,分析这种反复出 现的场景的共性是什么,想想如何重构你的代码以适应不断变化的应用场景,使你的 JavaScript 更加灵活通用,更加简洁。

下载文中代码

加载中
0
VongYatin
VongYatin

老大的帖子 要顶 顶了再看

0
jumkey
jumkey

先占位,再细看

0
xu81.com
xu81.com

占位,回头看

0
围墙
围墙

学习一下。

0
lino310
lino310

学习了

0
1001
1001

。。。。牛。。。。

0
马斯克才是个锤子
马斯克才是个锤子

配上汉字拼音转换就更完美了,呵呵

0
ails
ails

这文章写的可以直接那来当教程了,讲解的很细致,太好了

返回顶部
顶部