天天看點

【單頁應用】全局控制器app應該幹些什麼?

前言

之前,我們形成了頁面片相關的mvc結構,但是該結構還僅适用于view(頁面)級,那麼真正的全局控制器app應該幹些什麼事情呢?我覺得至少需要幹這些:

功能點

① 提供URL解析機制,以便讓控制器可以根據URL獲得目前是要加載哪個view的執行個體,比如

http://www.baidu.com/index.html#index

http://www.baidu.com/index

若是使用hashChange實作浏覽器跳轉便直接取出index這個鍵值;

若是使用pushState方案的話,便需要業務同僚給出取出URL鍵值的方法,最終我們需要得到index這個鍵值

② app應該保留各個view的執行個體,并且維護一個隊列關系

以現在部落格園為例,我們可能具有兩個view頁面片:index->detail

我們首次便是加載index這個view,點選其中一個項目便加載detail這個view,這個時候app是應該同時儲存兩個view,并且内部要維系一個通路順序隊列

這個隊列最好可與浏覽器儲存一緻,若不能儲存一緻,後期便可能會出現點選浏覽器後退死循環的問題

③ app應該提供view執行個體化的方法

是以的view執行個體若無特殊原因,皆應該由app生成,app應該具有執行個體化view的能力,view一般使用AMD規範管理,這裡涉及異步加載

PS:真實工作環境中,view需要自建一套事件機制,比如執行個體化時候要觸發什麼事件,顯示時候要觸發什麼事件,皆需要有,app隻會負責

執行個體化->顯示->隐藏

④ app應該提供監控浏覽器事件,每次自動加載各個view

如上面所述,app會注冊一個hashChange事件或者popState事件以達到改變URL不重新整理頁面的功能,這個功能主要用于使用者點選浏覽器原生後退鍵

以上便是全局控制器app該幹的事情,按程式邏輯說,應該是這樣的

程式邏輯

使用者鍵入一個URL,進到一個單頁應用,于是首次會發生以下事情:

① 屬性初始化,并且為浏覽器綁定hashChange/popState事件

② 解析URL取出,目前需要加載的VIEW鍵值,一般而言是index,或者會有一些參數

③ 根據鍵值使用requireJS文法加載view類,并且産生執行個體化操作

④ 執行個體化結束後,便調用view的show方法,首屏view顯示結束,内部會觸發view自身事件達到頁面渲染的效果

使用者點選其中一個項目會觸發一個類似forward/back的操作,這個時候流程會有所不同:

① app首先會屏蔽監控浏覽器的變化,因為這個是使用者主動觸發,不應該觸發hashChange類似事件

② app開始加載forward的view,這裡比如是list,将list執行個體化,然後執行index的hide方法,執行list的show方法,這裡便完成了一次view的切換

整個邏輯還可能發生動畫,我們這裡暫時忽略。

這時當使用者點選浏覽器後退,情況又會有所不同

① app中的hashChange或者popstate會捕捉到這次URL變化

② app會解析這個URL并且安裝之前約定取出鍵值,這個時候會發現app中已經儲存了這個view的執行個體

③ 直接執行list view的hide方法,然後執行index view的show方法,整體邏輯結束

整個app要幹的事情基本就是這樣,這種app邏輯一般為3-7百行,代碼少,但是其實作的功能比較複雜,往往是一個單頁應用的核心!

Backbone的控制器

事實上Backbone隻有一個History,并不具有控制器的行為,總的來說,Backbone最為有用的就是其view一塊的邏輯,我們很多時候也隻是需要這段邏輯

其路由功能本身沒有什麼問題,實作也很好,但是我們可以看到他并未完成我們以上需要的功能,是以對我來說,他便隻是一個簡單的路由功能,不是控制器

Backbone的路由首先會要求你将一個應用中的所有url與鍵值全部做一個映射,比如

 1 var App = Backbone.Router.extend({

 2   routes: {

 3     "": "index",    // #index

 4     "index": "index",    // #index

 5     "detail": "detail"    // #detail

 6   },

 7   index: function () {

 8     var index = new Index(this.interface);

 9 

10   },

11   detail: function () {

12     var detail = new Detail(this.interface);

13 

14   },

15   initialize: function () {

16 

17   },

18   interface: {

19     forward: function (url) {

20       window.location.href = ('#' + url).replace(/^#+/, '#');

21     }

22 

23   }

24 

25 });

然後整體的功能完全依賴于URL的變化觸發,那麼意味着一個單頁應用中所有的url我都需要在此做映射,我這裡當然不願意這樣做

事實上我做變化時候,隻需要一個view類的鍵值即可,是以我們這裡便直接跳過了路由映射這個邏輯

每次浏覽器主動發生的變化,我們直接解析其URL,拿出我們要的view 鍵值,進而加載這個view的執行個體

我們的控制器

根據前面的想法,我們的控制器一定會包含以下接口:

① 解析URL形成view鍵值的接口,并且該接口可被各業務覆寫:

getViewIdRules

② 異步加載View類以及執行個體化view的接口

loadView

③ 浏覽器事件監聽

buildEvent(hashChange/popState)

以上是幾個關鍵接口,其它接口,如view切換也需要提出,這裡我們首先得得出整個app的時序

其時序簡單分為三類,其實還有更加複雜的情況,我們這裡暫時不予考慮

① 首先是初始化的操作,首次便隻需要解析URL,加載預設view執行個體并且顯示即可,這個時候雖然注冊了hashChange/popState事件,不會觸發其中邏輯

② 其次是架構主動行為,主動要加載第二個view(view),這個時候便會執行個體化之,然後觸發自身switchview事件,切換兩個view

③ 最後是浏覽器觸發hashChange/popState事件,導緻架構發生切換view的事件,這個時候兩個view執行個體已經存在,是以隻需要切換即可

PS:每次架構隻需要執行簡單的show、hide方法即可,view内部自有其邏輯處理餘下事情,這些我們留待後面說

時序圖,出來後,我們就要考慮我們這個全局控制器app,的方法了,這裡先給出類圖再做一一實作:

這裡做初步的實作:

  1 "use strict";

  2 var Application = _.inherit({

  3 

  4   //設定預設的屬性

  5   defaultPropery: function () {

  6 

  7     //存儲view隊列的hash對象,這裡會建立一個hash資料結構,暫時不予理睬

  8     this.views = new _.Hash();

  9 

 10     //目前view

 11     this.curView;

 12 

 13     //最後通路的view

 14     this.lastView;

 15 

 16     //各個view的映射位址

 17     this.viewMapping = {};

 18 

 19     //本地維護History邏輯

 20     this.history = [];

 21 

 22     //是否開啟路由監控 

 23     this.isListeningRoute = false;

 24 

 25     //view的根目錄

 26     this.viewRootPath = 'app/views/';

 27 

 28     //目前對應url請求

 29     this.request = {};

 30 

 31     //目前對應的參數

 32     this.query = {};

 33 

 34     //pushState的支援能力

 35     this.hasPushState = !!(this.history && this.history.pushState);

 36 

 37     //由使用者定義的擷取viewid規則

 38     this.getViewIdRules = function (url, hasPushState) {

 39       return _.getUrlParam(url, 'viewId');

 40     };

 41 

 42   },

 43 

 44   //@override

 45   handleOptions: function (opts) {

 46     _.extend(this, opts);

 47   },

 48 

 49   initialize: function (opts) {

 50 

 51     this.defaultPropery();

 52     this.handleOptions(opts);

 53 

 54     //構造系統各個事件

 55     this.buildEvent();

 56 

 57     //首次動态調用,生成view

 58     this.start();

 59   },

 60 

 61   buildEvent: function () {

 62     this._requireEvent();

 63     this._routeEvent();

 64   },

 65 

 66   _requireEvent: function () {

 67     requirejs.onError = function (e) {

 68       if (e && e.requireModules) {

 69         for (var i = 0; i < e.requireModules.length; i++) {

 70           console.log('抱歉,目前的網絡狀況不給力,請重新整理重試!');

 71           break;

 72         }

 73       }

 74     };

 75   },

 76 

 77   //路由相關處理邏輯,可能是hash,可能是pushState

 78   _routeEvent: function () {

 79 

 80     //預設使用pushState邏輯,否則使用hashChange,後續出pushState的方案

 81     $(window).bind('hashchange', _.bind(this.onURLChange, this));

 82 

 83   },

 84 

 85   //當URL變化時

 86   onURLChange: function () {

 87     if (!this.isListeningRoute) return;

 88 

 89   },

 90 

 91   startListeningRoute: function () {

 92     this.isListeningRoute = true;

 93   },

 94 

 95   stopListeningRoute: function () {

 96     this.isListeningRoute = false;

 97   },

 98 

 99   //解析的目前url,并且根據getViewIdRules生成目前viewID

100   parseUrl: function (url) {

101 

102   },

103 

104   //入口點

105   start: function () {

106     var url = decodeURIComponent(window.location.hash.replace(/^#+/i, '')).toLowerCase();

107     this.history.push(window.location.href);

108     //處理目前url,會将viewid寫入request對象

109     this.parseUrl(url);

110 

111     var viewId = this.request.viewId;

112 

113     //首次不會觸發路由監聽,直接程式導入

114     this.switchView(viewId);

115 

116   },

117 

118   //根據viewId判斷目前view是否執行個體化

119   viewExist: function (viewId) {

120     return this.views.exist(viewId);

121   },

122 

123   //根據viewid,加載view的類,并會執行個體化

124   //注意,這裡隻會傳回一個view的執行個體,并不會顯示或者怎樣,也不會執行app的邏輯

125   loadView: function (viewId, callback) {

126 

127     //每個鍵值還是在全局views保留一個存根,若是已經加載過便不予理睬

128     if (this.viewExist(viewId)) {

129       _.callmethod(callback, this, this.views.get(viewId));

130       return;

131     }

132 

133     requirejs([this._buildPath(viewId)], $.proxy(function (View) {

134       var view = new View();

135 

136       this.views.push(viewId, view);

137 

138       //将目前view執行個體傳入,執行回調

139       _.callmethod(callback, this, view);

140 

141     }, this));

142   },

143 

144   //根據viewId生成路徑

145   _buildPath: function (viewId) {

146     return this.viewMapping[viewId] ? this.viewMapping[viewId] : this.viewRootPath + viewId;

147   },

148 

149   //注意,此處的url可能是id,也可能是其它莫名其妙的,這裡需要進行解析

150   forward: function (viewId) {

151 

152     //解析viewId邏輯暫時省略

153     //......

154     this.switchView(viewId);

155 

156   },

157 

158   //後退操作

159   back: function () {

160 

161   },

162 

163   //view切換,傳入要顯示和隐藏的view執行個體

164   switchView: function (viewId) {

165     if (!viewId) return;

166 

167     this.loadView(viewId, function (view) {

168       this.lastView = this.curView;

169       this.curView = view;

170 

171       if (this.curView) this.curView.show();

172       if (this.lastView) this.lastView.show();

173 

174     });

175   }

176 

177 });

結語

今天,我們一起分析了全局控制器app應該做些什麼,并且整理了下基本思路,那麼我們這個星期的主要目的便是實作這個app,今日到此結束。

本文轉自葉小钗部落格園部落格,原文連結:http://www.cnblogs.com/yexiaochai/p/3764376.html,如需轉載請自行聯系原作者

繼續閱讀