javascript组件开发

最近忙于重构项目,今天周末把在重构中的一些思想记记:

一、javascript的组件开发:基类的封装

由于这次重构项目需要对各种组件进行封装,并且这些组件的实现方式都差不多,所以想到对组件封装一个base基类(javascript没有类的概念,暂且这样叫把),由于javascript没有原生的类和继承的实现,所以我们首先需要对javascript简单的实现以下类和继承(见一下代码注释实现方案改于jq作者John Resig):

 1 //javascript简单的实现类和继承
 2 var Class = (function() {
 3   //模拟extend继承方式
 4   var _extend = function() {
 5     //属性混入函数,不混入原型上的属性,原型上的属性就多了哈
 6     var _mixProto = function(base, extend) {
 7       for (var key in extend) {
 8         if (extend.hasOwnProperty(key)) {
 9           base[key] = extend[key];
10         }
11       }
12     };
13     //这里是一个开关,目的是为了在我们继承的时候不调用父类的init方法渲染,而把渲染放在子类
14     //试想没这个开关,如果在继承的时候父类有init函数就会直接渲染,而我们要的效果是继承后的子类做渲染工作
15     this.initializing = true;
16     //原型赋值
17     var prototype = new this();
18     //开关打开
19     this.initializing = false;
20     //for循环是为了实现多个继承,例如Base.extend(events,addLog)
21     for (var i = 0,len = arguments.length; i < len; i++) {
22       //把需要继承的属性混入到父类原型
23       _mixProto(prototype, arguments[i].prototype||arguments[i]);
24     }
25     //继承后返回的子类
26     function SonClass() {
27       //检测开关和init函数的状态,看是否做渲染动作
28       if (!SonClass.initializing && this.init)
29         //调用返回的子类的init方法渲染,把传给组件的配置参数传给init方法
30         this.init.apply(this, arguments);
31     }
32     //把混入之后的属性和方法赋值给子类完成继承
33     SonClass.prototype = prototype;
34     //改变constructor引用,不认子类的构造函数将永远是Class超级父类
35     SonClass.prototype.constructor = SonClass;
36     //给子类页也添加继承方法,子类也可以继续继承
37     SonClass.extend = arguments.callee;//也可以是_extend
38     //返回子类
39     return SonClass
40   };
41   //超级父类
42   var Class = function() {};
43   
44   Class.extend = _extend;
45   //返回超级父类
46   return Class
47 })();

有了上面的代码,我们就可以这样做了:

var Base = Class.extend({
  init : function(__config) {
this.creatDom(__config)
this.bind(__config)
},
creatDom : function(config) {}, bind : function(config) {}, getVal : function() {}, setVal : function() {} })

然后如果我们有100个组件,我们可以这样:

1 var mod1 = Base.extend({
2 //组件独有的方法
3 });
4 //传入配置参数渲染mod1
5 var mod1 = new mod1(__config);

以上就实现了基类的封装,then

二、javascript的组件开发:组件交互

我们知道组件交互的东西是非常头疼的,组件交互可能涉及到两个,多个(比如标题的自动补全,区域的自动匹配等等),下面说说具体实现

有了上面的超级父类Class,现在组件间交互的实现如下:

 1 //定义事件交互对象(模仿jq的Callbacks的源码实现方案)
 2 var Event = {
 3   //内部方法,找出数组里某个元素的索引index
 4   _indexOf : function(array,key){
 5     if (array === null) return -1
 6     var i = 0, length = array.length
 7     for (; i < length; i++) if (array[i] === item) return i
 8     return -1
 9   },
10   //添加事件监听
11   add:function(key,listener){
12     //定义组件有哪些事件,每个时间的处理函数
13     if (!this.__events) {
14       this.__events = {}
15     }
16     //监听的事件如果在组件上已经有了则不做监听
17     if (!this.__events[key]) {
18       this.__events[key] = []
19     }
20     //对监听的事件push处理函数
21     //注意同一个监听事件可能有多个处理函数(比如某一个组件完成是需要对不同的组件做不同的处理)
22     if (this._indexOf(this.__events[key],listener) === -1 && typeof listener === 'function') {
23       this.__events[key].push(listener)
24     }
25     //返回this可以继续执行组件对象的方法
26     return this
27   },
28   //事件触发器
29   fire:function(key){
30     //检测是否存在监听事件
31     if (!this.__events || !this.__events[key]) return
32     //arguments转数组
33     var args = [].slice.call(arguments, 1) || []
34     //获取需要触发的事件
35     var listeners = this.__events[key]
36     var i = 0
37     var l = listeners.length
38 
39     for (i; i < l; i++) {
40       //执行绑定在触发事件上的回调函数
41       listeners[i].apply(this,args)
42     }
43     //返回this可以继续执行组件对象的方法
44     return this
45   },
46   //解绑事件(取消监听)
47   off:function(key,listener){
48     //不传任何参数直接解绑所有监听事件和执行函数
49     if (!key && !listener) {
50       this.__events = {}
51     }
52     //不传具体执行函数,解绑该事件
53     if (key && !listener) {
54       delete this.__events[key]
55     }
56     //都存在时,只解绑当前绑定事件的处理函数
57     if (key && listener) {
58       var listenerfn = this.__events[key];
59       var index = this._indexOf(listenerfn, listener)
60       //这个是加特技,如果index > -1,则执行后面的操作(如果传入的key和listener能在this.__events里面匹配到则删掉它,ps:这里没做删除后数组为空的处理)
61       (index > -1) && listenerfn.splice(index, 1)
62     }
63     //返回this可以继续执行组件对象的方法
64     return this;
65   }
66 };
67 //让子类都拥有事件监听
68 var Base = Class.extend(Event);

有了上面的Base子类(相对于超级父类Class来说):then 下面简单实现一个组件实现和两组间的交互:

 1 var mobile = Base.extend({
 2   init : function(opts){
 3     this.defaults = {
 4       type : "mobile",
 5       name : "Phone",
 6       title : "手机号",
 7       classname :"phoneNumber",
 8       prop : {placeholder : "请输入手机号码",issub : 1},
 9       notnull : true
10     };
11     this.options = $.extend(true, {}, this.defaults, opts);
12     this.render(this.options);
13     this.bind();
14     this.setVal();
15   },
16   bind : function(fnObj){
17     //some event
18     var _this = this;
19     //绑定finish事件blur等等
20     if (fnObj) {
21         _this.dom.on(fnObj);
22     };
24     _this.dom.on({'input': function(event) {             
25       var value = _this.getVal();
//当这个组件input时去触发setTitle自定义监听的事件 26 _this.fire("setTitle",value); 27 }}); 28 }, 29 render : function(options){ 30 var domStr = "<li class='_item "+options.classname+"'><span class='mb_title'>"+options.title+"</span><div class='mb_itemtext'><input type='number' placeholder='"+options.prop.placeholder+"' +options.name+"' name='"+options.name+"'></div><div class='errorTip "+options.name+"error"+"'><div class='errorTipDiv'></div>"+"<span></span>"+"</div></li>"; 31 var dom = $(domStr); 32 this.dom = dom.find("input"); 33 if (options.notnull) { 34 dom.appendTo(".notNullGroup"); 35 }else{ 36 dom.appendTo(".canNullGroup"); 37 } 38 }, 39 getVal : function(){ 40 return this.dom.val() 41 }, 42 setVal : function(){ 43 var val = detail[this.options.name]; 44 this.dom.val(val) 45 } 46 }); 47 var Phone = new mobile({ 48 type : "mobile", 49 name : "Phone", 50 title : "手机号", 51 classname :"phoneNumber", 52 prop : {placeholder : "输入手机号码",issub : 1}, 53 notnull : true 54 });

上面实现了一个电话号码组件下面在实现一个标题组件:

 1 var text = Base.extend({
 2       init : function(opts){
 3         this.defaults = {
 4               type : "text",
 5               name : "Title",
 6               title : "标题",
 7               classname :"titleInput",
 8               prop : {placeholder : "请填写8-28字的标题",issub : 1},
 9               notnull : true
10         };
11         this.options = $.extend(true, {}, this.defaults, opts);
12         this.render(this.options);
13         this.bind();
14         this.setVal();
15       },
16       bind : function(fnObj){
17           if(fnObj){
18             this.dom.on(fnObj)
19         }
20       },
21       render : function(options){
22         var domStr = "<li class='_item "+options.classname+"'><span class='tx_title'>"+options.title+"</span><div class='tx_itemtext'><input type='text' value='"+detail[options.name]+"' placeholder='"+options.prop.placeholder+"' +options.name+"' name='"+options.name+"'></div><div class='errorTip "+options.name+"error"+"'><div class='errorTipDiv'></div>"+"<span></span>"+"</div></li>";
23         var dom = $(domStr);
24         this.dom = dom.find("input");
25         if (options.notnull) {
26             dom.appendTo(".notNullGroup");
27         }else{
28             dom.appendTo(".canNullGroup");
29         }
30     },
31       getVal : function(){
32         return this.dom.val()
33       },
34       setVal : function(value){
35           if(value){
36               this.dom.val(value)
37           }else{
38               var val = detail[this.options.name];
39             this.dom.val(val)
40           }
41       }
42 });
43 var Title = new text({
44   type : "text",
45   name : "Title",
46   title : "标题",
47   classname :"titleInput",
48   prop : {placeholder : "请填写8-28字的标题",issub : 1},
49   notnull : true
50 });
//这里是添加监听setTitle事件
51 Phone.add("setTitle",function(val){});

OK 两个组件dom渲染实现 也简单的实现了组件交互

then:

实现了还不够,还要易于管理代码,这样做的话如果pm使劲加复杂的组件交互我们得累趴

then:

单拿出一个模块作为时间交互模块:

1 detailObj["Phone"].bind({'input': function(event) {             
2   var value = detailObj["Phone"].getVal();
3   detailObj["Phone"].fire("setTitle",value);
4 }});
5 detailObj["Phone"].add("setTitle",function(value){
6   detailObj["Title"].setVal(value);
7 });

OK上面的detailObj是一个所有组件的Map,用于查找实例化组件对象

这样我们就可以统一管理我们的时间交互模块了,需要加事件交互就可以往这里添加

1 detailObj["Phone"].bind({'blur': function(event) {             
2   var value = detailObj["Phone"].getVal();
3   detailObj["Phone"].fire("aaa",value);
4 }});
5 detailObj["Phone"].add("aaa",function(value){
6   alert(value)
7 });

三、javascript的组件开发:代码统一管理

上面是实现了基类的封装,我们的组件都可以继承父类的属性和方法,现在问题来了PM使劲加需求,什么错误日志、什么点击统计、什么组件交互啥的,我们就需要统一管理它们,要不然在组件内部封装好了,每次加需求都得动组件内部,是不是很想捶PM哇,这样我们就需要把日志统计、点击统计啥的单独封装模块,今天PM要下掉错误日志,把模块拿掉就是了,而不用动每个组件。

其实上面已经实现了简单的组件交互模块管理,下面看看一个简单的埋点统计clickLog模块的实现(线上的埋点统计好像是直接写在页面上的)

首先我们需要和上面的组件交互一样定义在Base里面为每个组件添加一个addLog对象

 1 var addLog = {
 2   //参数定义:(暂时只做了addLog,当然也需要delLog等不在这儿贴代码了)
 3   //type 需要统计日志类型click?load?change等等
 4   //targetDomArr 可以是数组,可以是单个元素 需要统计的组件对象的哪些Dom节点
 5   //fromName 统计日志传后台的参数
 6   addLog : function(type,targetDomArr,fromName){
 7     if($(targetDomArr).length > 1){
 8       for (var i = 0; i < targetDomArr.length; i++) {
 9         (function(i){
10           var targetDom = $(targetDomArr[i]);
11           targetDom.on(type,function(e){
12             clickLog("from="+fromName[i]);
13             //e.stopPropagation();
14             //e.preventDefault();
15           })
16         })(i)
17       }
18     }else{
19       targetDomArr.on(type,function(e){
20         clickLog("from="+fromName);
21         //e.stopPropagation();
22         //e.preventDefault();
23       })
24     }
25   }
26 };
27 var Base = Class.extend(Event,addLog);

这样我们每个组件对象都有一个addLog方法,需要统计啥就addLog就好了(当然像头尾并未封装成组件所以只能用$("xxx")绑定),如下:

 1 define([],function(){
 2     var  _clickLog = function(detailObj){
 3         //日志统计,PM要多少写多少
 4         $(".h_regist").on("click",function(){clickLog("from=post_fill_regist")});
 5         $(".h_login").on("click",function(){clickLog("from=post_fill_login")});
 6         detailObj["imgUpload"].addLog("click",[$(".upload_action"),$(".upload_delete")],["post_fill_camera","post_fill_camera_del"]);
 7         detailObj["canNullSplit"].addLog("click",detailObj["canNullSplit"].dom,"post_fill_optional");
 8         detailObj["button"].addLog("click",$(".btn_post"),"post_fill_release");
 9     };
10     return _clickLog;
11 })

ok:上面简单的实现了一个addLog模块

then:

该休息了(大晚上的语言组织有误还望指正)

下面是一个简单的组件交互demo,供参考