【單頁應用】全局控制器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,今日到此結束。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM