前言
之前,我們形成了頁面片相關的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,今日到此結束。