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

Using $resource in a promise chain (fixing deferred anti-pattern)

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

我找到了 which led me to this and TL;DR, apparently it's creating a redundant $q.defer() and is actually considered to be a 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 return 有很多额外的元信息,而我想要的只是一个数组的类型。在我的 loadProjectTypes 方法中,我包含了与最初相同的缓存,但我缓存的是 projectResource.getProjectTypes() 的结果而不是实际的响应数据(即使这可能正是我想要的由于 transformResponse 而缓存)。

这在快乐的道路上工作(减少对 API 的调用,return 对每个人来说都是一样的,等等)但我的主要问题是错误的链接和捕获。

在我最初的反模式示例中,如果 GET /project/types 有错误,我正在使用 dfr.reject(),然后将其传回我的控制器,我有一个 .catch( ).

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

$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 结果。如果 promise 链的要点之一是有一个点来处理来自任何链的错误 link,我做对了。

我尝试使用 $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中,有一个答案说:

"What's wrong with it? But the pattern works! Lucky you. Unfortunately, it probably doesn't, as you likely forgot some edge case. In more than half of the occurrences I've seen, the author has forgotten to take care of the error handler."

但是,我的原始代码 解决错误的。它工作正常,只是每个调用者都得到了自己的承诺。我觉得那是我错过了什么。

我可能会感到困惑,但我认为 loadProjectTypes 方法应该 return 对任何调用它的人都是相同的 promise/data,无论它何时被调用。它应该是任何 projectTypes 的唯一真正来源,并且只调用一次,而且是第一次。

任何时候我寻找任何这样的东西(很多 purple/visited google links 关于这些主题),每个人要么用人为的例子展示链接,要么只使用 $ http,或其他东西。我还没有发现任何人在使用 $resource.

的承诺链中进行错误捕获

更新:添加我对解决方案的要求。我 post 在我的回答中编辑了它们,但也想将它们包含在原始 post 中。

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

要求 2:必须能够将方法的结果用作实际数据,正如承诺规范所希望的那样。 var myStuff = service.loadStuff() 实际上应该将 myStuff 设置为 "stuff"。

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

我刚才写了一些与此非常相似的东西,但有几个主要区别:

  1. 我只在数据已经在缓存中时创建承诺,return在发起实际请求时创建本机承诺。

  2. 我添加了第三种状态,当资源请求已经挂起时。

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

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;
            }
        },
    };
});

不都是这样吗,一说出你的问题,你就会找到你的解决方案。

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

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

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

要求 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. 发布的那个,但我觉得它让我 load___ 方法比现在更冗长。由于其中有几个略有不同,因此导致我的实际代码中有很多冗余代码或复杂的 'smart' 方法。它确实解决了要求 1,但也可能解决了要求 2 和 3。

更新(问题):

所以,我已经使用这个模式几天了,它确实按照我的预期工作。它确实简化了我们的流程;然而,我最近在单一上下文中使用像 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
});

你会运行进入那个控制器'refreshes'的情况。例如,您在 controllerA 中有上面的代码,您更改了使用 controllerB 的 "pages",然后您返回到第一个 "page",并且 controllerA 刷新并再次尝试 运行。你得到的错误是 "there is no .then of undefined."

在控制台中检查它,第一次 loadProjectTypes() 运行s,它 returns 来自 $resource 的响应(包括 $promise 和所有 projectType 数据) .第二次 - 从 controllerB 返回 - 它只会保存 projectType 数据。不再有 $promise,因为您没有 returning 一个 $resource 的结果,您 returned 了您在第一次之后设置的缓存 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() 中的单个项目集是使用此解决方案时没有问题的唯一方法。真的没那么糟糕,还可以更糟。