异步请求两个常见功能的业务实践

@elcarim5efil 2017-06-19 11:24:31发表于 kaola-fed/blog 已归档


title: 异步请求功能性优化实践
date: 2017-06-19

背景

在做开发的时候,某些接口的响应会比较慢,原因是服务器数据处理时间较长造成的。当响应较慢时,一般的做法只是是给用户提示加载中(转菊花),而不对界面的按钮进行禁用。

试想一个场景,在发出请求 A 后,由于响应较慢,一些不耐心的用户会随便再点击一下页面,再次触发请求 B,此时响应 A 在响应 B 之前到达。如果不对响应的失效进行处理,那么会出现在发出请求 B 的筛选条件下显示请求 A 的响应结果,这显然是一个bug。

如果服务端无法更快速得响应请求,那么一个合理的方法是在数据加载过程中,不能允许用户重复可能触发该请求的操作,并给予提示。

但是对于一个存在多模块多接口的页面而言,某些模块之间交互存在联系,禁止界面操作显然并不是一个简单可行的方案。它会影响用户的体验,在实现起来也有一定的难度。在不禁止界面交互的情况下,要解决显示数据匹配的问题,就只能对响应的有效性进行判断,一旦得到的响应与最新发出的请求不对应,则直接抛弃。

在这里将对于日常业务开发中,常见的两种需求,loading 标记和响应有效星盘度,进行讨论,试图得到一个可行的解决方案。

loading标志位

loading标志位的在请求发出时设置为true,响应回来或错误时设置为false,再通过数据绑定将标志位传递到模版。

var BaseComponent = Component.extend({
    getChart: function() {
        var data = this.data;
        var option = {
            url: '/chart',
            onload: function(json) {
                this.setChart(json.chartData);
                data.loading = false;
            },
            onerror: function(json) {
                data.loading = false;
            },
        };
        data.loading = true;
        this.$request(option.url, option);
    },
});

功能很简单,但是每个接口都写一遍就很麻烦了,因此将其封装到公用的fetchData方法当中。但有时候仅仅一个标志位是不够,如果一个页面或者组件中包含多个接口时,而不同接口对应于模块不同的部分,则需要多组标志位以对这些接口进行区别。因此可以在其中参数配置中设置标志位的名称,示例代码如下:

var BaseComponent = Component.extend({
    fetchData: function(opt) {
        var that = this;
        var data = that.data;
        var loadingFlag = opt.loadingFlag || 'loading';
        var option = {
            onload: function(json) {
                opt.onload.call(that, json);
                data[loadingFlag] = false;
            },
            onerror: function() {
                data[loadingFlag] = false;
            },
        }
        data[loadingFlag] = true;
        this.$request(opt.url, opt);
    }
});

为了将数据绑定到模版中,所以要将数据挂载到组件的 data 上。使用的时候,只需正常调用方法发出异步请求,需要设置标志位的时候则通过loadingFlag进行设置,不需要写额外的逻辑。

var Page = BaseComponent.extend({
    getChart: function() {
        var option = {
            url: '/chart',
            loadingFlag: 'chartLoading'
        };
        this.fetchData(option);
    },
    getTable: function() {
        var option = {
            url: '/table',
            loadingFlag: 'tableLoading'
        };
        this.fetchData(option);
    }
});
<chart loading={chartLoading}/>
<basic.table loading={tableLoading}/>

请求有效性判断

对于迟到的响应,需要通过对比请求发出的时间进行判断,如果与最新的请求发出时间一致,则有效。这里的实现写得非常简单,但是在使用的时候需要特别注意。

var BaseComponent = Component.extend({
    fetchData: function(opt) {
        var that = this;
        var reqTime = +Date.now();  // 当前请求的时间
        opt.lastTime = reqTime;     // 更新最新请求时间
        var option = {
            onload: function(json) {
                // 判断该响应是否对应最新的请求
                if(reqTime < opt.lastTime) {
                    return;
                }
                opt.onload.call(that, json);
            }
        }
        this.$request(opt.url, opt);
    }
});

写法一:

直接使用,这里option是临时变量,因此在fetchData方法当中,option.lastTime是得不到持续性保留的,所以无法判断请求有效性。

var BaseComponent = Component.extend({
    getData: function() {
        var option = {
            url: '/data',
            param: {/*...*/},
            onload: function(){/*...*/}
        };
        this.fetchData(option);
    },
});

写法二:

option挂到data上,option.lastTime能得到持续性保留的,但是data上会多一个属性,污染了组件的 data

var BaseComponent = Component.extend({
    data: {
        dataRequestOption: {
            url: '/data',
            onload: function(){/*...*/}
        }
    },
    getData: function() {
        var option = this.data.dataRequestOption;
        option.param = {/*...*/};
        this.fetchData(option);
    },
});

写法三:

利用闭包将 option 保存起来,但这个 option 就成了静态方法的静态私有变量了,该组件的所有实例都只有一份,因此这个方法就相当于将 option 设置成组件的全局变量。对于一些不会重复在一个页面中实例化的业务组件而言,这种写法是可以直接用的,也写的方便明了,但是你得保证页面中该组件实例的唯一性。

var BaseComponent = Component.extend({
    getData: (function() {
        var option = {
            url: '/data',
            onload: function() {
                /*...*/
            }
        };
        return function() {
            option.param = {
                /*...*/
            };
            this.fetchData(option);
        }
    })(),
});

写法四:

利用 make方法 构造实例的异步接口请求方法,比较麻烦,但适用于会存在多个实例的组件。

var BaseComponent = Component.extend({
    config: function() {
        this.getData = this.makeGetData();
    },
    getData: function() {},
    makeGetData: function() {
        var option = {
            url: '/data',
            onload: function(json){
                this.setData(json.data);
            }
        };
        return function() {
            option.param = this.getParam();
            this.fetchData(option);
        }
    },
});

其实可以像 loading 标志位一样加入一个自定义标志位字段的接口,将最新请求的时间记录到 data[lastTimeFlag+'LastTime'] 上,但这种写法需要对每个请求都进行命名,同时会在 data 上挂上大量的属性,污染 data 属性,而这些值最后也不需要给向外部传递,所以并不适用。

下面是完整的代码:

var defaultOnError = function(json) {
    Notify.notify({ type: 'error', message: json.msg || '获取服务器数据异常'});
};

var BaseComponent = Component.extend({
    fetchData: function(opt) {
        var that = this;
        var data = that.data;
        var options;
        var reqTime = +Date.now();  // 获取当前请求时间

        opt.lastTime = reqTime;     // 更新最新请求时间
        opt.strict = opt.strict === undefined ? true : opt.strict;

        opt.autoLoading = opt.autoLoading === undefined ? true : opt.autoLoading;
        var loadingFlag = opt.loadingFlag = opt.loadingFlag || 'loading';
        var setLoadingFlag = function(val) {
            if(opt.autoLoading) {
                data[loadingFlag] = val;
            }
        };

        var onload = opt.onload || opt.fn;
        var onerror = typeof opt.onerror === 'function' ? opt.onerror : defaultOnError;
        options = {
            progress: true,
            method: opt.method || 'POST',
            type: opt.type,
            data: opt.param,
            onload: function(json) {
                json = json || {};
                setLoadingFlag(false );
                // 在严格模式下,返回数据滞后于最新的请求,则不更新列表
                if(opt.strict && reqTime < opt.lastTime) {
                    return;
                }
                if(json.code === 200 && typeof opt.onload === 'function') {
                    onload.call(that, json);
                } else {
                    onerror.call(that, json);
                }
            },
            onerror: function(json) {
                setLoadingFlag(false);
                onerror.call(that, json);
            }
        };

        setLoadingFlag(true);
        this.$request(opt.url, options);
    }
});