Angular最佳实践之$http-麻雀虽小 五脏俱全

@kuitos 2015-08-23 09:39:10发表于 kuitos/kuitos.github.io Angular

Angular最佳实践之$http-麻雀虽小 五脏俱全

原文写于 2014-10-26

AngularJs框架为我们封装了$http用于提供ajax服务,但是作为调用者而言,工程化的项目中直接调用$http去发请求总是不友好且不易于拓展的。合理的做法是前后端使用统一的接口规范、restful。前端采用angular resource调用restful接口。这样才更便于前端做统一封装,将通用需求对调用者透明。

先说说对于http请求我的通用化需求有哪些:

  1. 所有的查询请求都缓存起来,并实现修改通知机制。简而言之就是对某一资源的查操作均走缓存,而在对这个资源做过修改(update、insert、delete)之后要立即告知缓存需要更新。
  2. 基于http请求响应实现的loading动画。在所有http请求未全部响应完成之前会出现loading动画,响应全部成功回来之后结束loading,而且这一切对于调用者而言应该是透明,而不是每次发请求前loading=true,如何在响应成功回调里面loading=false。这样要累死的

好在Angular给我们提供了http拦截器,减少了我们一大半工作量。so,我们只需要在拦截器中加入请求缓存及修改刷新缓存的逻辑就可以实现我们的第一个需求。具体上代码

/**
 * @author kui.liu
 * @since 2014/10/10 下午5:52
 * http处理器,用于设定全局http配置,包括loading状态切换,拦截器配置,超时时间配置等
 */
;
(function (angular, undefined) {
    "use strict";
    // 模拟service的私有服务
    var _app = {};
    angular.module("common.http-handler", [])
        .config(["$httpProvider", function ($httpProvider) {

            /******************** http拦截器,用于统一处理错误信息、消息缓存、响应结果处理等 **********************/
            $httpProvider.interceptors.push(["$q", "$log", function ($q, $log) {
                return {
                    response: function (res) {
                        var config = res.config,
                            responseBody = res.data,
                            cache;
                        if (angular.isObject(responseBody) && !responseBody.success) {
                            $log.error("%s 接口请求错误:%s", config.url, responseBody.message);
                            return $q.reject(res);
                        } else {
                            // 自定义配置,用于query请求直接返回data部分
                            if (config.useDataDirect) {
                                res.data = responseBody.data;
                            }
                            // 自定义配置,若该请求成功后需要重新刷新cache(save,update,delete等操作),则清空对应cache。angular默认cache为$http
                            if (config.refreshCache) {
                                cache = angular.isObject(config.cache) ? config.cache : _app.cacheFactory.get("$http");
                                cache.removeAll();
                            }
                            return res;
                        }
                    },
                    responseError: function (res) {
                        $log.error("%s 接口响应失败! 状态:%s 错误信息:%s", res.config.url, res.status, res.statusText);
                        return $q.reject(res);
                    }
                }
            }]);
        }])
        .run(["$rootScope", "$timeout", "$cacheFactory", function ($rootScope, $timeout, $cacheFactory) {

            /** 绑定cache服务 **/
            _app.cacheFactory = $cacheFactory;
        }]);
})(window.angular);

然后我们定义resource时只需要加入缓存,对于需要通知缓存的方法加上refreshCache(这个是自定义属性,配合http interceptor使用)标识就行:

$resource(url,{}, {
    "get"   : {method: "GET", cache: true},
    "save"  : {method: "POST", refreshCache: true},
    "query" : {method: "GET", isArray: true, cache: true, useDataDirect: true},
    "remove": {method: "DELETE", refreshCache: true},
    "delete": {method: "DELETE", refreshCache: true}
});

调用还是跟往常一样,一切对用户透明。只是我们查看network的时候会发现请求都会被缓存起来。比如下面这个表格
image2014-10-26 16:30:42.png
点第二页
image2014-10-26 16:31:36.png
再点回第一页
image2014-10-26 16:32:19.png
可以看到并没有http请求发出,数据是直接从缓存中响应给请求的。如果我们某个时刻修改了这条数据,那么下次查询请求的时候就不会走缓存了,而是直接从走http然后重新把结果缓存起来。具体就不演示了。
可以毫不夸张的说,如果我们一个数据交互比较频繁的页面,用户在上面操作持续时间超过3分钟,那么我们的IO数就能节省50%以上,这样不仅能大大降低服务器压力,同时前端的响应速度也会大大提升。而且这个比例会随着用户的停留时间增加而增加。最主要的是,一切对调用者都是透明的!!

再来看看我们的第二个需要。基于http请求响应的自动loading状态。实现方式大同小异,同样是利用$httpProvider,只是这里用的是$http提供的transformRequest和transformResponse

/**
 * @author kui.liu
 * @since 2014/10/10 下午5:52
 * http处理器,用于设定全局http配置,包括loading状态切换,拦截器配置,超时时间配置等
 */
;
(function (angular, undefined) {
    "use strict";
    // 模拟service的私有服务
    var _app = {};
    angular.module("common.http-handler", [])
        .config(["$httpProvider", function ($httpProvider) {
            var
                /** loading状态切换 */
                count = 0,
                loading = false,
                stopLoading = function () {
                    count = 0;
                    loading = false;
                    _app.loading(false); // end loading
                };
            /*************************** http超时时间设为30s ***************************/
            $httpProvider.defaults.timeout = 30 * 1000;
            /* 广告时间哈哈.... */
            $httpProvider.defaults.headers.common["X-Requested-With"] = "https://github.com/kuitos";
            /************************* 根据http请求状态判断是否需要loading图标 *************************/
            $httpProvider.defaults.transformRequest.push(function (data) {  // global loading start
                count += 1;
                if (!loading) {
                    _app.timeout(function () {
                        if (!loading && count > 0) {
                            loading = true;
                            _app.loading(true);
                        }
                    }, 500); // if no response in 500ms, begin loading
                }
                return data;
            });
            $httpProvider.defaults.transformResponse.push(function (data) { // global loading end
                count -= 1;
                if (loading && count === 0) {
                    stopLoading();
                }
                return data;
            });

        }])
        .run(["$rootScope", "$timeout", "$cacheFactory", function ($rootScope, $timeout, $cacheFactory) {
            /** 绑定timeout服务 */
            _app.timeout = $timeout;
            /** loading状态切换 **/
            _app.loading = function (flag) {
                $rootScope.loading = flag;
            };
            /** 绑定cache服务 **/
            _app.cacheFactory = $cacheFactory;
        }]);
})(window.angular);

当响应时间超过500ms(可自己设定,一般不要超过1s),则loading动画自动出现,响应成功回来后动画消失。

angular给开发者提供了一套便利的设备龚开发者使用,几乎后端mvc框架中最常用的东西它都会提供。只是angular的官方文档实在写的太烂,这也是社区一直在吐槽的点,很多时候api上并不会告诉你它那个接口的所有功能,搞不定的时候我们还是应该好好读读它那一块的源码,相信我,你总会有意外收获!
最新最详尽代码请移步:source code