前言
之前,我們形成了頁面片相關的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,如需轉載請自行聯系原作者