1. 程式人生 > >《Ext JS 6.2實戰》節選——遷移管理模版

《Ext JS 6.2實戰》節選——遷移管理模版

Ext JS 6的示例中,提供瞭如圖7-1所示的管理模版和如7-2所示的Executive模版。這兩套模版都採用了當前流行的簡潔大氣風格,且都是響應式設計,是大家比較喜歡的型別。尤其是管理模版,可以說是當前最流行的後臺管理模版,非常適合用來開發應用程式,但比較遺憾的是,目前想通過Sencha Cmd直接使用模版來建立應用程式還是比較困難,只能通過手動遷移的方式來移植模版。還好,這也不是難事,當成一次學習之旅就行了。本章的主要目的就是介紹如何移植管理模版。

7.1 管理模版簡介

在遷移之前,最好是先熟悉一下管理模版的工作模式,這樣移植起立才能得心應手。而要了解管理模版的工作模式,最好的途徑是跟著應用程式的程式碼走一遍流程。與Ext JS的其他應用程式一樣,管理模版的入口是app.js檔案,我們就從這裡開始我們的旅途。

7.1.1 App.js

在Ext JS框架庫的templates\admin-dashboard資料夾下可以找到管理模版的全部原始碼,在該資料夾下開啟App.js,將看到如程式碼清單7-1所示的程式碼。
程式碼清單7-1 App.js

Ext.application({
    name: 'Admin',

    extend: 'Admin.Application',

    requires: [
        'Admin.*'
    ]
});

程式碼中最大的亮點就是使用“Admin.*”來引用應用程式所有的類,這種方式雖然方便,但並沒有順序,是根據資料夾結構依次遍歷檔案得出的結果,很容易造成錯誤,因而,要使用這種方法來引用類,一定要謹慎。
在類中沒有提供更多的資訊,只能去研究它的父類Admin.Application。

7.1.2 Application.js

由於管理模版是使用通用應用程式模式開發的,因而,在app資料夾下是找不到Application.js的。我們使用的是經典模式,需要開啟classic\src資料夾下的Application.js,檔案開啟後,將看到如程式碼清單7-2所示的程式碼。
程式碼清單7-2 Application.js

Ext.define('Admin.Application', {
    extend: 'Ext.app.Application',

    name: 'Admin',

    stores: [
        'NavigationTree'
], defaultToken : 'dashboard', mainView: 'Admin.view.main.Main', onAppUpdate: function () { Ext.Msg.confirm('Application Update', 'This application has an update, reload?', function (choice) { if (choice === 'yes') { window.location.reload(); } } ); } });

在程式碼中,引用了一個名為導航樹(NavigationTree)的儲存,還定義了預設的令牌(defaultToken)dashborad。
應用程式的主檢視(mainView)則定義為類Admin.view.main.Main,也就是說,一切都從這個主檢視開始的。

7.1.3 主檢視:Admin.view.main.Main

開啟classic\src\view\main資料夾下的Main.js,將看到如程式碼清單7-3所示的程式碼。
程式碼清單7-3 Main.js

Ext.define('Admin.view.main.Main', {
    extend: 'Ext.container.Viewport',

    requires: [
        'Ext.button.Segmented',
        'Ext.list.Tree'
    ],

    controller: 'main',
    viewModel: 'main',

    cls: 'sencha-dash-viewport',
    itemId: 'mainView',

    layout: {
        type: 'vbox',
        align: 'stretch'
    },

    listeners: {
        render: 'onMainViewRender'
    },

    items: [
        {
            xtype: 'toolbar',
            cls: 'sencha-dash-dash-headerbar shadow',
            height: 64,
            itemId: 'headerBar',
            items: [
                {
                    xtype: 'component',
                    reference: 'senchaLogo',
                    cls: 'sencha-logo',
                    html: '<div class="main-logo"><img src="resources/images/company-logo.png">Sencha</div>',
                    width: 250
                },
                {
                    margin: '0 0 0 8',
                    ui: 'header',
                    iconCls:'x-fa fa-navicon',
                    id: 'main-navigation-btn',
                    handler: 'onToggleNavigationSize'
                },
                '->',
                {
                    xtype: 'segmentedbutton',
                    margin: '0 16 0 0',

                    platformConfig: {
                        ie9m: {
                            hidden: true
                        }
                    },

                    items: [{
                        iconCls: 'x-fa fa-desktop',
                        pressed: true
                    }, {
                        iconCls: 'x-fa fa-tablet',
                        handler: 'onSwitchToModern',
                        tooltip: 'Switch to modern toolkit'
                    }]
                },
                {
                    iconCls:'x-fa fa-search',
                    ui: 'header',
                    href: '#searchresults',
                    hrefTarget: '_self',
                    tooltip: 'See latest search'
                },
                {
                    iconCls:'x-fa fa-envelope',
                    ui: 'header',
                    href: '#email',
                    hrefTarget: '_self',
                    tooltip: 'Check your email'
                },
                {
                    iconCls:'x-fa fa-question',
                    ui: 'header',
                    href: '#faq',
                    hrefTarget: '_self',
                    tooltip: 'Help / FAQ\'s'
                },
                {
                    iconCls:'x-fa fa-th-large',
                    ui: 'header',
                    href: '#profile',
                    hrefTarget: '_self',
                    tooltip: 'See your profile'
                },
                {
                    xtype: 'tbtext',
                    text: 'Goff Smith',
                    cls: 'top-user-name'
                },
                {
                    xtype: 'image',
                    cls: 'header-right-profile-image',
                    height: 35,
                    width: 35,
                    alt:'current user image',
                    src: 'resources/images/user-profile/2.png'
                }
            ]
        },
        {
            xtype: 'maincontainerwrap',
            id: 'main-view-detail-wrap',
            reference: 'mainContainerWrap',
            flex: 1,
            items: [
                {
                    xtype: 'treelist',
                    reference: 'navigationTreeList',
                    itemId: 'navigationTreeList',
                    ui: 'navigation',
                    store: 'NavigationTree',
                    width: 250,
                    expanderFirst: false,
                    expanderOnly: false,
                    listeners: {
                        selectionchange: 'onNavigationTreeSelectionChange'
                    }
                },
                {
                    xtype: 'container',
                    flex: 1,
                    reference: 'mainCardPanel',
                    cls: 'sencha-dash-right-main-container',
                    itemId: 'contentPanel',
                    layout: {
                        type: 'card',
                        anchor: '100%'
                    }
                }
            ]
        }
    ]
});

從配置項extend可以知道,主檢視是一個視區容器(Ext.container.Viewport),也就是把BODY元素作為應用程式的頂層元素,並附加了容器的功能。
在視區中,先將視區使用垂直盒子佈局(Ext.layout.container.VBox)將視區劃分為上下兩部分,在頂部是一個高度為64畫素的工具欄,在底部是一個封裝了的主容器。在主容器內,又使用水平盒子佈局(Ext.layout.container.HBox)將容器劃分了左右兩個區域,在左邊的是一個寬度為250畫素的導航樹,而在右邊是使用了卡片佈局(Ext.layout.container.Card)的內容面板。
在工具欄內,從左至右依次是Logo圖示、導航欄切換按鈕、平臺切換按鈕、查詢圖示、電子郵件圖示、問答圖示、配置圖示、使用者名稱和使用者頭像。
在主檢視內,還為主檢視綁定了render事件,該事件會在主檢視渲染後觸發。而在導航樹內,繫結選擇改變(selectionchange)事件。工具欄的各按鈕,也相應的綁定了各自的單擊事件。
在主檢視內,並沒有定義事件所繫結的方法,說明這些方法都是在檢視控制器內定義的,這個我們等下再研究,現在先來研究主容器。

7.1.4 主容器:Admin.view.main.MainContainerWrap

開啟classic\src\view\main資料夾下的MainContainerWrap.js檔案,將看到如程式碼清單7-4所示的程式碼。
程式碼清單7-4 Admin.view.main.MainContainerWrap

Ext.define('Admin.view.main.MainContainerWrap', {
    extend: 'Ext.container.Container',
    xtype: 'maincontainerwrap',

    requires : [
        'Ext.layout.container.HBox'
    ],

    scrollable: 'y',

    layout: {
        type: 'hbox',
        align: 'stretchmax',

        animate: true,
        animatePolicy: {
            x: true,
            width: true
        }
    },

    beforeLayout : function() {

        var me = this,
            height = Ext.Element.getViewportHeight() - 64,  // offset by topmost toolbar height
            navTree = me.getComponent('navigationTreeList');

        me.minHeight = height;

        navTree.setStyle({
            'min-height': height + 'px'
        });

        me.callParent(arguments);
    }
});

在主容器內,為佈局的調整定義了動畫,還重寫了beforeLayout方法。方法beforeLayout的主要作用是將主容器和導航樹的最小高度設定為視區高度減去工具欄(64畫素)後餘下的高度,這樣,導航樹和主容器的高度就不會只侷限於視區的高度,當他們的高度超出最小高度後,就會出現滾動條,可通過滾動來檢視隱藏的區域。

7.1.5 主檢視控制器:Admin.view.main.MainController

開啟classic\src\view\main\MainController.js檔案,會看到如程式碼清單7-5所示的程式碼。
程式碼清單7-5 Admin.view.main.MainController

Ext.define('Admin.view.main.MainController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.main',

    listen : {
        controller : {
            '#' : {
                unmatchedroute : 'onRouteChange'
            }
        }
    },

    routes: {
        ':node': 'onRouteChange'
    },

    lastView: null,

    setCurrentView: function(hashTag) {
        hashTag = (hashTag || '').toLowerCase();

        var me = this,
            refs = me.getReferences(),
            mainCard = refs.mainCardPanel,
            mainLayout = mainCard.getLayout(),
            navigationList = refs.navigationTreeList,
            store = navigationList.getStore(),
            node = store.findNode('routeId', hashTag) ||
                   store.findNode('viewType', hashTag),
            view = (node && node.get('viewType')) || 'page404',
            lastView = me.lastView,
            existingItem = mainCard.child('component[routeId=' + hashTag + ']'),
            newView;

        // Kill any previously routed window
        if (lastView && lastView.isWindow) {
            lastView.destroy();
        }

        lastView = mainLayout.getActiveItem();

        if (!existingItem) {
            newView = Ext.create({
                xtype: view,
                routeId: hashTag,  // for existingItem search later
                hideMode: 'offsets'
            });
        }

        if (!newView || !newView.isWindow) {
            // !newView means we have an existing view, but if the newView isWindow
            // we don't add it to the card layout.
            if (existingItem) {
                // We don't have a newView, so activate the existing view.
                if (existingItem !== lastView) {
                    mainLayout.setActiveItem(existingItem);
                }
                newView = existingItem;
            }
            else {
                // newView is set (did not exist already), so add it and make it the
                // activeItem.
                Ext.suspendLayouts();
                mainLayout.setActiveItem(mainCard.add(newView));
                Ext.resumeLayouts(true);
            }
        }

        navigationList.setSelection(node);

        if (newView.isFocusable(true)) {
            newView.focus();
        }

        me.lastView = newView;
    },

    onNavigationTreeSelectionChange: function (tree, node) {
        var to = node && (node.get('routeId') || node.get('viewType'));

        if (to) {
            this.redirectTo(to);
        }
    },

    onToggleNavigationSize: function () {
        var me = this,
            refs = me.getReferences(),
            navigationList = refs.navigationTreeList,
            wrapContainer = refs.mainContainerWrap,
            collapsing = !navigationList.getMicro(),
            new_width = collapsing ? 64 : 250;

        if (Ext.isIE9m || !Ext.os.is.Desktop) {
            Ext.suspendLayouts();

            refs.senchaLogo.setWidth(new_width);

            navigationList.setWidth(new_width);
            navigationList.setMicro(collapsing);

            Ext.resumeLayouts(); // do not flush the layout here...

            // No animation for IE9 or lower...
            wrapContainer.layout.animatePolicy = wrapContainer.layout.animate = null;
            wrapContainer.updateLayout();  // ... since this will flush them
        }
        else {
            if (!collapsing) {
                // If we are leaving micro mode (expanding), we do that first so that the
                // text of the items in the navlist will be revealed by the animation.
                navigationList.setMicro(false);
            }

            // Start this layout first since it does not require a layout
            refs.senchaLogo.animate({dynamic: true, to: {width: new_width}});

            // Directly adjust the width config and then run the main wrap container layout
            // as the root layout (it and its chidren). This will cause the adjusted size to
            // be flushed to the element and animate to that new size.
            navigationList.width = new_width;
            wrapContainer.updateLayout({isRoot: true});
            navigationList.el.addCls('nav-tree-animating');

            // We need to switch to micro mode on the navlist *after* the animation (this
            // allows the "sweep" to leave the item text in place until it is no longer
            // visible.
            if (collapsing) {
                navigationList.on({
                    afterlayoutanimation: function () {
                        navigationList.setMicro(true);
                        navigationList.el.removeCls('nav-tree-animating');
                    },
                    single: true
                });
            }
        }
    },

    onMainViewRender:function() {
        if (!window.location.hash) {
            this.redirectTo("dashboard");
        }
    },

    onRouteChange:function(id){
        this.setCurrentView(id);
    },

    onSearchRouteChange: function () {
        this.setCurrentView('searchresults');
    },

    onSwitchToModern: function () {
        Ext.Msg.confirm('Switch to Modern', 'Are you sure you want to switch toolkits?',
                        this.onSwitchToModernConfirmed, this);
    },

    onSwitchToModernConfirmed: function (choice) {
        if (choice === 'yes') {
            var s = location.search;

            // Strip "?classic" or "&classic" with optionally more "&foo" tokens
            // following and ensure we don't start with "?".
            s = s.replace(/(^\?|&)classic($|&)/, '').replace(/^\?/, '');

            // Add "?modern&" before the remaining tokens and strip & if there are
            // none.
            location.search = ('?modern&' + s).replace(/&$/, '');
        }
    },

    onEmailRouteChange: function () {
        this.setCurrentView('email');
    }
});

在主檢視的檢視控制器內,首先看到的是listen配置項,根據API文件的說明,它是用來監聽在Ext JS中被稱為事件域(event domains)的其他事件源的。配置項listen裡的controller配置項說明這裡要監聽的是控制器事件域(Ext.app.domain.Controller)。在控制器事件域內,使用了#號萬用字元來選擇觸發事件的類。對於這個#號萬用字元,在API文件中找不到任何說明,只好求助於原始碼了。在Ext.app.domain.Controlle類的原始碼檔案 內找到以下與#號萬用字元相關的程式碼:

if (selector === '*') {
    result = true;
} else if (selector === '#') {
    result = !!target.isApplication;
} else if (this.idMatchRe.test(selector)) {
    result = target.getId() === selector.substring(1);
} else if (alias) {
    result = Ext.Array.indexOf(alias, this.prefix + selector) > -1;
}

從程式碼可以知道,只有isApplication屬性為true的類才符合要求。通過檔案查詢功能,查了一下原始碼,只有Ext.app.Application(packages\core\src\app\Application.js檔案)這個類中才有isApplication屬性,也就是說,只有Ext.app.Application或它的派生類觸發的事件才會被主檢視的檢視控制器接收。接下來是具體的事件了,在這裡綁定了unmatchedroute事件,在API文件中也找不到,又通過檔案查詢功能查了一下原始碼,在路由器(Ext.app.route.Router)中找到了,事件程式碼在onStateChange內,具體相關程式碼如下:

onStateChange : function (token) {
    var me = this,
        app = me.application,

        ......

        if (!matched && app) {
            app.fireEvent('unmatchedroute', token);
        }
    }
},

從程式碼中可以看到,觸發unmatchedroute事件的,不是路由器自己,而是路由器的application屬性所指定的物件。這個物件是不是就是Ext.app.Application類或它的派生類呢?切換回Ext.app.Application的程式碼,查詢Router,會在建構函式中找到以下程式碼:

Ext.app.route.Router.application = me;

終於真相大白了,原來Ext.app.Application是這樣和路由器搭上關係的,並觸發unmatchedroute事件的,剛才還在納悶,是Ext.app.Application觸發的事件,怎麼跑到路由器裡去了,原來是這樣實現的。
現在回頭看看路由器的說明,發現它是用來實現路由功能 的。而onStateChange方法,是用來實現路由響應的。
根據指南(Guides)中的說明,可以知道Ext JS的路由功能是基於地址的雜湊值 的,也就是說,當地址的雜湊值發生改變的時候,就會觸發unmatchedroute事件,執行onRouteChange方法。而在onRouteChange方法內,會直接跳到setCurrentView方法。
在setCurrentView方法內,先通過references找到內容面板和導航樹,再通過內容面板的getLayout方法或得它的佈局,通過導航樹的getStore方法獲得它的儲存。獲取到導航樹的儲存後,就使用儲存的findNode方法來尋找routeId欄位和viewType欄位與雜湊值相匹配的節點,如果節點沒有找到,則給view賦值page404,如果找到,就把欄位的viewType值賦值給view。餘下的3個區域性變數,根據名字大概可以知道他們的作用,lastview是用來記錄最後一次顯示的檢視的,existingItem則是用來查詢卡片佈局中是否存在routeId屬性與地址雜湊值相同的檢視,newView則是準備建立的新檢視。
初始化了區域性變數後,先判斷最後一個檢視存在,且是視窗型別(isWindow為true是視窗特有的標誌)的檢視,如果存在,則呼叫destroy方法銷燬它。
呼叫佈局的getActiveItem方法的作用是為了獲取卡片佈局內的活動檢視。這句程式碼的最終目的是要把卡片佈局的內的檢視作為最後一個檢視,以避免視窗檢視銷燬的影響。
如果卡片佈局內不存在routeId屬性與地址雜湊值相同的檢視,說明該檢視還沒建立,需要呼叫Ext.create方法來建立檢視,為檢視新增routeId屬性的目的是為了能通過雜湊值來查詢檢視,配置項hideMode的作用是將檢視的隱藏模式設定為偏移(offsets)模式。
如果新檢視(newView)不存在,說明檢視(existingItem的值)已存在,不需要新增到佈局中。又或者新檢視是視窗,也不需要新增到佈局中。如果檢視存在(existingItem),且不是佈局內當前的活動檢視,則呼叫setActiveItem方法將檢視(existingItem)設定為活動檢視。如果檢視不存在,則呼叫Add方法先新增新檢視(newView),再呼叫setActiveItem方法將新檢視設定為活動檢視。在新增和設定活動檢視前,呼叫了suspendLayouts方法來阻止佈局事件的觸發,等操作完成後,再呼叫resumeLayouts方法來恢復佈局事件,這樣的好處是可以避免過多佈局計算以提高渲染速度。
在檢視已設定為活動檢視之後,呼叫導航樹的setSelection方法來選中節點,以標出當前檢視所對應的導航節點。
設定好導航樹的節點後,通過isFocusable方法來判斷活動檢視是否具有焦點特性,如果有,則將焦點移動到活動檢視上。
最後是將活動檢視賦值給屬性lastview。
通過對setCurrentView方法的理解,可以知道,它的主要作用是根據路由(雜湊值)來切換檢視,而且路由是與導航樹的routeId欄位和viewType欄位相關。而在建立檢視時,雜湊值就是檢視的xtype值,這說明,應用程式是使用檢視的xtype值作為路由值(雜湊值)的,也就是說,我們在定義檢視的時候,必須定義xtype配置項,而且必須在導航樹中有與之相匹配的節點,不然就會顯示xytpe值為page404的檢視了。
在瞭解了setCurrentView方法後,我們來看看導航樹是怎麼運作的。在7.1.3節中,我們已經知道導航樹將selectionchange事件繫結到了onNavigationTreeSelectionChange方法,在該方法內,先獲取節點的routeId欄位或viewType欄位的值,然後呼叫redirectTo方法。根據API文件的說明,redirectTo方法的作用是更新雜湊值,不過,如果當前的值與要更新的值相同,則不執行路由。如果雜湊值發生改變更改,最終執行的還是setCurrentView方法。
在主檢視中,為主檢視的render事件綁定了onMainViewRender方法,在方法內,如果雜湊值不存在,就切換到儀表盤(dashboard)檢視,也就是說,如果沒有指定要顯示的內容面板,就切換到儀表盤檢視,把它作為預設檢視。
在主檢視的檢視控制器內,餘下的方法基本上是工具欄各按鈕所對應的方法。除了導航欄顯示切換和平臺切換,基本都是使用setCurrentView方法來切換檢視的,這裡就不深入研究了,有興趣可自行研究。
總的來說,管理模版的一大特色就是充分利用路由功能來實現檢視的切換,這樣做,不單實現了瀏覽器的歷史功能,還簡化了檢視之間的切換流程,非常的方便。

7.1.6 要遷移的檢視

在瞭解了管理模版的運作之後,接下來要考慮的是管理模版中的檢視,有多少是我們可以保留需要遷移到我們的應用程式的。在瀏覽器開啟管理模版的示例頁面,在瀏覽過所有檢視後,就心中有數了,需要遷移的檢視包括空白檢視(Blank Page)、404檢視(404 Error)、500檢視(500 Error)、登入檢視(Login)和重置密碼檢視(Password Reset)。除了以上所說的檢視外, 其他檢視中的樣式也會使用到,這個需要結合應用程式中的實際情況再進行遷移。

7.2 實施遷移

7.2.1 主檢視

1. 指令碼

主檢視包含4個檔案,都需要複製,把classic\src\view\main資料夾下的檔案全部複製到專案的Sencha\app\view\main資料夾下。提示是否替換,全部替換就行了。複製完成後,把main資料夾下的List.js檔案刪除。
導航樹需要使用app\store資料夾下的儲存來存放資料,這個需要複製。把app\store資料夾下的NavigationTree.js檔案複製到專案的Sencha\app\store資料夾下,並把資料夾內的Personnel.js檔案刪除。
檔案複製完後,開啟全部檔案,將檔案內的名稱空間Admin全部修改為專案的名稱空間SimpleCMS。想偷懶的話,可通過在檔案內查詢並替換的方式來替換。
名稱空間修改完以後,開啟app\application.js檔案,在stores配置項內新增導航樹儲存的引用。
儲存的引用新增後,切換到Main.js檔案,在requires配置項中新增對主容器、主檢視的檢視控制器和主檢視的檢視模型的引用。在工具欄的配置物件中,除了保留公司Logo、導航切換按鈕、佔位符(->)、使用者名稱外,其他元件全部刪除,同時在檢視控制器中刪除與刪除的按鈕相關的方法。
做Logo這東西不是筆者強項,也懶得去找,那就繼續使用現在的這個Sencha公司的Logo把,把圖片路徑中的圖片resources/images/company-logo.png複製到Sencha\resources\images資料夾下,如果images資料夾還沒有,就新建一個。
Logo複製後,需要考慮下Logo的顯示路徑問題。在釋出後,resources資料夾的路徑習慣都是位於應用程式的根資料夾下,而現在是位於Sencha資料夾下。如果現在寫死了路徑,生成前還需要修改,如果這樣的圖片路徑很多,而且分散在各個檔案內,這又和修改訪問地址一樣麻煩。為了避免這樣的麻煩,我們修改下訪問地址類,讓它支援返回資源的路徑。開啟Sencha\app\util\Url.js,先新增一個DEBUG的屬性,用了指定當前是除錯狀態還是生成狀態,再新增一個resources屬性,用來定義各種資源的路徑,程式碼如下:

    resources: {
        logo: 'resources/images/company-logo.png'
    },

如果確定資源都將放在resources/images資料夾內,可以考慮在定義中把這兩個也去掉,在返回資源路徑時再新增上去。
資源有了,餘下就是定義一個getResource方法用來返回資源,程式碼如下:

getResource: function (res) {
    var me = this;
    return ROOTPATH + (me.DEBUG ? '/sencha/' : '/') + me.resources[res];
}

程式碼中,如果是DEBGU狀態,則新增secnha路徑,否則不新增。
方法getResource定義好以後,就可以將顯示Logo的程式碼修改為以下程式碼了:

html: '<div class="main-logo"><img src="'+ URL.getResource('logo') +'">'+ I18N.AppTitle + '</div>',

以上程式碼除了使用訪問地址類來獲取Logo的地址,還使用了本地化類來獲取應用程式的標題。本地化類中的具體資源資訊筆者就不贅述了,這個可自行新增進去。
修改完Logo後,將顯示使用者名稱的元件中的text配置項刪除,然後新增bind配置項,值為“{ text: ‘{UserName}’ }”,這樣就把元件的顯示文字和資料物件UserName繫結在一起了,只要更新資料物件UserName的值,就可重新整理文字的顯示。使用繫結方式,比通過查詢元件,並呼叫元件的方法來重新整理顯示要方便。現在切換到主檢視的檢視模型,在data配置項內新增資料物件UserName,值為null。
開啟導航樹的儲存,把根節點(root配置項)中,children配置項內的子節點全部刪除。
開啟主檢視的檢視控制器,在變數newView的定義程式碼前新增以下程式碼:
parentNode = node ? node.parentNode : null,
程式碼的主要作用是用來獲取節點的父節點。在原來的設計中,雖然檢視切換後會選中與檢視相關的節點,但沒有考慮所選節點可能是隱藏在摺疊節點之下,這就會造成顯示效果很怪異,在導航樹上看不到那個節點被是被選中的。這裡獲取父節點的作用是為了判斷它是否處於摺疊狀態,如果是,則呼叫expand方法展開父節點,以便看到選中的子節點。在呼叫導航樹的setSelection方法的程式碼上新增以下程式碼來實現展開父節點的功能:

if (parentNode && !parentNode.isRoot() && !parentNode.isExpanded()) parentNode.expand();

程式碼先判斷父節點是否存在,如果存在,再判斷父節點是否根節點,如果是根節點,則不需要展開。如果不是根節點,則呼叫isExpanded方法來判斷父節點是否已經展開,如果還沒有展開,則呼叫expand方法展開父階段。

2. 樣式

主檢視的樣式遷移比較麻煩點,因為樣式分散在了sass和classic\sass這兩個資料夾內。樣式遷移所要做的是要把通用樣式和經典模式的樣式合併起來。
為了簡化操作,可以先把sass資料夾下的內容覆蓋Sencha\sass資料夾下的內容。檔案覆蓋後,在Sencha\sass\src\view下,除了main資料夾外,其餘的資料夾先刪除,以避免生成時出現不必要的錯誤。
資料夾sass內的檔案遷移完成後,就要考慮classic\sass資料夾內的檔案遷移了。把classic\sass\etc資料夾內的all.scss檔案裡的內容全部複製到Sencha\sass\etc\all.scss檔案內;把classic\sass\src\view\main\資料夾內的Main.scss檔案裡的內容全部追加到Sencha\sass\src\view\main\Main.scss檔案內;把classic\sass\var\view\main資料夾內的Main.scss檔案裡內容全部追加到Sencha\sass\var\view\main\Main.scss檔案內。
樣式處理完以後,生成一次應用程式,然後在瀏覽器上開啟應用程式,如看到如圖7-3所示的效果,說明主檢視的遷移已經順利完成了。

7.2.2 空白檢視

1. 指令碼

由於餘下要遷移的檢視多多少少有些相關性,因而為了快捷起見,可直接將classic\src\view資料夾下的authentication和pages資料夾複製到Sencha\app\view資料夾下。檔案複製後,先刪除Sencha\app\view\authenication資料夾下的Register.js檔案,再刪除Sencha\app\view\pages下的FAQ.js檔案。檔案刪除後,開啟餘下的檔案,將裡面的名稱空間Admin全部修改為應用程式的名稱空間SimpleCMS。
名稱空間修改完以後,在主檢視的requires配置項中新增“’SimpleCMS.view.pages.”和“SimpleCMS.view.authentication.”來引用剛剛複製到專案的類。
開啟空白頁檢視,將html配置項中的說明文字使用本地化資源進行替換。

2. 樣式

由於在sass檔案下沒有檢視對應的樣式,因而只需要複製classic\sass\資料夾下與檢視對應的樣式就行了。需要複製的資料夾包括classic\sass\src\view\authentication、classic\sass\src\view\pages、classic\sass\var\view\authentication和classic\sass\var\view\pages,將這些資料夾複製到專案對應的資料夾就行了。複製完成後,把不需要的FAQ.scss檔案全部刪除。
在這些樣式中,使用了lock-screen-background.jpg和error-page-background.jpg這兩個背景圖片,我們需要從resources\images資料夾中將這兩個檔案複製到專案的Sencha\resources\images資料夾下。

3. 如何使用

大家一定很奇怪,為什麼會有這麼一節內容呢?問題的關鍵在於,在setCurrentView方法中,只有在導航樹中存在的檢視才允許顯示,如果不存在,則顯示404檢視去了。那麼,將空白檢視加到導航節點不就解決了麼,想法很好,問題是使用者看到導航樹上有個空白頁面的節點,點進去真的是空白頁面,不知道會有什麼想法。筆者覺得最低限度會問一句,這是什麼鬼?有什麼用?
要解決這個問題,辦法有很多,可以在導航樹內使用隱藏節點,也可以不從導航樹中查詢,而從自定義的陣列中查詢檢視,也可以是兩者的結合。在導航樹中使用隱藏節點是最簡單的方式,不需要對setCurrentView方法做任何調整。如果是陣列中查詢或既從導航樹中查詢,又從陣列中查詢,則需要調整setCurrentView方法,而且需要新增一個屬性來存放檢視陣列。大家可選擇自己喜歡的形式來實現。在當前專案將使用最簡單的方式——在導航樹中使用隱藏節點的方式。開啟導航樹的儲存,在根節點的childrens陣列內,新增以下節點程式碼:

{
    text: '空白頁',
    viewType: 'pageblank',
    leaf: true,
    visible: false
}

程式碼中的關鍵是配置項visible,將它設定為false後,節點就隱藏起來了。
生成一次應用程式後,在瀏覽器中開啟http://localhost:55263/#pageblank,將看到如圖7-4所示的空白檢視效果。

7.2.3 404檢視

404檢視已經在7.2.2節複製到專案了,我們要做的是修改裡面的內容,以實現本地化。修改除了Error404Window.js檔案,還需要修改它的父類ErrorBase.js,視窗的標題是在ErrorBase.js中定義的。
對於404檢視,可新增到導航樹中,也可不新增,無論新增與否,最終顯示的結果都是一樣的。完成後的404檢視如圖7-5所示。

7.2.4 500檢視

與404檢視一樣,只需要實現本地化就行了。不過,這個檢視要新增到導航樹中。完成後的500檢視如圖7-6所示。

500檢視對應的是伺服器500的錯誤,而在統一的錯誤處理中,我們採用的是資訊提示視窗方式來顯示500錯誤的,是否將該錯誤切換到500檢視的方式,是我們需要考慮的。其實也不需要考慮什麼,主要是你認為那種方式最貼合用戶體驗,或者使用者更願意接受哪種方式,就選用哪種方式。在本專案,還是以資訊提示視窗的方式來實現。
如果希望使用500檢視的方式,可修改錯誤處理中ajax方法的程式碼,將呼叫alert的方法修改為以下程式碼就行了:

window.location.hash=’page500’

7.2.5 登入檢視

在登入檢視內,先把不需要的忘記密碼(Forgot Password)、或者分隔線(OR,2個)、Facebook登入按鈕(Login with Facebook)和建立帳號(Creatre Account)等元件刪除。
多餘元件刪除後,將使用者名稱欄位的name由userId修改為UserName,刪除bind配置項。將密碼欄位的name由password修改為Password,刪除bind配置項。為記住我欄位新增name配置項,值為RemberMe,刪除bind配置項。
欄位修改完成後將顯示的描述性文字全部實現本地化。最後將登入檢視新增到導航樹中。完成後的登入檢視如圖7-7所示。

7.2.6 重置密碼檢視

對於重置密碼檢視,只需要它的框架,裡面的內容需要調整為修改密碼的內容。
先要改造的是電子郵件輸入欄位,將name由email修改為Password;新增配置項inputType,值為password;刪除vtype配置項;將triggers配置項中的cls修改為登入視窗中密碼欄位所使用的樣式,以便將圖示顯示為一把鎖。
電子郵件欄位修改完成後,複製欄位的配置物件,貼上兩次。將粘貼後的第一個欄位的name修改為NewPassword。將貼上的第二個欄位的name修改為ConfirmPassword。
調整完欄位後,將重置密碼按鈕的ui修改為“soft-green”,與登入視窗的登入按鈕保持一致。其他的可修改可不修改,自己掌握。
由於這是全屏視窗,沒有關閉圖示,因而,我們需要新增一個返回按鈕用來返回主檢視。複製一份儲存按鈕的程式碼就行了。將配置項reference和formBind刪除。將ui修改為“soft-blue”。將單擊事件繫結的方法修改為“onReturnClick”。
開啟AuthenticationController.js檔案,先將方法onFaceBookLogin、onLoginAsButton、
onNewAccount和onSignupClick刪除。然後新增onReturnClick方法,程式碼如下:

onReturnClick: function () {
    window.history.back();
}

由於應用程式實現了歷史記錄功能,因而可以在這裡使用歷史功能的back方法來返回上次顯示檢視的。
對於新密碼和確認密碼,還需要新增驗證,已驗證兩次輸入的密碼是否相同,這個需要在確認密碼欄位中新增vtype配置項,值為password;新增initialPassField配置項,值為NewPassword。在新密碼欄位中新增itemId配置項,值為NewPassword。
為了對新密碼實施簡單的驗證,需要新增regex和regexText配置項,程式碼如下:

regex: /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z\W]{6,}$/,
regexText: I18N.PasswordRegexText,

程式碼使用了正則表示式來驗證密碼是否包含陣列和字母,且最小長度為6位,如果不包含,則提示錯誤。如果需要其他規則,可自行修改正則表示式。配置項regextText是用來定義正則表示式驗證不通過時的錯誤資訊的。
為了驗證舊密碼不能和新密碼不能相同,我們需要為新密碼欄位新增validator配置項來實現額外的驗證,程式碼如下:

validator: function (v) {
    var me = this,
        form = me.up('form'),
        values = form.getForm().getValues(),
        old = values["Password"];
    return old === v ? I18N.OldPasswordEqualNew : true;
}

由於validator只有欄位的值這一個引數,因而,我們需要通過欄位本身,先使用up方法找到表單,然後再使用getValues方法返回表單中的所以值,再將舊密碼的值取出來,與新密碼做比較,如果兩個值相等,就返回錯誤資訊,否則返回true,表示驗證通過。
重置密碼的欄位和按鈕調整好以後,把裡面的提示性文字全部使用本地化資源代替。最後在導航樹中新增隱藏節點。完成後的重置密碼檢視如圖7-8所示。

7.3 小結

在本章,主要實現了管理模版的遷移工作,工作量雖然不大,但需要的是耐心。如果不夠耐心,很容易忽略了一些細節,從而達不到預期的效果。
對於學習Ext JS,筆者認為,還是得多研究示例、模版和框架自身的原始碼,從原始碼中可以學到的東西是很多教程所不能教不到的。譬如,要實現某個功能,而這功能在某個元件有類似功能,那就可以研究一下這個元件是怎麼去實現這個功能的,是否可以參考這個元件的實現方法來實現所需的功能。
當然,要去閱讀原始碼也需要一定的程式碼閱讀、理解和分析能力,尤其是需要掌握很多與原始碼相關的知識,如三層開發、開發模式等等知識,而這些,需要日積月累,才會有所提高。以上所說的,其實都脫離不了耐心兩字,沒有耐心很容易半途而廢,直接問人去了,問人後獲得所需的結果後,也沒耐心去搞明白個前因後果,只要能完成任務就行,這樣,往往對自身的發展沒任何益處。
廢話不多說了,在下一章,將講述登入與許可權控制等問題。

相關推薦

Ext JS 6.2實戰節選——遷移管理模版

Ext JS 6的示例中,提供瞭如圖7-1所示的管理模版和如7-2所示的Executive模版。這兩套模版都採用了當前流行的簡潔大氣風格,且都是響應式設計,是大家比較喜歡的型別。尤其是管理模版,可以說是當前最流行的後臺管理模版,非常適合用來開發應用程式,但比較遺

Ext JS 6.2實戰》一書上傳按鈕的問題

近日,有熱心讀者發郵件給我說上傳按鈕有bug,第一次開啟上傳按鈕時,可以開啟檔案選擇對話方塊,當開啟第二個檢視時,就不能開啟檔案選擇對話方塊了。經研究,發現是建立plupload.Uploader物件時

Ext Js 6.2.1 classic grid 滾動條bug解決方案

efi 父類 滾動 cti seq position column spa 元素 此bug未在其他版本發現,參考高版本代碼重寫類解決此bug,直接上代碼: 1 /** 2 * 如果列表同時存在橫向滾動條和豎向滾動條,當豎向滾動條滾動到底部時 3 * 點擊

Ext JS 4.2實戰

3月底各大網店新華書店均有銷售 內容簡介 本書是一本實戰系列的書,主要通過簡單的CMS系統的開發過程,介紹了使用Ext JS 4.2開發應用程式的新模式和新思路。本書可以說是作者的集大成之做,是作者使用ExtJS進行開發實踐的經驗之談。本書不單是一本學習Ext JS的書籍

[Ext JS 6 By Example 翻譯] 第2章 - 核心概念

轉載自:http://www.jeeboot.com/archives/1217.html     在下一章我們會構建一個示例專案,而在這之前,你需要學習一些在 Ext JS 中的核心概念,這有助於你更容易理解示例專案。這一章我們將學習以下知識點: 類系統,

一個Ext JS 6可用的下載類

HTML5為A標籤添加了download屬性,可用來指定連結的檔名,單擊A標籤後就可實現檔案下載功能,該元件就是利用這個特性來實現的,具體程式碼如下: Ext.define('Admin.util.Do

[Ext JS 6 By Example 翻譯] 第7章 - 圖表(chart)

轉載自:http://www.jeeboot.com/archives/1229.html   本章中將探索在 ExtJS 中使用不同型別的圖表並使用一個名為費用分析的示例專案結束本章所學。以下是將要所學的內容: 圖表型別 條形圖 和 柱形圖 圖表 區域 和

[Ext JS 6 By Example 翻譯] 第6章 - 高階元件

轉載自:http://www.jeeboot.com/archives/1227.html   本章涵蓋了高階元件,比如 tree 和 data view。它將為讀者呈現一個示例專案為 圖片瀏覽器,它使用 tree 和 data view 元件。以下是本章將要討論的主題:

[Ext JS 6 By Example 翻譯] 第5章 - 表格元件(grid)

轉載自:http://www.jeeboot.com/archives/1225.html   本章將探索 Ext JS 的高階元件 grid 。還將使用它幫助讀者建立一個功能齊全的公司目錄。本章介紹下列幾點主題: 基本的 grid 排序 渲染器 過濾

[Ext JS 6 By Example 翻譯] 第4章 - 資料包裝

轉載自:http://www.jeeboot.com/archives/1222.html   本章探索 Ext JS 中處理資料可用的工具以及伺服器和客戶端之間的通訊。在本章結束時將寫一個呼叫 RESTful 服務的例子。下面是本章的內容: 模型 Schema

[Ext JS 6 By Example 翻譯] 第3章 - 基礎元件

轉載自:http://www.jeeboot.com/archives/1219.html     在本章中,你將學習到一些 Ext JS 基礎元件的使用。同時我們會結合所學建立一個小專案。這一章我們將學習以下知識點: 熟悉基本的元件 – 按鈕,文字框,日期

[Ext JS 6 By Example 翻譯] 第1章 – 入門指南

轉載自:http://www.jeeboot.com/archives/1211.html   前言 本來我是打算自己寫一個系列的 ExtJS 6 學習筆記的,因為 ExtJS 6 目前的中文學習資料還很少。google 搜尋資料時找到了一本國外牛人寫的關於 ExtJS 6 的

6.2.3-軟體包管理-rpm命令管理-查詢

查詢只能通過rpm來查詢 查詢是否安裝(不用進入光碟目錄就可以執行) 【[email protected]~】#rpm -q 包名 #查詢包是否安裝 選項: -q 查詢(query) 【[email protected]~】#rpm -qa #查詢所已經安裝的RP

Ext JS 6開發例項(四) :調整主檢視

上文把主介面設定好,但是主檢視因為介面的微調出現了顯示問題,本文將把它調整好了。 開啟app/view/main/Main.js,可以看到主檢視是派生於標籤面板(Ext.tab.Panel)的。在檢視的標籤欄內,除了顯示標籤外,還顯示了標題欄。由於已經重新設計

Ext Js 3.2Ext.ajax.request方法詳解

1:Ext.Ajax.request([Object options]):Number     options中的一些屬性和含義如下:     url:指定請求的服務端url    params: 指定要傳遞的引數,可以是一個包含引數名稱及值的物件,也可以是類似於name=

Ext Js 3.2中Record的使用方法

先來了解Ext Js和SQL的不用語法:    1:Ext Js語法       var EmployeeRecord = new Ext.data.Record.create({              {name: 'empId ', type: 'int'}    

Ext Js 3.2中設定行的顏色

1:程式程式碼 <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String b

Ext JS 6:將日期欄位修改為日期時間欄位(二)

在上文《Ext JS 6:將日期欄位修改為日期時間欄位(一) 》(以下稱文一)只是簡單的利用日期選擇欄位的原始碼建立了一個日期時間選擇欄位,而不是採用繼承的方式,因而在本地化上,並不能很好的利用日期選擇欄位的本地化資源,需要自己考慮本地化的問題。為了解決這個問題

[Ext JS 4] Grid 實戰之分頁功能

前言 分頁功能的實現有兩種途徑: 一種是服務端分頁方式, 也就是web客戶端傳遞頁碼引數給服務端,服務端根據頁面引數返回指定條數的資料。也就是要多少取多少。這種方式比較適合Grid  的資料量很大,需分批取。 另一種是客戶端分頁方式, 一次性從服務端取回所有的資料在客戶端

EXT-JS 6示例程式-Login示例程式

1.        用Sencha Cmd生成應用程式模版 sencha -sdk /path/to/ExtSDK generate app -classic TutorialApp./TutorialApp 2.        建立Login View元件 在./Tut