单页系统前端MVC设计 – 获取数据

一个单页系统中,由于浏览器始终不能离开当前页面,从而无法利用服务器端的动态页面(ASP、JSP、PHP等)将数据与页面结合起来,数据与表现相比单独的页面请求分离得更为彻底。其中数据获取的部分,通常使用的是AJAX的技术,利用XMLHttpRequest向服务器请求数据。

我在很多篇文章中提到过,现有互联网的大部分应用是数据驱动的,数据是整个业务的核心和关键所在。在框架的执行过程中,Action同样需要数据才可以驱动整个界面的渲染及交互,因此当Action进入自身的执行流程时,第一件事就是将自己需要的所有数据获取过来。

ActionMapping和Controller

Action流程

Actionexecute开始,到renderView绘制视图,并在之后与视图进行交互,其执行流程如下所述:

  1. 调用execute函数开始流程。
  2. 调用prepareData函数,该函数中利用DataProxy组件获取数据。
  3. 当所有数据请求完成后,会调用buildModel函数,该函数通过ValueStackModelDescriptor两个组件,将第2步中获取的数据组装成为更加结构化、可用于视图中的控件树的模型。
  4. 完成了模型的组装后,调用getRoot函数,根据预先配置的内容,从模板中获取当前Action对应的根元素。
  5. 调用createPage函数,分析模板,生成一个Page对象,并将准备好的模型交付给该Page对象。Page对象根据模型的结构,在控件树中逐级分配每个控件需要的model。
  6. 调用renderView函数,将这个Page对应的HTML结构加载到DOM树中,到这一步用户已经可以看到完整的界面。
  7. 调用initBehavior函数,该函数使得Action可以监听控件树中各个控件的事件、通过事件与控件树进行交互,达到修改、更新界面等目的。

在第2步使用DataProxy组件获取数据时,对于没有特别复杂的数据获取过程的Action,可以通过框架默认支持的datasource配置项来完成对DataProxy的配置:

datasource: [  
    { name: 'userInfo', url: config.url.getUserInfo, converter: mappingUserInfo },
    { name: 'postList', url: config.url.getPostLost, params: { pageIndex: 1 } }
]

在以上的配置下,Action会自动识别,并通过DataProxy请求userInfopostList两项数据。

而在第3步进行模型的组装时,对于不是特别复杂的模型结构,同样可以通过框架默认支持的modelDescriptor配置项来完成:

modelDescriptor: {  
    userPanel: 'userInfo',
    pager: {
        pageSize: ['postList.pageSize', 'defaultPageSize'],
        pageIndex: 'postList.pageIndex',
        pageCount: 'postList.pageCount'
    },
    table: {
        title: 'postTableTitle',
        header: 'postTableHeader',
        data: 'postList.list'
    },
    searchBar: {
        status: ['postList.search.status', 'search.postStatus'],
        keyword: ['postList.search.keyword', 'search.keyword']
    }
}

当使用以上配置的时候,Action会自动通过ValueStack,将上一步给定的userInfopostList两个对象,转为更有结构性的模型,其中的userPanelpagertablesearchBar等对应着控件树中的控件名称,会根据这个名称将具体的对象进一步分发到指定的控件上。

DataProxy组件

获取数据,即和MVC中的M部分进行交互。在框架中,特别添加了DataProxy组件用于ActionModel之间的通信交互。

组件作用

DataProxy用于和Model(通常是服务器端开放的相关数据访问接口)进行交互,利用XMLHttpRequest获取需要的数据。

但是由于一个Action不可能仅仅依赖与一项数据,一个Action可能需要发送多个请求才可以完整地获取自己需要的数据集合。因此DataProxy也需要协调多个请求,控制请求间的并行运行,并可以在所有数据都到达客户端后通知Action

另一方面,DataProxy组件存在的最大意义是阻止Action去直接使用XMLHttpRequest获取数据,这使得框架在可测性有了很大的提高。

通常来说,在测试过程中,很重要的一个环节是测试在对应的数据之下,界面的显示是否正确,也就是测试ActionexecuterenderView的整个过程,以及最终视图的结构是否正确。在这种测试中,如果Action直接使用了XMLHttpRequest,就需要服务器端配合给出指定的数据(或者使用Fiddler等工具拦截HTTP请求),显然测试的成本较高。

而使用DataProxy把获取数据的逻辑分离之后,可以通过继承DataProxy,生成一个不发送任何请求的用于伪造数据的MockDataProxy,事先指定返回的数据,可以快速地构建测试用例。

接口设计

DataProxy用于协调异步的多个获取数据的过程,因此一个最简单的DataProxy对外暴露以下函数:

  • add(name, url, params, method):向url发送parmas对象指定的数据,并将返回的数据以name为键存放在数据池中。
  • load():在通过add函数配置完请求之后,调用load函数开始并行的数据获取过程。
  • complete事件:在所有数据获取成功后,触发complete事件。加载的数据通过事件函数的参数传递。

实现原理

异步函数的依赖维护一文中有提到,多个异步执行之间的一种依赖形式是回调依赖,即一个回调需要在多个异步完成之后才可以执行。

DataProxy的设计中,complete事件就是一个典型的1:n的回调依赖,需要在所有的数据完成加载后才可以触发。而在绝大多数情况下,Action获取数据的请求之间不存在更加复杂的依赖,因此最简化的默认的DataProxy实现仅仅控制一个回调的依赖,保证加载所有数据后触发complete事件:

var DataProxy = function() {  
    this._queue = []; // 请求队列
    this._context = {}; // 存放所有数据的池
};

DataProxy.prototype.add = function(name, url, params, method) {  
    var data = {
        name: name, url: url, params: params, method: method
    };
    this._queue.push(data);
};

DataProxy.prototype.load = function() {  
    var proxy = this;
    // 处理循环中的lift问题,提取为单独的函数
    function request(item, proxy) {
        ajax[item.method || 'get'](
            item.url, 
            item.params, 
            function(data) {
                proxy._context[item.name] = data;
                length--;
                // 触发事件
                if (!length) {
                    proxy.fireEvent('complete', proxy._context);
                }
            }
        );
    };
    for (var i = 0; i < this._queue.length; i++) {
        var item = this._queue[i];
        request(item, proxy);
    }
};

ValueConverter组件

虽然在图上将DataProxyValueConverter放在一起,但是ValueConverter其实是一个通用的组件,整个框架中很多环节会需要使用到他。

组件作用

顾名思义,ValueConveter组件用于将一个数据转换为另一个数据,主要用于以下场合:

  • 输入的数据结构不满足后续使用的要求,因此需要进行转换。
  • 输入的数据类型不满足后续使用的要求,需要转换。

最典型的,如所有AJAX请求的responseText都是字符串,但根据需要可以转换为JSON对象、Document对象等多种形式。

Action获取数据的过程中,往往是因为Action需要的数据中各个属性的属性名称与服务器返回的不同,因此需要一个转换的过程,所以DataProxy会进一步依赖ValueConverter,用于将服务器返回的数据做一次转换。

接口设计

ValueConverter是概念层面的组件,这决定了他并不局限于特定的类型,因此以下类型的对象是可以作为ValueConverter使用的:

  • 带有1个参数作为输入,并能返回对象的函数。
  • ValueConverter接口继承的类型。

进一步细分:

  • 最简单的ValueConverter就是原生类型构造函数,如StringNumber都可以作为ValueConverter直接使用。
  • 其次是原生的某些函数,如parseFloatJSON.parse也可以作为ValueConverter使用。
  • 最后是为了满足复杂的数据转换的需要而建立的ValueConverter的实现类。

ValueConverter接口定义如下,仅包含一个简单的convert函数:

var ValueConverter = function() {};  
ValueConverter.prototype.convert = function(input) {  
    return input;
};

简单实现

对于DataProxy来说,最常见的情况是处理“属性名称不正确”的问题,比如服务器返回以下对象:

{
    id: 123,
    name: 'otakustay',
    age: 25,
    sex: 'male'
}

但是前端由于利用某些组件的限制,需要的结构如下:

{
    userId: 123,
    username: 'otakustay',
    age: 25,
    gender: 'male'
}

在这种情况下,就需要一个映射的过程,最简单的方式就是实现一个通用的MappingValueConverter,通过配置的方式完成映射:

ValueConverter.Mapping = function(mappign) { this.mapping = mapping; };  
//继承
ValueConverter.Mapping.prototype = new ValueConverter();  
ValueConverter.Mapping.prototype.constructor = ValueConvert.Mapping;  
//重写
ValueConverter.Mapping.prototype.convert = function(input) {  
    var output = {};
    for (var key in input) {
        output[this.mapping[key] || key] = input[key];
    }
    return output;
};

随后通过简单的代码就可以完成数据间的转换:

var mapping = {  
    id: 'userId', 
    name: 'username'
};
var converter = new ValueConverter.Mapping(mapping);  
var data = converter.convert(data);  

组件结合

修改DataProxy的接口,add函数添加最后一个参数converter,指定需要的DataConverter,参数可选。

并修改DataProxy中的请求函数:

function request(item, proxy) {  
    ajax[item.method || 'get'](
        item.url, 
        item.params, 
        function(data) {
            // 添加数据转换
            var converter = item.converter;
            proxy._context[item.name] =
                converter instanceof ValueConverter ?
                converter.convert(data) : converter(data);
            length--;
            // 触发事件
            if (!length) {
                proxy.fireEvent('complete', proxy._context);
            }
        }
    );
}

最后在Action配置使用DataProxy时,给定相应的ValueConverter即可。