《Ext JS 6.2实战》节选——迁移管理模版

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/tianxiaode/article/details/78559695

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,笔者认为,还是得多研究示例、模版和框架自身的源代码,从源代码中可以学到的东西是很多教程所不能教不到的。譬如,要实现某个功能,而这功能在某个组件有类似功能,那就可以研究一下这个组件是怎么去实现这个功能的,是否可以参考这个组件的实现方法来实现所需的功能。
当然,要去阅读源代码也需要一定的代码阅读、理解和分析能力,尤其是需要掌握很多与源代码相关的知识,如三层开发、开发模式等等知识,而这些,需要日积月累,才会有所提高。以上所说的,其实都脱离不了耐心两字,没有耐心很容易半途而废,直接问人去了,问人后获得所需的结果后,也没耐心去搞明白个前因后果,只要能完成任务就行,这样,往往对自身的发展没任何益处。
废话不多说了,在下一章,将讲述登录与权限控制等问题。

展开阅读全文

没有更多推荐了,返回首页