【單頁應用之通訊機制】view之間應該如何通訊

前言

在單頁應用中,view與view之間的通訊機制一直是一個重點,由於單頁應用的全部操做以及狀態管理所有發生在一個頁面上javascript

沒有很好的組織的話很容易就亂了,就算表面上看起來沒有問題,事實上會有各類隱憂,各類坑等着你去跳css

最初就沒有必定理論上的支撐,極有多是這麼一種狀況:html

① 需求下來了,搞一個demo作交待java

② 發現基本知足很滿意,因而直接在demo中作調整git

上面的作法自己沒有什麼問題,問題發生在後期github

③ 在demo調整後應用到了實際業務中,發現不少地方有問題,因而見一個坑解決一個坑bootstrap

④ 到最後感受整個框架零零散散,有不少if代碼,有不少代碼不太懂意思,可是一旦移除就報錯閉包

這個時候咱們就想到了重構,重構過程當中就會發現最初的設計,或者說整個框架的基礎有問題,因而就提出推翻重來mvc

如果時間上容許,還能夠,可是每每重構過程當中,會多一些不按套路出牌的同窗,將API接口給換了,這一換全部的業務系統所有崩潰app

因此說,新的框架會對業務線形成壓力,會提升測試與編碼成本,因而就回到了咱們上篇博客的問題

【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皆有可能像滾雪球似的越滾越多,因此這裏須要框架自己作出約定,具體看後期實踐吧......

相關文章
相關標籤/搜索