继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

【单页应用之通信机制】view之间应该如何通信

不要慕码人我要切诺基
关注TA
已关注
手记 281
粉丝 37
获赞 120

前言

在单页应用中,view与view之间的通信机制一直是一个重点,因为单页应用的所有操作以及状态管理全部发生在一个页面上

没有很好的组织的话很容易就乱了,就算表面上看起来没有问题,事实上会有各种隐忧,各种坑等着你去跳

最初就没有一定理论上的支撑,极有可能是这么一种情况:

① 需求下来了,搞一个demo做交待

② 发现基本满足很满意,于是直接在demo中做调整

上面的做法本身没有什么问题,问题发生在后期

③ 在demo调整后应用到了实际业务中,发现很多地方有问题,于是见一个坑解决一个坑

④ 到最后感觉整个框架零零散散,有很多if代码,有很多代码不太懂意思,但是一旦移除就报错

这个时候我们就想到了重构,重构过程中就会发现最初的设计,或者说整个框架的基础有问题,于是就提出推翻重来

若是时间上允许,还可以,但是往往重构过程中,会多一些不按套路出牌的同学,将API接口给换了,这一换所有的业务系统全部崩溃

所以说,新的框架会对业务线造成压力,会提高测试与编码成本,于是就回到了我们上篇博客的问题

【UI插件】简单的日历插件(下)—— 学习MVC思想

一些同学认为,以这种方式写UI组件过于麻烦,但是我们实际的场景是这样的

我们所有的UI 组件可能会由一个UIAbstractView继承而来,这样的继承的好处是:

① 我们每个UI组件都会遵循一个事件的流程做编写,比如:

onCreate->preShow->show->afterShow->onHide->destroy (简单说明即可)

于是我们想在每一个组件显示前做一点操作的话,我们可以统一写到AbstractView中去(事实上我们应该写到businessView中)

② 在AbstractView中我们可以维护一个共用的闭包环境,这个闭包环境被各个UI组件共享,于是UI与UI之间的通信就变成了实例的操作而不是dom操作

当然,事实上通过DOM的操作,选择器,id的什么方式可能一样可以实现相同的功能,但是正如上面所言,这种方式会有隐忧

事实上是对UI组件编写的一种约束,没有约束的组件做起来当然简单但是有了约束的组件的状态处理才能被统一化,因为单页应用的内存清理、状态管理才是真正的难点

PS:此文仅代表个人浅薄想法,有问题请指出

消息通信机制

其实所谓消息通信,不过是一种发布订阅的关系,又名观察者;观察者有着一对多的关系

多个对象观察同一个主体对象,若是主体对象发生变化便会通知所有观察者变化,事实上观察者本身又可以变成主体对象,所以多对多的关系偶尔不可避免

还有一些时候观察者也可能变成自己,自己的某些状态会被观察

其实前面扯那么多有的没的不如来一个代码,在Backbone中有一段代码简单实现了这个逻辑

复制代码

 1 var Events = Backbone.Events = { 2   on: function (name, callback, context) { 3     if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 4     this._events || (this._events = {}); 5     var events = this._events[name] || (this._events[name] = []); 6     events.push({ callback: callback, context: context, ctx: context || this }); 7     return this; 8   }, 9 10   off: function (name, callback, context) {11     var retain, ev, events, names, i, l, j, k;12     if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;13     if (!name && !callback && !context) {14       this._events = {};15       return this;16     }17 18     names = name ? [name] : _.keys(this._events);19     for (i = 0, l = names.length; i < l; i++) {20       name = names[i];21       if (events = this._events[name]) {22         this._events[name] = retain = [];23         if (callback || context) {24           for (j = 0, k = events.length; j < k; j++) {25             ev = events[j];26             if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||27               (context && context !== ev.context)) {28               retain.push(ev);29             }30           }31         }32         if (!retain.length) delete this._events[name];33       }34     }35 36     return this;37   },38 39   trigger: function (name) {40     if (!this._events) return this;41     var args = slice.call(arguments, 1);42     if (!eventsApi(this, 'trigger', name, args)) return this;43     var events = this._events[name];44     var allEvents = this._events.all;45     if (events) triggerEvents(events, args);46     if (allEvents) triggerEvents(allEvents, arguments);47     return this;48   },49 };

复制代码

这是一段简单的逻辑,也许他的主干还不全,我们这里若是做一个简单的实现的话就会变成这个样子:

复制代码

 1 var Events = {}; 2 Events.__events__ = {}; 3  4 Events.addEvent = function (type, handler) { 5   if (!type || !handler) { 6     throw "addEvent Parameter is not complete!"; 7   } 8   var handlers = Events.__events__[type] || []; 9   handlers.push(handler);10   Events.__events__[type] = handlers;11 };12 13 Events.removeEvent = function (type, handler) {14   if (!type) {15     throw "removeEvent parameters must be at least specify the type!";16   }17   var handlers = Events.__events__[type], index;18   if (!handlers) return;19   if (handler) {20     for (var i = Math.max(handlers.length - 1, 0); i >= 0; i--) {21       if (handlers[i] === handler) handlers.splice(i, 1);22     }23   } else {24     delete handlers[type];25   }26 };27 28 Events.trigger = function (type, args, scope) {29   var handlers = Events.__events__[type];30   if (handlers) for (var i = 0, len = handlers.length; i < len; i++) {31     typeof handlers[i] === 'function' && handlers[i].apply(scope || this, args);32   }33 };

复制代码

整个程序逻辑如下:

① 创建一个events对象作为消息存放点

② 使用on放events中存放一个个事件句柄

③ 在满足一定条件的情况下,触发相关的事件集合

IScroll中的消息机制

简单而言,以IScroll为例,他在构造函数中定义了默认的属性:

this._events = {};

然后提供了最简单的注册、触发接口

复制代码

 1 on: function (type, fn) { 2   if (!this._events[type]) { 3     this._events[type] = []; 4   } 5  6   this._events[type].push(fn); 7 }, 8  9 _execEvent: function (type) {10   if (!this._events[type]) {11     return;12   }13 14   var i = 0,15         l = this._events[type].length;16 17   if (!l) {18     return;19   }20 21   for (; i < l; i++) {22     this._events[type][i].call(this);23   }24 },

复制代码

因为IScroll中涉及到了自身与滚动条之间的通信,所以是个很好的例子,我们看看IScroll的使用:

他对自身展开了监听,若是发生以下事件便会触发响应方法

复制代码

 1 _initIndicator: function () { 2   //滚动条 3   var el = createDefaultScrollbar(); 4   this.wrapper.appendChild(el); 5   this.indicator = new Indicator(this, { el: el }); 6  7   this.on('scrollEnd', function () { 8     this.indicator.fade(); 9   });10 11   var scope = this;12   this.on('scrollCancel', function () {13     scope.indicator.fade();14   });15 16   this.on('scrollStart', function () {17     scope.indicator.fade(1);18   });19 20   this.on('beforeScrollStart', function () {21     scope.indicator.fade(1, true);22   });23 24   this.on('refresh', function () {25     scope.indicator.refresh();26   });27 28 },

复制代码

比如在每次拖动结束的时候,皆会抛一个事件出来

that._execEvent('scrollEnd');

他只负责抛出事件,然后具体执行的逻辑其实早就写好了,他不必关注起做了什么,因为那个不是他需要关注的

再说回头,IScroll的事件还可以被用户注册,于是用户便可以在各个事件点封装自己想要的逻辑

比如IScroll每次移动的结果都会是一个步长,便可以在scrollEnd触发自己的逻辑,但是由于iScroll最后的移动值为一个局部变量,所以这里可能需要将其中的newY定制于this上

实现消息中心

如IScroll的消息机制只会用于自身,如Backbone的Model、View层各自维护着自己的消息中心,在一个单页框架中,此消息枢纽事实上可以只有一个

比如页面标签的View可以是一个消息群组

UI组件可以是一个消息群组

Model层也可以是一个消息群组

......

所以这个统一的消息中心,事实上我们一个框架可以提供一个单例,让各个系统去使用

复制代码

  7 Dalmatian = {};  8   9 Dalmatian.MessageCenter = _.inherit({ 10   initialize: function () { 11     //框架所有的消息皆存于此 12     /*  13     { 14     view: {key1: [], key2: []}, 15     ui: {key1: [], key2: []}, 16     model: {key1: [], key2: []} 17     other: {......} 18     } 19     */ 20     this.msgGroup = {}; 21   }, 22  23   _verify: function (options) { 24     if (!_.property('namespace')(options)) throw Error('必须知道该消息的命名空间'); 25     if (!_.property('id')(options)) throw Error('该消息必须具备key值'); 26     if (!_.property('handler')(options) && _.isFunction(options.handler)) throw Error('该消息必须具备事件句柄'); 27   }, 28  29   //注册时需要提供namespace、key、事件句柄 30   //这里可以考虑提供一个message类 31   register: function (namespace, id, handler) { 32     var message = {}; 33  34     if (_.isObject(namespace)) { 35       message = namespace; 36     } else { 37       message.namespace = namespace; 38       message.id = id; 39       message.handler = handler; 40  41     } 42  43     this._verify(message); 44  45     if (!this.msgGroup[message.namespace]) this.msgGroup[message.namespace] = {}; 46     if (!this.msgGroup[message.namespace][message.id]) this.msgGroup[message.namespace][message.id] = []; 47     this.msgGroup[message.namespace][message.id].push(message.handler); 48   }, 49  50   //取消时候有所不同 51   //0 清理所有 52   //1 清理整个命名空间的事件 53   //2 清理一个命名空间中的一个 54   //3 清理到具体实例上 55   unregister: function (namespace, id, handler) { 56     var removeArr = [ 57       'clearMessageGroup', 58       'clearNamespace', 59       'clearObservers', 60       'removeObserver' 61       ]; 62     var removeFn = removeArr[arguments.length]; 63  64     if (_.isFunction(removeFn)) removeFn.call(this, arguments); 65  66   }, 67  68   clearMessageGroup: function () { 69     this.msgGroup = {}; 70   }, 71  72   clearNamespace: function (namespace) { 73     if (this.msgGroup[namespace]) this.msgGroup[namespace] = {}; 74   }, 75  76   clearObservers: function (namespace, id) { 77     if (!this.msgGroup[namespace]) return; 78     if (!this.msgGroup[namespace][id]) return; 79     this.msgGroup[namespace][id] = []; 80   }, 81  82   //没有具体事件句柄便不能被移除 83   removeObserver: function (namespace, id, handler) { 84     var i, len, _arr; 85     if (!this.msgGroup[namespace]) return; 86     if (!this.msgGroup[namespace][id]) return; 87     _arr = this.msgGroup[namespace][id]; 88  89     for (i = 0, len = _arr.length; i < len; i++) { 90       if (_arr[i] === handler) _arr[id].splice(i, 1); 91     } 92   }, 93  94   //触发各个事件,事件句柄所处作用域需传入时自己处理 95   dispatch: function (namespace, id, data, scope) { 96     var i, len, _arr; 97  98     if (!(namespace && id)) return; 99 100     if (!this.msgGroup[namespace]) return;101     if (!this.msgGroup[namespace][id]) return;102     _arr = this.msgGroup[namespace][id];103 104     for (i = 0, len = _arr.length; i < len; i++) {105       if (_.isFunction(_arr[i])) _arr[i].call(scope || this, data);106     }107   }108 109 });110 111 Dalmatian.MessageCenter.getInstance = function () {112   if (!this.instance) {113     this.instance = new Dalmatian.MessageCenter();114   }115   return this.instance;116 };117 118 Dalmatian.MSG = Dalmatian.MESSAGECENTER = Dalmatian.MessageCenter.getInstance();

复制代码

完了这块我们怎么使用了,这里回到我们的alert与日历框,让我们实现他们之间的通信

alert与calendar之间的通信

我们实现这样的效果,点击alert框时,显示一个时间,并且日历上将此日期标红

PS:每次一到这个时间久累了,代码未做整理

① 我们在calendar实例化的时候便做事件注册(订阅)

复制代码

//事件注册点,应该单独封装Dalmatian.MSG.register('ui', 'alertShow', $.proxy(function (data) {  var s = '';}, this));

复制代码

② 在每次设置message内容时候便抛出事件

复制代码

 1 set: function (options) { 2   _.extend(this.adapter.datamodel, options); 3   //    this.adapter.datamodel.content = options.content; 4   this.adapter.notifyDataChanged(); 5  6   if (options.content) { 7     Dalmatian.MSG.dispatch('ui', 'alertShow', options.content); 8   } 9 10 },

复制代码

于是便有了这样的效果,每次设置值的时候,我这里都会被触发

而且这里的this指向的是calendar,所以我们这里可以做处理,由于时间原因,我这里便乱干了

可以看到,每次操作后,calendar得到了更新,但是由于我这里是直接操作的dom未做datamodel操作,所以没有做状态保存,第二次实际上是该刷新的

这里我们暂时不管

  1 <!doctype html>  2 <html lang="en">  3 <head>  4   <meta charset="UTF-8">  5   <title>ToDoList</title>  6   <meta name="viewport" content="width=device-width, initial-scale=1.0">  7   <link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/bootstrap/css/bootstrap.css">  8   <link rel="stylesheet" type="text/css" href="http://designmodo.github.io/Flat-UI/css/flat-ui.css">  9   <link href="../style/main.css" rel="stylesheet" type="text/css" /> 10   <style type="text/css"> 11     .cui-alert { width: auto; position: static; } 12     .txt { border: #cfcfcf 1px solid; margin: 10px 0; width: 80%; } 13     ul, li { padding: 0; margin: 0; } 14     .cui_calendar, .cui_week { list-style: none; } 15     .cui_calendar li, .cui_week li { float: left; width: 14%; overflow: hidden; padding: 4px 0; text-align: center; } 16   </style> 17 </head> 18 <body> 19   <article class="containerCalendar"> 20   </article> 21   <div style="border: 1px solid black; margin: 10px; padding: 10px; clear: both;"> 22     华丽的分割</div> 23   <article class="containerAlert"> 24   </article> 25   <input type="text" id="addmsg" class="txt"> 26   <button id="addbtn" class="btn"> 27     show message</button> 28   <script type="text/underscore-template" id="template-alert"> 29       <div class=" cui-alert" > 30         <div class="cui-pop-box"> 31           <div class="cui-bd"> 32             <p class="cui-error-tips"><%=content%></p> 33             <div class="cui-roller-btns"> 34               <div class="cui-flexbd cui-btns-cancel"><%=cancel%></div> 35               <div class="cui-flexbd cui-btns-sure"><%=confirm%></div> 36             </div> 37           </div> 38         </div> 39       </div> 40   </script> 41   <script type="text/template" id="template-calendar"> 42     <ul class="cui_week"> 43       <% var i = 0, day = 0; %> 44       <%for(day = 0; day < 7; day++) { %> 45       <li> 46         <%=weekDayItemTmpt[day] %></li> 47       <%} %> 48     </ul> 49  50     <ul class="cui_calendar"> 51       <% for(i = 0; i < beginWeek; i++) { %> 52         <li class="cui_invalid"></li> 53       <% } %> 54       <% for(i = 0; i < days; i++) { %> 55         <% day = i + 1; %> 56         <% if (compaign && compaign.days && compaign.sign && _.contains(compaign.days, day)) { day = compaign.sign } %> 57         <li class="cui_calendar_item" data-date="<%=year%>-<%=month + 1%>-<%=day%>"><%=day %></li> 58       <% } %> 59     </ul> 60   </script> 61   <script type="text/javascript" src="../../vendor/underscore-min.js"></script> 62   <script type="text/javascript" src="../../vendor/zepto.min.js"></script> 63   <script src="../../src/underscore.extend.js" type="text/javascript"></script> 64   <script src="../../src/util.js" type="text/javascript"></script> 65   <script src="../../src/message-center-wl.js" type="text/javascript"></script> 66   <script src="../../src/mvc.js" type="text/javascript"></script> 67   <script type="text/javascript"> 68  69     (function () { 70       var htmltemplate = $('#template-alert').html(); 71  72       var AlertView = _.inherit(Dalmatian.View, { 73         templateSet: { 74           0: htmltemplate 75         }, 76  77         statusSet: { 78           STATUS_INIT: 0 79         } 80       }); 81  82  83       var Adapter = _.inherit(Dalmatian.Adapter, { 84         parse: function (data) { 85           return data; 86         } 87       }); 88  89       var Controller = _.inherit(Dalmatian.ViewController, { 90         //设置默认信息 91         _initialize: function () { 92           this.origindata = { 93             content: '', 94             confirm: '确定', 95             cancel: '取消' 96           } 97         }, 98  99         initialize: function ($super, opts) {100           this._initialize();101           $super(opts);102           this._init();103         },104 105         //基础数据处理106         _init: function () {107           this.adapter.format(this.origindata);108           this.adapter.registerObserver(this);109           this.viewstatus = this.view.statusSet.STATUS_INIT;110         },111 112         render: function () {113           var data = this.adapter.viewmodel;114           this.view.render(this.viewstatus, data);115         },116 117         set: function (options) {118           _.extend(this.adapter.datamodel, options);119           //    this.adapter.datamodel.content = options.content;120           this.adapter.notifyDataChanged();121 122           //*****************************123           //淳敏看这样是否合理124           //*****************************125           if (options.content) {126             Dalmatian.MSG.dispatch('ui', 'alertShow', options.content);127           }128 129         },130 131         events: {132           "click .cui-btns-cancel": "cancelaction"133         },134 135         cancelaction: function () {136           this.onCancelBtnClick();137         }138       });139 140       var view = new AlertView()141       var adapter = new Adapter();142 143       var controller = new Controller({144         view: view,145         adapter: adapter,146         container: '.containerAlert',147         onCancelBtnClick: function () {148           alert('cancel 2')149         }150       });151 152       $('#addbtn').on('click', function (e) {153         var content = $('#addmsg').val();154         // adapter.datamodel.content = content;155         // adapter.notifyDataChanged();156         controller.set({ content: content, confirm: '确定1' });157         controller.show();158       });159 160     })();161 162     (function () {163 164       var tmpt = $('#template-calendar').html();165 166       var CalendarView = _.inherit(Dalmatian.View, {167         templateSet: {168           0: tmpt169         },170 171         statusSet: {172           STATUS_INIT: 0173         }174       });175 176       var CalendarAdapter = _.inherit(Dalmatian.Adapter, {177         _initialize: function ($super) {178           $super();179 180           //默认显示方案,可以根据参数修改181           //任意一个model发生改变皆会引起update182           this.weekDayItemTmpt = ['日', '一', '二', '三', '四', '五', '六'];183         },184 185         //该次重新,viewmodel的数据完全来源与parse中多定义186         parse: function (data) {187           return _.extend({188             weekDayItemTmpt: this.weekDayItemTmpt189           }, data);190         }191       });192 193       var CalendarController = _.inherit(Dalmatian.ViewController, {194 195         _initialize: function () {196           this.view = new CalendarView();197           this.adapter = new CalendarAdapter();198 199           //默认业务数据200           this.dateObj = new Date();201           this.container = '.containerCalendar';202 203           var s = '';204         },205 206         initialize: function ($super, opts) {207           this._initialize();208           $super(opts);209 210           //事件注册点,应该单独封装211           Dalmatian.MSG.register('ui', 'alertShow', $.proxy(function (data) {212             var date = new Date(data);213             this.handleDay(date, function (el) {214               //*****************215               //此处该做datamodel操作,暂时不予处理216               //*****************217               el.css('color', 'red');218             });219             220             var s = '';221 222           }, this));223 224         },225 226         onViewBeforeCreate: function () {227 228           //使用adpter之前必须注册监听以及格式化viewModel,此操作应该封装起来229           this.adapter.registerObserver(this);230           this.adapter.format(this._getMonthData(this.dateObj.getFullYear(), this.dateObj.getMonth()));231 232           //view显示之前必定会给予状态,此应该封装233           this.viewstatus = this.view.statusSet.STATUS_INIT;234 235           var s = '';236         },237 238         render: function () {239 240           //该操作可封装241           var data = this.adapter.viewmodel;242 243           console.log(data)244 245           this.view.render(this.viewstatus, data);246         },247 248         //根据传入年月,返回该月相关数据249         _getMonthData: function (year, month) {250           this.date = new Date(year, month);251           var d = new Date(year, month);252           //description 获取天数253           var days = dateUtil.getDaysOfMonth(d);254           //description 获取那个月第一天时星期几255           var _beginWeek = dateUtil.getBeginDayOfMouth(d);256           return {257             year: d.getFullYear(),258             month: d.getMonth(),259             beginWeek: _beginWeek,260             days: days,261             compaign: null262           };263         },264 265 266         handleDay: function (dateStr, fn) {267           if (dateUtil.isDate(dateStr)) dateStr = dateUtil.format(dateStr, 'Y-m-d');268           var el = this.viewcontent.find('[data-date="' + dateStr + '"]');269 270           if (typeof fn == 'function') fn(el, dateUtil.parse(dateStr, 'y-m-d'), this);271 272         }273 274       });275 276       var calendar = new CalendarController();277       calendar.show();278 279       calendar.handleDay(new Date(), function (el, date, calendar) {280         el.html('今天');281       });282 283     })();    284 285   </script>286 </body>287 </html>

View Code

结语

今天暂时到此,我们下次继续,本人技术有限,若是文中有任何不足以及问题,请提出

这里命名空间以及id皆有可能像滚雪球似的越滚越多,所以这里需要框架本身做出约定,具体看后期实践吧......

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP