Ext JS 6.7 中文文档:应用架构介绍(MVC/MVVM)

最近为了做内网的后台学了下 Ext JS,觉得挺有意思的,顺手翻译一下文档,主要是架构部分和核心概念部分。

这个文档是我目前看到的最好的关于 MVC、MVVM 的解释与探讨,十分值得参考。


前言

Ext JS 提供了对 MVC 和 MVVM 架构的支持。这两种架构都致力于根据功能逻辑分割代码,又各有其长。

这个教程的目的在于讲解构成这两个架构的组件的基础知识。

什么是 MVC?

在 MVC 架构中,大部分类是 Model(模型)、View(视图)或 Controller(控制器)。用户与视图交互,视图显示模型中保存的数据,由控制器根据需要更新视图和模型来响应交互。

View 和 Model 之间通常没有直接联系,因为控制器指导着两者的更新。通常来说,在 MVC 架构中 Controller 包含了大部分的应用程序逻辑,而 View 几乎没有什么业务逻辑。 Model 则主要是数据的接口,包含着用于管理数据变化的业务逻辑。

MVC 的目标是清晰地定义应用中每个类的职能,使它们能够有效地解耦,为应用程序的测试和维护提供便利,并提高代码的复用性。

什么是 MVVM?

MVC 和 MVVM 最大的区别是 MVVM 把 View 抽象为 ViewModel,ViewModel 通过 “数据绑定” 协调 Model 的数据和 View 的表现。

使用 MVVM 可以最小化或消除直接操纵 View 的应用程序逻辑,而使框架发挥更大的作用。(即减少了 Controller 的许多工作量)

MVC 和 MVVM

为了选择更适合你的应用的架构,我们需要先进一步定义这几个字母。

  • (M)Model - 这是你的应用的数据。一系列类定义了其数据的字段(比如一个用户 Model 定义了用户名和密码字段)。Model 知道如何从数据包(库)中获取数据以及如何与其它 Model 关联。
    模型通常与 Store(仓库)结合使用,为网格和其它组件提供数据。Model 也是包含你需要的各种数据逻辑的理想位置,比如验证、转换等。
  • (V) View - View 即各种可视化的组件,比如 grid,tree,panel 等。
  • (C) Controller - Controller 被用来维护 View 的逻辑,以使应用程序正常工作。它可能包含视图的渲染、路由、实例化 Model 以及其它的应用程序逻辑。
  • (VM) ViewModel - ViewModel 是管理用于 View 的数据的类,它可以与相关组件绑定并在数据变化时对 View 进行更新。

这俩应用架构为代码提供了清晰的结构和一致性。遵循我们的建议将会有如下好处:

  • 每个应用的工作方式都一样,不用重复学习
  • 在应用程序间复用代码很容易
  • 可以使用 Sencha Cmd 创建优化的生产版本

创建示例应用

在进一步的讨论之前我们先用 Sencha Cnd 创建一个示例应用。首先,下载并解压 Ext JS SDK,然后在命令行输入如下指令

sencha -sdk local/path/to/ExtJS generate app MyApp MyApp
cd app
sencha app watch

应用概述

文件结构

Ext JS 应用遵循统一的目录结构。在我们推荐的布局中,所有 Store,Model,ViewModel 和 ViewController 类都放在 app 文件夹中(模型放在 model 中,Store 放在 store 中,ViewModel/Controller 放在 view 中)。最佳的做法是使用类似的命名结构把 ViewController 和 ViewModel 分组在 app/view 的子文件夹中(参考下图中的 app/view/main/classic/src/view/main/ 文件夹)。

文件结构

命名空间

每个类的第一行是各种类型的“地址”,这个“地址”称为命名空间,命名空间的形式如下:

<AppName>.<foldername>.<ClassAndFileName>

在样例应用中,“MyApp” 是应用名称,“view” 是文件夹名称,“main” 是子文件夹名称,“Main” 是类名和文件名,根据这些信息,框架从如下位置寻找名叫 “Main.js” 的文件:

// Classic
classic/src/view/main/Main.js

// Modern
modern/src/view/main/Main.js

// Core
// "MyApp.view.main.MainController" shared between toolkits would be located at:
app/view/main/MainController.js

如果找不到该文件,Ext JS 将抛出如下错误:
在这里插入图片描述

应用

首先我们探讨 index.html

<!DOCTYPE HTML>
<html manifest="">
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta charset="UTF-8">

    <title>MyApp</title>


    <script type="text/javascript">
        var Ext = Ext || {}; // Ext namespace won't be defined yet...

        // This function is called by the Microloader after it has performed basic
        // device detection. The results are provided in the "tags" object. You can
        // use these tags here or even add custom tags. These can be used by platform
        // filters in your manifest or by platformConfig expressions in your app.
        //
        Ext.beforeLoad = function (tags) {
            var s = location.search,  // the query string (ex "?foo=1&bar")
                profile;

            // For testing look for "?classic" or "?modern" in the URL to override
            // device detection default.
            //
            if (s.match(/\bclassic\b/)) {
                profile = 'classic';
            }
            else if (s.match(/\bmodern\b/)) {
                profile = 'modern';
            }
            else {
                profile = tags.desktop ? 'classic' : 'modern';
                //profile = tags.phone ? 'modern' : 'classic';
            }

            Ext.manifest = profile; // this name must match a build profile name

            // This function is called once the manifest is available but before
            // any data is pulled from it.
            //
            //return function (manifest) {
                // peek at / modify the manifest object
            //};
        };
    </script>


    <!-- The line below must be kept intact for Sencha Cmd to build your application -->
    <script id="microloader" type="text/javascript" src="bootstrap.js"></script>

</head>
<body></body>
</html>

Ext JS 使用 Microloader(微加载器)加载 app.json 文件中描述的应用程序资源,而不用把它们都写在 index.html 里。通过使用 app.json 把所有的应用程序元数据都存放在了一个位置,之后 Sencha Cmd 就可以使用简单有效的方式编译你的应用。

有关 app.json 配置的讨论很多,并且有丰富的可参阅资源。

app.js

当我们生成应用之前,我们创建了一个类(在 Application.js 中)并启动了它的实例(在 app.js 中),可以在 app.js 中看到如下代码:

/*
 * This file is generated and updated by Sencha Cmd. You can edit this file as
 * needed for your application, but these edits will have to be merged by
 * Sencha Cmd when upgrading.
 */
Ext.application({
    name: 'MyApp',

    extend: 'MyApp.Application',

    requires: [
        'MyApp.view.main.Main'
    ],

    // The name of the initial view to create. With the classic toolkit this class
    // will gain a "viewport" plugin if it does not extend Ext.Viewport. With the
    // modern toolkit, the main view will be added to the Viewport.
    //
    mainView: 'MyApp.view.main.Main'

    //-------------------------------------------------------------------------
    // Most customizations should be made to MyApp.Application. If you need to
    // customize this file, doing so below this section reduces the likelihood
    // of merge conflicts when upgrading to new versions of Sencha Cmd.
    //-------------------------------------------------------------------------
});

通过为 mainView 指定一个容器类,可以把任何类作为你的 Viewport(可视区域)。在上面的例子中,我们把 MyApp.view.main.Main (一个 TabPanel 类)作为我们的 Viewport。

mainView 的配置指示应用程序创建指定的视图并加上 Viewport Plugin,从而将视图连接到 document body 中。

Application.js

每一个 Ext JS 应用都以 Application 类的实例开始。这个类可以由 app.js 启动,也可以实例化以进行测试。

下面是由 Sencha Cmd 自动生成的 Application.js 的代码:

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

    name: 'MyApp',

    stores: [
        // TODO: add global / shared stores here
    ],

    launch: function () {
        // TODO - Launch the application
    },

    onAppUpdate: function () {
        Ext.Msg.confirm('Application Update', 'This application has an update, reload?',
            function (choice) {
                if (choice === 'yes') {
                    window.location.reload();
                }
            }
        );
    }
});

Application 类包含应用的全局设置,如应用程序的命名空间、共享存储等。当应用过时(浏览器缓存版本对比服务器最新版本)时,onAppUpdate 方法被调用。系统会提示用户重新加载应用以使用最新版本。

View

一个 View 只不过是一个组件(Component),即 Ext.Component 的子类。它包含了应用程序的所有负责显示的方面。

如果打开示例应用的 classic/src/view/main/Main.js 文件,你将看到如下代码:

Ext.define('MyApp.view.main.Main', {
    extend: 'Ext.tab.Panel',
    xtype: 'app-main',

    requires: [
        'Ext.plugin.Viewport',
        'Ext.window.MessageBox',

        'MyApp.view.main.MainController',
        'MyApp.view.main.MainModel',
        'MyApp.view.main.List'
    ],

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

    ui: 'navigation',

    tabBarHeaderPosition: 1,
    titleRotation: 0,
    tabRotation: 0,

    header: {
        layout: {
            align: 'stretchmax'
        },
        title: {
            bind: {
                text: '{name}'
            },
            flex: 0
        },
        iconCls: 'fa-th-list'
    },

    tabBar: {
        flex: 1,
        layout: {
            align: 'stretch',
            overflowHandler: 'none'
        }
    },

    responsiveConfig: {
        tall: {
            headerPosition: 'top'
        },
        wide: {
            headerPosition: 'left'
        }
    },

    defaults: {
        bodyPadding: 20,
        tabConfig: {
            plugins: 'responsive',
            responsiveConfig: {
                wide: {
                    iconAlign: 'left',
                    textAlign: 'left'
                },
                tall: {
                    iconAlign: 'top',
                    textAlign: 'center',
                    width: 120
                }
            }
        }
    },

    items: [{
        title: 'Home',
        iconCls: 'fa-home',
        // The following grid shares a store with the classic version's grid as well!
        items: [{
            xtype: 'mainlist'
        }]
    }, {
        title: 'Users',
        iconCls: 'fa-user',
        bind: {
            html: '{loremIpsum}'
        }
    }, {
        title: 'Groups',
        iconCls: 'fa-users',
        bind: {
            html: '{loremIpsum}'
        }
    }, {
        title: 'Settings',
        iconCls: 'fa-cog',
        bind: {
            html: '{loremIpsum}'
        }
    }]
});

请注意 View 不包含任何应用程序逻辑。所有的视图逻辑应该被放在 ViewController 里,我们将在后面讨论。

这个视图中比较有趣的地方是 controllerviewModel 的配置。

另一个比较有意思的视图是 classic/src/main/view/List.js

/**
 * This view is an example list of people.
 */
Ext.define('MyApp.view.main.List', {
    extend: 'Ext.grid.Panel',
    xtype: 'mainlist',

    requires: [
        'MyApp.store.Personnel'
    ],

    title: 'Personnel',

    store: {
        type: 'personnel'
    },

    columns: [
        { text: 'Name',  dataIndex: 'name' },
        { text: 'Email', dataIndex: 'email', flex: 1 },
        { text: 'Phone', dataIndex: 'phone', flex: 1 }
    ],

    listeners: {
        select: 'onItemSelected'
    }
});

Controller 配置

controller 配置为你的 View 指定了 ViewController。当通过这种方式完成指定后,对应的 ViewController 就成为了你的事件处理方法和相关引用的容器,同时建立了与通过 View 触发的组件和事件之间的一一对应关系。我们将在后面进一步讨论 Controller。

ViewModel 配置

viewModel 配置为 View 指定了 ViewModel。ViewModel 是此组件及其子组件的数据提供者。ViewModel 中包含的数据通常通过添加绑定配置用在需要展示或编辑这些数据的组件中。

在 “Main” 视图中,你可以看到 headertitle 被绑定到 ViewModel ,这说明 title 将由数据的 “name” 值填充,该值由 ViewModel 管理。如果 ViewModel 的值发生了改变,那么 title 的值会自动改变。后面我们还会继续讨论 ViewModel。

Controllers

接下来,我们瞅一眼 Controllers。示例程序生成的 ViewController MainContoller.js 如下:

Ext.define('MyApp.view.main.MainController', {
    extend: 'Ext.app.ViewController',

    alias: 'controller.main',

    onItemSelected: function (sender, record) {
        Ext.Msg.confirm('Confirm', 'Are you sure?', 'onConfirm', this);
    },

    onConfirm: function (choice) {
        if (choice === 'yes') {
            //
        }
    }
});

如果你后头看一下 list 的 View,即 List.js,你会发现有个为 grid 的选择事件指定的函数。那里的 handler 映射到了一个名为 onItemSelected 的函数。无需任何额外的配置,这个 Controller 已经准备好了处理事件。

这使得为应用程序添加逻辑十分容易。你需要的只是定义 onItemSelected 函数。

ViewController 的作用在于:

  • 通过 listenerreference 连接到 View
  • 通过视图的生命周期自动管理关联的 ViewController。从实例化到销毁,Ext.app.ViewController 被绑定到引用它的组件。同一个 View 类的第二个示例会获得它自己的 ViewController 实例。当这些视图被销毁后,它们相关的 ViewController 也会被销毁。
  • 提供封装以使视图的嵌套更为直观

ViewModel

接下来,我们看看 ViewModel。打开 app/view/main/MainModel.js,将看到如下代码:

Ext.define('MyApp.view.main.MainModel', {
    extend: 'Ext.app.ViewModel',

    alias: 'viewmodel.main',

    data: {
        name: 'MyApp',

        loremIpsum: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
    }

    //TODO - add data, formulas and/or methods to support your view
});

ViewModel 是用来管理数据对象的类。这个类允许对这些数据感兴趣的 View 绑定这些数据并在这些数据变化时获得通知。ViewModel 归引用它的视图所有。因为它与视图相关联,因此他们也可以被连接到由组件层次结构中的上级组件所拥有的父级 ViewModel 中。这样,子 View 就可以方便地“继承”父级 ViewModel 的数据。

我们使用 ViewModel 配置在 Main.js 中创建了 View 到 ViewModel 的链接,此链接允许将配置与 setter 绑定,以通过声明自动将 ViewModel 中的数据展示到 View 上。数据在 “MainModel.js” 中是行内的,也就是说,你的数据可以是来自任何地方的任何类型。数据可能有任何类型的代理(AJAX,REST 等)提供。

Model 和 Store

Model 和 Store 构成了应用的信息网关。你的大部分数据由这两个类发送、检索、组织和“建模”。

Model

Ext.data.Model 代表应用程序中的任何类型的可持久化数据。每个 Model 包含对数据“建模”的字段和函数。Model 经常与 Store 一同使用,Store 可以被数据绑定组件(如 grid,tree 和 chart)使用。

样例程序中没有包含 Model,所以我们添加如下代码:

Ext.define('MyApp.model.User', {
    extend: 'Ext.data.Model',
    fields: [
        {name: 'name',  type: 'string'},
        {name: 'age',   type: 'int'}
    ]
});

当然,需要在 “app/model” 中创建 User.js,就像在命名空间一节中提到的一样。

字段

Ext.data.Model 描述了包含 “字段” 的记录。Model 可以通过 fields 配置来声明这些字段。在上面的例子中, name 是一个 string 而 age 是一个 integer。在 API 文档中提供了其它字段类型。

虽然有充分的理由去声明字段和他们的类型,但其实并不需要这么做。如果没有配置 fieds,数据会被自动读取并被插入到数据对象中。当然,在如下情况你可能需要定义字段:

  • 验证
  • 默认值
  • 转换函数

接下来让我们建立一个 Store,看看它和 Model 如何一起工作。

Store

Store 是记录(模型类的实例)的客户端缓存。它提供了排序、过滤和查询其中包含的记录的功能。

样例程序没有包含 Store,我们创建一个,并为它分配一个 Model。

Ext.define('MyApp.store.Users', {
    extend: 'Ext.data.Store',
    alias: 'store.users',
    model: 'MyApp.model.User',
    data : [
     {firstName: 'Seth', age: '34'},
     {firstName: 'Scott', age: '72'},
     {firstName: 'Gary', age: '19'},
     {firstName: 'Capybara', age: '208'}
    ]
});

上面的代码应该放在 “app/store/” 中,文件名为 User.js

如果你想要 Store 的全局实例,你可以把这个 Store 添加到 Application.js 的配置中,如下:

stores: [
    'Users'
],

在这个例子中, Store 直接包含数据。大多数现实情况需要使用 Model 或 Store 上的代理(proxy)来收集数据。代理允许数据提供者和应用之间的数据传输。

想要了解更多,请阅读文档的 Data Guide 部分。

下一步

我们创建了一个强大且有用的程序,叫做 “Ticket App”。这个程序管理登录/注销会话,包含数据绑定,并且展示了使用 MVC+VM 框架的最佳方式。这个例子同样也经过了充分的讨论,所以每个细节都很清晰。

我们建议花费一些时间浏览 Ticket APP 以学习更多有关理想的 MVC+VM 应用框架的内容。

猜你喜欢

转载自blog.csdn.net/Neuf_Soleil/article/details/90300990