开头想明确一些概念,因为有些概念不明确会导致很多问题,比如你写这个框架为什么不去解决啥啥啥的问题,哎,心累。
什么是框架?
百度的解释:框架(Framework)是整个或部分系统的可重用设计,表现为一组抽象构件及构件实例间交互的方法;另一种定义认为,框架是可被应用开发者定制的应用骨架。其实就是某种应用的半成品,就是一组组件,供你选用完成你自己的系统。简单说就是使用别人搭好的舞台,你来做表演。但是更核心的是,作者通过框架更多的传达的不是技术的实现,而是一种设计思想的展现。
什么是模块化?
在javascript权威指南中是这样说的,首先将js中的代码组织到类中,可以在很多不同场景实现复用。但类不是唯一的模块化方式,一般来讲,模块是一个独立的js文件。模块文件可以包含一个类定义,一组相关的类,一个实用的函数库或者是一些待执行的代码。只要以模块的形式编写代码,任何js代码段就可以当作一个模块。
为什么要写框架?
首先框架是一种半成品,为任何人提供了通过这个半成品去更快速的开发自己的项目。在软件开发领域,不可能有一个框架去细分出所有完善领域,所以每个框架是针对一个细分领域的完善,比如,jQuery是为了更方便操作DOM,require是为了管理js和模块化的加载,vue和anguar为了在MVVM中解决viewmodel这类问题等等。
该框架的解决目标:
1. 针对传统布局确定之后再修改布局就要全部重新设计页面问题,引入加载容器方案,重新更换容器配置组件映射关系,即可完成更换
2. 针对传统页面功能模块之间的高耦合低内聚问题,拆分所有页面组件,完成每个组件从html+js+css只完成本组件的所有事
3. 提供前端分布式协作开发提供一种解决方案。提供一个网站入口,解决多人可在不同地点、不同时间、不同空间协作开发的方案
4. 针对传统维护卸载整个项目维护问题。该方案提供了一种在线动态卸载加载组件方案
5. 其他彩蛋可自己发现,因功能正在完善中...
好了废话不多说了,下面直接切入正题。该篇博客牵扯到的概念:
设计模式
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
观察者模式
观察者模式(有时又被称为发布(publish )-订阅(Subscribe)模式、模型-视图(View)模式、源-收听者(Listener)模式或从属者模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。
简单可理解的观察者模式代码如下:
/** * Created by gerry.zhong on 2017/2/13. */ //创建发布者 function Publisher(){ this.subscribers = []; } //发布动作 Publisher.prototype.deliver = function(data){ this.subscribers.forEach(function(fn){fn(data);}); return this; }; //定义订阅者 Function.prototype.subscribe = function(publisher){ var that = this; var alreadyExists = publisher.subscribers.some(function(el){ return el === that; }); if (!alreadyExists){ publisher.subscribers.push(this); }; return this; }; //定义退订 Function.prototype.unsubscribe = function(publisher){ var that = this; publisher.subscribers = publisher.subscribers.filter(function(el){ return el !== that }); return this; };
测试代码:
+(function(){ var T1 = new Publisher; var T2 = new Publisher; var T3 = new Publisher; var s1 = function(from){ console.log(from); }; var s2 = function(from){ console.log(from); }; var s3 = function(from){ console.log(from); }; s1.subscribe(T1); s2.subscribe(T1).subscribe(T2).subscribe(T3); s3.subscribe(T1).subscribe(T3); T1.deliver("我是T1 推送"); T2.deliver("我是T2 123"); T3.deliver("我是T3 11"); })();
测试结果:
这是最简单的订阅和发布者机制,下面开始和框架整合。
思路如下:
1. 将订阅和发布机制代码以工具插入代码供核心使用
//订阅 Function.prototype.subscribe = function(publisher){ var that = this; var alreadyExists = publisher.subscribers.some(function(el){ return el === that; }); if (!alreadyExists){ publisher.subscribers.push(this); }; return this; }; //退订 Function.prototype.unsubscribe = function(publisher){ var that = this; publisher.subscribers = publisher.subscribers.filter(function(el){ return el !== that }); return this; };
2. 在加载时候首先记录总共加载的组件和当前加载完毕的组件的数值(初始化),然后判断该组件状态,是否卸载,如果加载则为组件创建发布者。
// 4. 处理配置容器和组件映射关系,取得所有容器所要加载组件的信息 var temp = ui.dataPool.getData_glo("private","pageConName"); //取得配置文件中关于当前容器中的容器-组件对应关系 var tempS = ui.dataPool.getData_glo("config","con_com",temp); //记录组件的数量,为后期组件之间的流转数据做准备 ui.dataPool.setData_glo("private",{"comCount":0}); ui.dataPool.setData_glo("private",{"currCount":1}); // 5. 判断组件是否存在,存在即加载组件 $.each(tempS,function(value,key){ var getComInfo = ui.component.isExist_com(value); if(getComInfo){ if (getComInfo[4]){ // 生成组件的发布者 var temp ={};temp[value] = new $5; ui.dataPool.setData_glo("private","observer",temp); //该数据是需要推迟到组件加载完毕之后再发布消息,so 先存储 ui.dataPool.setData_glo("private",{"delayPubArr":[]}); ui.component.loadComponent(value,getComInfo[0]); }else { var height = _("[ui-con='"+key+"']").css("height"); _("[ui-con='"+key+"']").html($4.loadErr("line-height:"+height)); }; }else { console.log($3.component.comConfig(value)); } });
3. 在加载组件的js脚本中计算加载的数量,并在回调中处理发布的消息
//加载组件脚本,并注入组件所需要的数据 loadComJs:function(url,comName,uuidCom,callback){ if (url === undefined || url === "") return; var count = ui.dataPool.getData_glo("private","comCount")+1; //获取当前组件加载的数量 ui.dataPool.setData_glo("private",{"comCount":count}); //统计加载的数量 var scriptDom = _.createTag("script",{"src":url,"uuid":uuidCom,"comName":comName}); scriptDom.onload = scriptDom.onreadystatechange = function(){ if(!this.readyState || this.readyState=='loaded' || this.readyState=='complete'){ use.data = ui.component.getInfoFromPool(this.uuid,this.comName); //获取自动注入参数 use(true); ui.component.delayPublish(); //推迟消息发布 if (callback === undefined) return ; else callback(use.callObj); } }; _("head").append(scriptDom); },
4. 核心组件方法中增加3个方法,针对框架本身做集成处理
//组件发布消息 deliverCom:function(comName,content,isInit){ var whoPublisher = ui.dataPool.getData_glo("private","observer",comName); //如果为初始化时候发布的消息,则推迟到组件加载完毕再发布 if (!isInit) { whoPublisher.deliver(content); }else { //该数据需要推迟到组件加载完毕之后再发布消息,so 先存储 ui.dataPool.getData_glo("private","delayPubArr").push([whoPublisher,content]); }; }, //推迟消息发布,延迟到所有组件加载完毕 delayPublish:function(){ var comCount = ui.dataPool.getData_glo("private","comCount"); var currCount = ui.dataPool.getData_glo("private","currCount"); console.log("组件总数量:"+comCount+",当前加载组件:"+currCount); if ( currCount === comCount ){ console.log("组件加载完毕!"); var publishArr = ui.dataPool.getData_glo("private","delayPubArr"); $.each(publishArr,function(value){ value[0].deliver(value[1]); }); }; ui.dataPool.setData_glo("private",{"currCount":currCount+1}); }, //处理组件的订阅 subscribeCom:function(comNameArr,callback){ $.each(comNameArr,function(value){ var whoPublisher = ui.dataPool.getData_glo("private","observer",value); callback.subscribe(whoPublisher); }); },
5. 每个组件模块的js中配置发布和回调(test组件和test1组件以及test2组件)
test组件js代码:
/** * Created by gerry.zhong on 2017/2/5. */ use(function(data,that){ ui.component.reader({ //reader为一些初始化需要的操作,有时候会有注册事件等,或者一些预操作,加载完毕,会首先跑这个方法,这是一个入口 reader:function(){ console.log("组件1执行...."); that = this; ui.component.deliverCom(data.comName,"发布消息1!"); that.registerEle.click_demo1(); }, //注入所有的选择器,方便选择器变化,直接修改该对象中的选择器,而不需要全局去更改 selector:{ testBtn:"#testBtn", //按钮 demo1:"#demo1" }, //注入page中所有的事件,统一管理,建议命名规范:事件_命名,例 click_login registerEle:{ click_demo1:function(){ document.querySelectorAll(that.selector.demo1)[0].onclick = function(){ ui.component.deliverCom(data.comName,"发布消息!") } } }, //注入所有ajax请求,页面所有请求,将在这里统一管理,建议命名规范:ajax_命名,例 ajax_login ajaxRequest:{ }, //处理所有回调函数,针对一个请求,处理一个回调 callback:{ }, //临时缓存存放区域,仅针对本页面,如果跨页面请存放cookie或者localstorage等 //主要解决有时候会使用页面控件display来缓存当前页面的一些数据 temp:{ }, /* * 业务使用区域,针对每个特别的业务去串上面所有的一个个原子 * 因为上面所有的方法,只是做一件事,这边可以根据业务进行串服务,很简单的 * */ firm:{ }, subscribe_Com:[], //该对象配置该组件需要订阅哪个组件的消息 //该方法为消息发布的回调 subscribe_call:function(data){ }, }); });
test2组件的js:
/** * Created by gerry.zhong on 2017/2/5. */ use(function(data,that){ /* * 该对象承载所有需要抛出去的对象 * 1.该对象中的方法可以自己写 * 2.该对象中的方法可以注入(例子中的tempObj.tool.AA) * 3.该对象也可以选择性抛出给使用者需要的方法,也可以隐藏(tool.BBBB) * */ ui.component.reader({ //reader为一些初始化需要的操作,有时候会有注册事件等,或者一些预操作 reader:function(){ console.log("组件2执行...."); that = this; }, //注入所有的选择器,方便选择器变化,直接修改该对象中的选择器,而不需要全局去更改 selector:{ testBtn:"#testBtn", //按钮 }, //注入page中所有的事件,统一管理,建议命名规范:事件_命名,例 click_login registerEle:{ }, //注入所有ajax请求,页面所有请求,将在这里统一管理,建议命名规范:ajax_命名,例 ajax_login /* * 该请求中有2种方案,看需求使用 * 1.不公用一个请求方案 * 2.公用一个请求,但是回调处理不一样 * */ ajaxRequest:{ }, //处理所有回调函数,针对一个请求,处理一个回调 callback:{ }, //临时缓存存放区域,仅针对本页面,如果跨页面请存放cookie或者localstorage等 //主要解决有时候会使用页面控件display来缓存当前页面的一些数据 temp:{ }, /* * 业务使用区域,针对每个特别的业务去串上面所有的一个个原子 * 因为上面所有的方法,只是做一件事,这边可以根据业务进行串服务,很简单的 * */ firm:{ }, //配置订阅组件 subscribe_com:["test"],
//订阅消息的回调 subscribe_call:function(data){ console.log("接受订阅消息为:"+data); } }); });
test2组件的js:
/** * Created by gerry.zhong on 2017/2/5. */ use(function(data,that){ /* * 该对象承载所有需要抛出去的对象 * 1.该对象中的方法可以自己写 * 2.该对象中的方法可以注入(例子中的tempObj.tool.AA) * 3.该对象也可以选择性抛出给使用者需要的方法,也可以隐藏(tool.BBBB) * */ var tempObj ={ //reader为一些初始化需要的操作,有时候会有注册事件等,或者一些预操作 reader:function(){ that = this; console.log("组件3执行...."); }, //注入所有的选择器,方便选择器变化,直接修改该对象中的选择器,而不需要全局去更改 selector:{ testBtn:"#testBtn", //按钮 }, //注入page中所有的事件,统一管理,建议命名规范:事件_命名,例 click_login registerEle:{ click_testBtn:function(){ //注册单击事件 document.querySelectorAll(that.selector.testBtn)[0].onclick = function(){ that.firm.testLoad(); } } }, //注入所有ajax请求,页面所有请求,将在这里统一管理,建议命名规范:ajax_命名,例 ajax_login /* * 该请求中有2种方案,看需求使用 * 1.不公用一个请求方案 * 2.公用一个请求,但是回调处理不一样 * */ ajaxRequest:{ }, //处理所有回调函数,针对一个请求,处理一个回调 callback:{ }, //临时缓存存放区域,仅针对本页面,如果跨页面请存放cookie或者localstorage等 //主要解决有时候会使用页面控件display来缓存当前页面的一些数据 temp:{ }, /* * 业务使用区域,针对每个特别的业务去串上面所有的一个个原子 * 因为上面所有的方法,只是做一件事,这边可以根据业务进行串服务,很简单的 * */ firm:{ testLoad:function(){ alert("获取接口的值:"+data.interface) } }, //订阅组件配置 subscribe_com:["test"],
//订阅组件的回调函数 subscribe_call:function(data){ console.log("组件3接受订阅消息为:"+data); } }; ui.component.reader(tempObj); });
6. 组件之间数据流转测试。初始化的组件reader方法中的消息发布没有执行,但是注册的单击事件的消息发布成功了。so 这里肯定有问题。因为组件加载的时候,比如组件test加载完了之后,但是其他组件(test1、test2)都没有加载成功,所以组件test的消息发布其他组件是接受不到的。所以,只能将所有初始化中的消息的发布,推迟到所有组件加载完毕之后再推送消息。
7. 所以在核心组件的发布消息中定义了一个参数,最后一个参数为ture的时候,会推迟到组件加载完毕之后再发布的。
ui.component.deliverCom(data.comName,"发布消息1!",true); //最后一个参数为true的时候,延迟加载
8. 再看测试结果,点击事件中的发布也可以使用了。
组件之间的消息流转是组件的核心,因为这样可以使组件开发更加低耦合。以前开发,可能会出现功能组件之间的高度耦合状况,可能我左边有一个菜单组件,右边有一个内容组件,左边菜单变更的时候,在单击事件中使用到右边组件的选择器啊,html标签变更,或者状态展示变更。这2个组件之间太耦合,导致以后变更的时候必须2个组件同时变更。现在通过完善的消息发布和订阅机制,每个组件只需要关心自己组件的问题,其他组件通过消息传递过来,组件根据消息进行变更。这样就完成了组件的搞内聚,更改组件so easy。只需要更改后发布消息就好了,组件之间相互影响降到最低。
ui.js 1.1版本完善了组件之间的消息流转问题,这样使得开发更专注于开发一个好组件。
github地址:https://github.com/GerryIsWarrior/UI.js 点颗星,动力。将框架完善的更好。
我愿用我力所能及的力量,改变世界!