在promise链中使用$ resource(修复延迟反模式)

时间:2015-07-01 20:30:44

标签: javascript angularjs promise angular-resource

我有一个带有方法的服务,该方法使用$ resource获取项目类型列表。它对我来说很有效,除了如果我进行多个几乎同时的调用(例如,两个指令),每个都会创建另一个请求,而不是使用相同的响应/ $ promise / data。

我发现this导致我this和TL; DR,显然它正在创建一个冗余的$ q.defer(),实际上被认为是deferred anti-pattern

如果获取项目类型的调用明显错开(例如,相隔超过毫秒),则下面的代码运行良好。使用shared.projectTypes解析连续调用。它的工作原理是,如果获取项目类型的请求失败,dfr.reject()将被触发并被调用控制器中的.catch捕获。

angular.module('projects')
.factory('projectService', function(notificationService){

    // an object to share data gathered by this service
    var shared = {};

    // $resource for projects API
    var projectResource = $resource(baseApiPath + 'projects', {}, {
        ...,
        getProjectTypes: {
            method: 'GET',
            url: baseApiPath + 'projects/types'
        },
        ...
    });

    // loads a list of project types
    var loadProjectTypes = function(){
        var dfr = $q.defer();

        // if we've already done this, just return what we have.
        if(shared.projectTypes){
            dfr.resolve(shared.projectTypes);
        }
        else {
            // begin anti-pattern (?)
            projectResource.getProjectTypes(null,
            function(response){
                shared.projectTypes = response.result.projectTypes;
                dfr.resolve(response);
            },
            function(errResponse){
                console.error(errResponse);
                notificationService.setNotification('error', errResponse.data.messages[0]);
                dfr.reject(errResponse);
            });
        }
        return dfr.promise;
    };

    return {
        shared: shared,
        project: projectResource,
        loadProjectTypes: loadProjectTypes
    };
});

所以,我读到有这个额外的var dfr = $q.defer()没有必要,因为$ resource会为我提供所有这些。通过一些重构,我最终得到了这个:

...
    // $resource for projects API
    var projectResource = $resource(baseApiPath + 'projects', {}, {
        ...,
        getProjectTypes: {
            method: 'GET',
            url: baseApiPath + 'projects/types',
            isArray: true,
            transformResponse: function(response){
                return JSON.parse(response).result.projectTypes;
            }
        },
        ...
    });

    // loads a list of project types
    var loadProjectTypes = function(){
        return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());
    };
...

为了澄清,我已将isArraytransformResponse添加到资源中,因为我的API返回了大量额外的元信息,而我想要的只是一个类型数组。在我的loadProjectTypes方法中,我包含了我们原来拥有的相同缓存,但是我正在缓存projectResource.getProjectTypes()的结果而不是实际的响应数据(即使这可能正是我的意思由于transformResponse而缓存。

这适用于快乐路径(减少对API的调用,向每个人返回相同的内容等)但我的主要问题是链接和捕获错误。

在我原来的反模式示例中,如果GET / project / types存在错误,我正在使用dfr.reject(),然后将其传递回我的控制器,我有一个.catch()。< / p>

这是来自控制器的代码,它实际上使原始请求获得项目类型:

$q.all([
    projectService.loadProjects(),
    userService.loadUserRole('project_manager'),
    userService.loadUserRole('sales_representative'),
    projectService.loadProjectTypes(),
    clientService.loadClients()
])
.then(function(response){
    // doing stuff with response
})
.catch(function(errResponse){
    // expecting errors from service to bubble through here
    console.error(errResponse);
});

使用反模式示例,dfr.reject导致错误显示在catch中,但在我假设的非反模式示例中,它没有发生。我不确定如何以与以前相同的方式拒绝或解决$ resource结果。如果承诺链中的一个点是要有一个点来处理来自任何链接链接的错误,我就是这么做的。

我尝试使用$ q.resolve()/ reject(),因为我没有dfr,但这看起来很愚蠢,无论如何都不起作用。

return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes(null, 
    function(response){
        return $q.resolve(response);
    },
    function(errResponse){
        return $q.reject(errResponse);
    }));

如何使链工作,以便控制器中的.catch()处理错误?

我是否真的在原始代码中实现反模式,或者是使用$ q.defer()的可接受方式之一,它根本不是反模式?

second link I posted中,有一个答案说:

  

“这有什么问题?但模式有效!幸运的你。   不幸的是,它可能没有,因为你可能忘记了一些优势   案件。在我看过的一半以上的事件中,作者都​​有   忘了照顾错误处理程序。“

但是,我的原始代码解决错误。它工作正常,除了每个来电者都有自己的承诺。我觉得那是我错过的地方。

我可能会感到困惑,但我认为loadProjectTypes方法应该向调用它的任何人返回相同的promise / data,无论何时调用它。它应该是任何projectTypes的真正来源,并且只能进行一次调用,这是第一次。

任何时候我都会寻找这些主题(这些主题上有很多紫色/访问过的谷歌链接),每个人都要么用人为的示例显示链接,要么只使用$ http或其他东西。我没有发现任何人在使用$ resource的promise链中捕获错误。

更新:添加我对解决方案的要求。我在答案中发布了这些内容,但也希望将它们包含在原帖中。

要求1:允许多次调用该方法,但只发出一个API请求,用同一数据更新所有呼叫者。

要求2:必须能够使用方法的结果作为实际数据,就像promise规范所指出的那样。 var myStuff = service.loadStuff()实际上应该将myStuff设置为“东西”。

要求3:必须允许承诺链,以便链的任何部分中的所有错误都可以被链末端的单个捕获捕获。正如我在我的解决方案中发现的那样,可以有多个链和多个捕获,但重点是每个链都有一个捕获,并且链中的任何“链接”都应该报告它们的错误他们各自的捕获。

2 个答案:

答案 0 :(得分:3)

总是那样,一旦你说出你的问题,你就会遇到你的解决方案。

要求1:每次方法调用只发出一个请求。这是通过对反模式的原始修复来解决的。这将始终返回$ resource结果,方法是返回缓存的$ resource或同时返回和缓存。

var loadProjectTypes = function(){
    return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());
};

要求2:能够将服务方法用作promise,我可以将$ scope变量的值直接设置为loadProjectTypes()的结果。使用上面修改过的方法,我可以简单地说明$scope.theTypes = projectService.loadProjectTypes(),当它们进入时它会自动填充类型列表,就像promise规范所指出的那样。

要求3:能够将多个$ resource调用链接在一起并使其错误被单个.catch()捕获。通过在$ q.all()中使用loadProjectTypes结果的$ promise,我可以捕获任何我想要的任何错误。

$q.all([
    ...,
    projectService.loadProjectTypes().$promise,
    ...
])
.then(function(response){
    // my project types comes in as response[n]
})
.catch(function(errResponse){
    // but any errors will be caught here
});

从技术上讲,我可以把捕获物放在不同的地方,但它们都会起作用。每当我有loadProjectTypes()时,我都可以使用.catch(),我的错误将在那里处理。每个类型的加载器都可以按照自己的方式处理API。实际上这可能真的很棒。控制器可能会让UI显示消息,而小指令可能只显示其他内容,或者根本不显示任何内容。他们每个人都可以用自己的方式处理坏事。

我的服务,指令和控制器现在看起来像这样:

angular.module('projects')
.factory('projectService', function(notificationService){

    // an object to share data gathered by this service
    var shared = {};

    // $resource for projects API
    var projectResource = $resource(baseApiPath + 'projects', {}, {
        ...,
        getProjectTypes: {
            method: 'GET',
            url: baseApiPath + 'projects/types',
            isArray: true,
            transformResponse: function(response){
                return JSON.parse(response).result.projectTypes;
            }
        },
        ...
    });

    // loads a list of project types
    var loadProjectTypes = function(){
        return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());
    };

    return {
        shared: shared,
        project: projectResource,
        loadProjectTypes: loadProjectTypes
    };
});


angular.module('projects')
.directive('projectPageHeader', ['projectService', function(projectService){
    return {
        restrict: 'E',
        scope: {
            active: '@',
        },
        templateUrl: 'src/js/apps/projects/partials/dir_projectPageHeader.html',
        replace: true,
        controller: function($scope){
            $scope.projectService = projectService;

            // sets the types to the array of types
            // as given by the transformResponse
            $scope.types = projectService.getProjectTypes();

            // could also do a .$promise.catch here if I wanted.
            // all catches will fire if get projectTypes fails.
        }
    };
}]);

angular.module('projects')
.controller('projectListPageController', [
    '$scope','projectService',
function($scope, projectService){

    // load it all up
    $q.all([
        projectService.loadProjectDetails($routeParams.projectId).$promise,
        userService.loadUserRole('project_manager').$promise,
        userService.loadUserRole('sales_representative').$promise,
        projectService.loadProjectStatuses().$promise,
        projectService.loadProjectTypes().$promise,
        clientService.loadClients().$promise
    ])
    .then(function(response){
        // do work with any/all the responses
    })
    .catch(function(errResponse){
        // catches any errors from any of the $promises above.
    })
}]);

由于loadProjectTypes(或任何其他load_____方法)将类型保存在它来自的服务中,我实际上不需要在控制器上进行任何存储。 projectService.shared.projectTypes在整个应用中都是通用的。如果所有服务都在内部存储其加载结果(这是我喜欢的方式),我的控制器中的.then()方法可能是noop,除非我需要对它们进行一些特定于视图的事情。我通常只使用控制器来处理整个页面或$ modals。其他所有内容都被分解为指令,大多数信息和逻辑都在服务中。

我打开这个问题,万一有人有更好的解决方案。我喜欢Jack A.发布的那个,但我觉得这让我的负载方法比现在更加冗长。由于它们中的一些略有不同,因此在我的实际代码中会导致大量冗余代码或复杂的“智能”方法。它肯定解决了要求1,可能还有2和3。

更新(GOTCHA):

所以,我现在已经使用这种模式几天了,而且它的确非常符合我的意图。它真的简化了我们的流程;然而,我最近在单一语境中使用像loadProjectTypes这样的方法时遇到了问题(即:$q.all()之外)。

如果你只是使用这样的加载方法:

// This code is just placed in your controllers init section
loadProjectTypes()
.$promise
.then(function(response){
    // ... do something with response (or noop)
})
.catch(function(errResponse){
    // ... do something with error
});

当控制器“刷新”时,您将遇到这种情况。例如,你在controllerA中有上面的代码,你改变使用controllerB的“pages”,然后你回到第一个“page”,controllerA刷新并尝试再次运行它。您得到的错误是“没有.then未定义。”

在控制台中检查这个,第一次loadProjectTypes()运行时,它返回$ resource的响应(包括$ promise和所有projectType数据)。第二次 - 从controllerB返回 - 它只保存projectType数据。没有$ promise,因为你没有返回$资源的结果,你返回了你在第一次之后设置的缓存shared.projectTypes。这就是为什么我们做了这一切,还记得吗?我不确定为什么这会消失,因为那是你保存到shared.projectTypes的内容,但确实如此,实际上并不重要。

return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());

对我来说,最简单的解决方法是让loadProjectTypes().$promise成为$q.all()集的唯一成员:

// again, this code is just placed somewhere near the top of your controller
$q.all([
    loadProjectTypes().$promise
])
.then(...)
.catch(...);

在大多数情况下,我的控制器将获得不止一件事,所以这最终会发生,但总会有一种情况,你只需要加载一件事。使用$q.all()中设置的单个项目是使用此解决方案时没有问题的唯一方法。真的不是那么糟糕,可能会更糟。

答案 1 :(得分:0)

我刚才写了一些非常相似的东西,但有一些关键的区别:

  1. 我只在数据已经在缓存中时创建了promise,并在启动实际请求时返回本机promise。

  2. 我添加了第三个状态,表示资源请求已经挂起。

  3. 代码的简化版本如下所示:

    module.factory("templateService", function ($templateCache, $q, $http) {
        var requests = {};
        return {
            getTemplate: function getTemplate(key, url) {
                var data = $templateCache.get(key);
                // if data already in cache, create a promise to deliver the data
                if (data) {
                    var deferred = $q.defer();
                    var promise = deferred.promise;
                    deferred.resolve({ data: data });
                    return promise;
                }
                // else if there is an open request for the resource, return the existing promise
                else if (requests[url]) {
                    return requests[url];
                }
                // else initiate a new request
                else {
                    var req = $http.get(url);
                    requests[url] = req;
                    req.success(function (data) {
                        delete requests[url];
                        $templateCache.put(key, data);
                    });
                    return req;
                }
            },
        };
    });