zoukankan      html  css  js  c++  java
  • 【单页应用之通信机制】view之间应该如何通信

    前言

    在单页应用中,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: tmpt
    169         },
    170 
    171         statusSet: {
    172           STATUS_INIT: 0
    173         }
    174       });
    175 
    176       var CalendarAdapter = _.inherit(Dalmatian.Adapter, {
    177         _initialize: function ($super) {
    178           $super();
    179 
    180           //默认显示方案,可以根据参数修改
    181           //任意一个model发生改变皆会引起update
    182           this.weekDayItemTmpt = ['', '', '', '', '', '', ''];
    183         },
    184 
    185         //该次重新,viewmodel的数据完全来源与parse中多定义
    186         parse: function (data) {
    187           return _.extend({
    188             weekDayItemTmpt: this.weekDayItemTmpt
    189           }, 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: null
    262           };
    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皆有可能像滚雪球似的越滚越多,所以这里需要框架本身做出约定,具体看后期实践吧......

  • 相关阅读:
    性能测试分析
    常见的性能缺陷
    性能测试中TPS上不去的几种原因浅析
    Linux新增和删除环境变量
    JProfiler的详细使用介绍
    详解Tomcat的连接数和线程池
    造数据存储过程
    shell脚本解压多个jar包
    使用shell快速建立上万个文件夹
    df、du命令
  • 原文地址:https://www.cnblogs.com/yexiaochai/p/3721802.html
Copyright © 2011-2022 走看看