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+"' id='"+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+"' id='"+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){Title.setVal(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,供参考