Extjs5.0(6):控制器(Controller、ViewController)和路由器(Router)
控制器
上一篇文章我們已經為專案添加了左側導航欄,接下來要為導航欄新增點選事件,點選左側導航欄,右側介面出現相應變化。
為元件新增事件,就要用到控制器了。Extjs5提供了兩種控制器:Controller和ViewController,這兩種控制器都繼承自BaseController。從功能上來講,兩者的大多數功能都是一致的,也就是說,在不嚴格區分的情況下,這兩者選哪個都能用。從作用範圍上講,Controller範圍比較大,屬於應用級別的,ViewController範圍比較小,屬於介面級別的,也就是說Controller一般用於控制整個app,ViewController一般用於控制app中的某一個view。
在Extjs4中,只有Controller。Extjs4 MVC中,Application一開始時,就載入所有的Controller(有些人用了其他的方法避免了這個問題,有興趣可以上網查下),其生命週期和我們的Application是一樣的。這裡就存在一些問題了,一般來講,Application開始執行時,我們只需要載入首頁的相關內容就行,其他頁面由於沒有顯示,就無需載入了。例如我們首頁是頁面A,在A中點選某個按鈕能跳到頁面B,比較好的做法是,B頁面的View和Controller我們可以在A中點選按鈕之後才載入,沒有必要再載入A頁面的同時也載入B頁面,但是前面講了,Application開始時,就載入了所有的Controller,無論當前有沒用到,都載入了,並且跟隨著Application的生命週期一同存在,並且Controller裡面還需要載入其控制的View,那麼就相當於Application一開始,就把所有東西都載入進來了,效能和效率都是比較差的。
在Extjs5中,針對於Controller存在的問題,新增了ViewController。ViewController無需在Application開始時就載入,只有在使用了某個View時,才在View中載入其對應的ViewController,所以說ViewController是控制頁面級別的。
根據前面的解釋,似乎我們為導航欄新增控制器,應該使用Controller。但是,這裡我還是使用ViewController。只要使用ViewController的View不銷燬,那麼ViewController也一樣一直存在於Application中,一樣能達到Controller的效果。所以,當我前面講的都是廢話吧。
為什麼不用Controller呢?在官方的文件中有這麼一段話“While Ext JS 5 is backwards compatible with current controllers, itintroduces a new type of controller designed to handle these challenges:Ext.app.ViewController.”,我翻譯為:“Ext JS 5在向後相容目前的控制器的同時,也引入了一個新的控制器設計來處理這些挑戰(我們前面講的Controller存在的問題):Ext.app.ViewController”,並且我們發現,在用senchacmd5 生成的Extjs5專案結構中,並沒有controller這個包,而在extjs 4的專案中,是有這個包的。所以,我認為,Controller之所以存在,是為了相容舊版本,新版本中,我們應該使用ViewController。但是有一點需要說明的是,ViewController對Controller做了簡化,有些方法Controller裡有而ViewController裡沒有,所以不排除有些時候你只能用Controller。
下面開始為導航欄新增ViewController,監聽樹節點的點選事件。
在我們的main包中,已經有一個ViewController:MainController,我們這裡就不用重複建立,直接使用這個ViewController。
修改MainController.js如下:
Ext.define('MyApp.view.main.MainController', {
extend: 'Ext.app.ViewController',
requires: [
'Ext.window.MessageBox',
'MyApp.store.main.Navigation'
],
alias: 'controller.main',
control: {
'app-navigation': {//元件別名,表示要控制的是該元件
selectionchange: 'onTreeNavSelectionChange'
}
},
onTreeNavSelectionChange:function(selModel, records){
var record = records[0];
alert(record.getId());
}
});
注意我們新增的這一段程式碼
control: {
'app-navigation': {//元件別名,表示要控制的是該元件
selectionchange: 'onTreeNavSelectionChange'
}
},
'app-navigation'是我們要控制的元件的別名,我們的導航欄的別名就是'app-navigation',所以這裡控制的是導航欄這個view。再看selectionchange,表示的我們要監聽treepanel(導航欄)的“選擇改變”事件,當改變選擇的樹節點時,會觸發該事件。後面的'onTreeNavSelectionChange'指的是觸發時使用的函式,這個函式我們定義在最下面
onTreeNavSelectionChange:function(selModel, records){
var record = records[0];
alert(record.getId());
}
所以我們當前為控制器增加的功能是:改變點選樹節點時,alert出所選擇的樹節點的id值。
控制器寫好之後,要把控制器引入到其要控制的view中,我們當前的控制器要控制的是導航欄(Navigation.js),我們可以把控制器引入到Navigation.js中,由於導航欄是屬於主介面(Main.js)中的,所以我們也可以把控制器引入到Main.js中,這裡我選擇後者,因為以後可能Header.js也需要監聽器,或者主介面中其他元件也要監聽,我們沒必要建立那麼多控制器,就把這些元件的控制事件都集中在一個控制器裡面就好,然後把控制器引入到這些元件的父介面上,就可以了。
觀察Main.js程式碼
Ext.define('MyApp.view.main.Main', {
extend: 'Ext.container.Container',
requires: [
'MyApp.view.main.MainController',//引入controller
'MyApp.view.main.MainModel',
'MyApp.view.main.Header',//引入Header
'MyApp.view.main.Navigation'//引入導航欄
],
xtype: 'app-main',
controller: 'main',//指定控制器
viewModel: {
type: 'main'
},
layout: {
type: 'border'
},
items: [
{
region: 'north',
xtype: 'app-header'//使用Header
}, {
xtype: 'app-navigation',
region: 'west'
}, {
region: 'center',
xtype: 'tabpanel',
items: [{
title: 'Tab 1',
html: '<h2>Content appropriate for the current navigation.</h2>'
}]
}]
});
我們在requires中載入'MyApp.view.main.MainController',同時用controller:'main'指定當前介面使用的控制器,’main’指的是控制器的別名,我們前面在定義控制器的時候,有這麼一段程式碼,就是建立別名的:alias:'controller.main'。這樣子就為我們的導航欄新增好點選事件了。
瀏覽器中開啟我們的專案,點選左側導航欄節點,效果圖
路由器
在一個正常的網站中,使用者從不同的頁面導航,點選連結跳轉到新的頁面,可以按瀏覽器上的“前進”“後退”按鈕跳轉到下一個或者上一個頁面,然而,在一個單頁面應用程式中,使用者的互動不載入一個新頁面,這時如果使用者點選後退按鈕,則會退出我們整個應用。路由器就是用來解決這個問題的。
路由器可以通過使用瀏覽器歷史堆疊來跟蹤應用程式狀態。路由器還允許深度連結到應用程式中,允許直接連線到一個特定的應用程式的一部分。說白了,路由器就是跟蹤應用的狀態,當點選瀏覽器“前進”或者“後退”按鈕時,路由器會使我們的應用跳轉到前一狀態或者後一狀態。
瀏覽器瀏覽網際網路使用URI由許多部分組成。讓我們看一個示例URI:
這應該比較熟悉。然而,你可能不認識#user/1234。這部分的URI被稱為“雜湊”或片段識別符號。雜湊的更多資訊,請參閱http://en.wikipedia.org/wiki/Fragment_identifier這個資源。這個雜湊為應用程式提供了一種方法來控制瀏覽器的歷史堆疊,而不用重新載入當前頁面。隨著雜湊的變化,瀏覽器新增整個URI到歷史堆疊,然後允許您使用瀏覽器的前進/後退按鈕來遍歷URI。例如,如果你更新雜湊為:
使用者可以單擊後退按鈕回到#user/ 1234雜湊,應用程式可以根據這個通知做相應編號。Extjs的路由器就是依賴於瀏覽器的允許應用程式狀態跟蹤和深度連結雜湊功能。
以上是我翻譯的官網上對路由器的解釋。總結而言就是:路由器可以使我們的應用程式在不重新載入頁面的情況下,通過瀏覽器上的“後退”“前進”按鈕來返回到前一步操作或者前進到下一步操作。
我們在MainController.js中使用路由器功能,當點選導航欄節點時,在我們瀏覽器的url地址後面加上#id,然後我們通過路由器讀取這個id,並做下一步操作。
修改MainController.js中onTreeNavSelectionChange這個函式為:
onTreeNavSelectionChange: function(selModel, records) {
var record = records[0];
if (record) {
this.redirectTo(record.getId());
}
}
重新整理瀏覽器頁面,點選左邊樹節點,會發現我們的位址列變了
這時候可以點選瀏覽器的“前進”“後退”按鈕,會發現位址列跟著變。
接下來根據地址欄的變化,控制我們頁面的變化,MainController.js中加入以下程式碼。
routes : {
':id': 'handleRoute'//執行路由
},
繼續新增'handleRoute'函式
handleRoute : function(id) {
console.log("action"+id);
}
這時候重新整理瀏覽器頁面,開啟瀏覽器控制檯,再點選我們應用中導航欄的某個節點,觀察控制檯列印的資訊
說明我們已經捕捉到瀏覽器中地址的變化,並且拿到這個id值。我們要實現的功能是,把取到的id值作為別名,通過這個別名來建立相應的元件,放在右側的內容panel裡面。這樣做會遇到一個問題,就是如果沒有以此id為別名的元件,那麼跳轉過去就會出錯。所以,我們在路由跳轉時應該分兩步操作,第一步判斷是否有此元件,有的話則跳轉,沒有的話,則不跳轉。
修改MainController.js中的routes
routes : {
':id': {
action: 'handleRoute',//執行跳轉
before: 'beforeHandleRoute'//路由跳轉前操作
}
},
路由跳轉前執行'beforeHandleRoute'函式,先判斷瀏覽器位址列中#號後面的id引數,是否在左側導航欄有節點的id與之相同,如果有,則繼續執行,如果沒有,跳轉到原始介面(剛進入專案時的介面)。
beforeHandleRoute: function(id, action) {
var me = this,
store =Ext.StoreMgr.get('navigation');
var node = store.getNodeById(id);
if (node) {
//resume action
action.resume();
} else if(store.getCount() === 0){
action.stop();
me.redirectTo(id);
}else {
Ext.Msg.alert(
'路由跳轉失敗',
'找不到id為' + id + ' 的元件. 介面將跳轉到應用初始介面',
function() {
me.redirectTo('all');
}
);
//stop action
action.stop();
}
},
注意三個地方,第一,我們查詢樹節點時,是通過storeId查詢到樹的store,然後再通過路由傳過來的id獲取這個節點,所以我們要先為treestore定義一個storeId,這樣才能查詢到。
修改MainModel.js,為navigationStore新增storeId
Ext.define('MyApp.view.main.MainModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.main',
requires:['MyApp.store.main.Navigation'],//引入必要檔案
data: {
navigationTitle: '導航欄'//導航欄標題
},
stores:{
navigationStore:{
type:'navigation',//導航欄treestore
storeId:'navigation'
}
}
});
第二個注意的地方就是這段程式碼“else if(store.getCount() === 0)”,這裡多了這一步判斷主要是因為在路由跳轉過程中,有可能出現store還未載入,就已經開始判斷是否有這個樹節點,這時候肯定是沒有這個節點的,所以如果store的資料數量為0,則表示未載入store,就停止當前跳轉,重新定向。
第三個要注意的地方就是這段程式碼“me.redirectTo('all')”,表示找不到對應元件時,路由器時跳轉到’all’,我們這裡把all這個路由地址表示為我們的原始頁面,也就是說,瀏覽器地址最後如果是#all的話,則跳轉到原始介面,那麼我們在應用開始時,也應該設定預設路由為’all’。
修改Application.js
Ext.define('MyApp.Application', {
extend: 'Ext.app.Application',
name: 'MyApp',
stores: [
// TODO: add global / shared stores here
],
launch: function () {
},
init: function() {
var me = this;
//設定預設路由
me.setDefaultToken('all');
}
});
這樣設定之後,我們一開始執行應用時,位址列會在後面有個#all,來標誌當前狀態為初始狀態。
瀏覽器中執行專案,會發現瀏覽器位址列的改變。
同時也觸發了提示資訊
由於當前我們左側樹節點沒有id為all的節點,所以就觸發了警告,我們可以為樹節點的根節點新增id,設定為all。
修改store/main/Navigation.js
Ext.define('MyApp.store.main.Navigation', {
extend: 'Ext.data.TreeStore',
alias: 'store.navigation',
proxy: {
type: 'ajax',
url: 'resources/data/Navigation.json'
},
root: {
text: 'All',
id: 'all',
expanded: true
}
});
再重新整理頁面,發現警告沒了。
接下來繼續修改修改MainController.js,新增'handleRoute'函式,該函式為路由跳轉成功後的操作,如果當前點選的是左側導航欄的葉子節點,右側panel則跳轉到葉子節點id對應的元件,如果點選的是非葉子節點,右側panel則跳轉到另外的用於導航的介面上。這裡我們先做簡單的響應,具體的響應,下篇部落格再介紹。
handleRoute: function(id) {
var me = this,
store = Ext.StoreMgr.get('navigation'),
node = store.getNodeById(id);
if(node.isLeaf()){
Ext.Msg.alert(
'提示',
'當前點選的是葉子節點,右側panel將跳轉到對應的元件上');
}else{
Ext.Msg.alert(
'提示',
'當前點選的是非葉子節點,右側panel將跳轉到導航介面上');
}
}
完整的MainController.js程式碼:
Ext.define('MyApp.view.main.MainController', {
extend: 'Ext.app.ViewController',
requires: [
'Ext.window.MessageBox',
'MyApp.store.main.Navigation'
],
alias: 'controller.main',
control: {
'app-navigation': {//元件別名,表示要控制的是該元件
selectionchange: 'onTreeNavSelectionChange'
}
},
routes : {
':id': {
action: 'handleRoute',//執行跳轉
before: 'beforeHandleRoute'//路由跳轉前操作
}
},
onTreeNavSelectionChange: function(selModel, records) {
var record = records[0];
if (record) {
this.redirectTo(record.getId());
}
},
beforeHandleRoute: function(id, action) {
var me = this,
store =Ext.StoreMgr.get('navigation');
var node = store.getNodeById(id);
if (node) {
//resume action
action.resume();
} else if(store.getCount() === 0){
action.stop();
me.redirectTo(id);
}else {
Ext.Msg.alert(
'路由跳轉失敗',
'找不到id為' + id + ' 的元件. 介面將跳轉到應用初始介面',
function() {
me.redirectTo('all');
}
);
//stop action
action.stop();
}
},
handleRoute: function(id) {
var me = this,
store = Ext.StoreMgr.get('navigation'),
node = store.getNodeById(id);
if(node.isLeaf()){
Ext.Msg.alert(
'提示',
'當前點選的是葉子節點,右側panel將跳轉到對應的元件上');
}else{
Ext.Msg.alert(
'提示',
'當前點選的是非葉子節點,右側panel將跳轉到導航介面上');
}
}
});
執行一下,點選左側導航欄不同的樹節點
下篇部落格介紹點選左側導航欄,右側跳轉到相應介面的具體操作。
另外,我也把這個專案託管在github上:https://github.com/likeadog/Extjs5.0Demo