【问题标题】:Using $resource in a promise chain (fixing deferred anti-pattern)在承诺链中使用 $resource(修复延迟的反模式)
【发布时间】:2015-09-19 04:28:51
【问题描述】:

我有一个服务,它使用 $resource 为我获取项目类型列表。它对我来说效果很好,除了如果我进行多个几乎同时的调用(比如说,两个指令),每个调用都会创建另一个请求,而不是使用相同的 response/$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() 的结果而不是实际的响应数据(尽管这可能正是我正在缓存的内容,因为转换响应)。

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

在我最初的反模式示例中,如果 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 导致错误显示在此处,但在我假设的非反模式示例中,它没有发生。我不确定如何以与以前相同的方式拒绝或解决 $resource 结果。如果 Promise 链接的要点之一是有一个点来处理来自任何链接链接的错误,那么我做对了。

我尝试使用 $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 方法应该向调用它的任何人返回相同的承诺/数据,无论何时调用。它应该是任何 projectTypes 的真正来源,并且只进行一次调用,第一次。

任何时候我寻找这些(关于这些主题的大量紫色/访问过的谷歌链接),每个人要么显示链接与人为的例子,要么只使用 $http 或其他东西。我没有发现任何人在使用 $resource 的承诺链中进行错误捕获。

更新:添加我对解决方案的要求。我将它们发布在我的答案中,但也想将它们包含在原始帖子中。

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

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

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

【问题讨论】:

  • 我在想也许我根本不应该使用额外的方法,控制器应该直接调用资源对象的方法: $q.all([..., projectService. project.getProjectTypes(), ...]).then(...);
  • 这个答案暗示了我原来的“反模式”。现在真的很好奇我所做的是对还是错。 stackoverflow.com/a/25070789/2800116
  • 是的,您肯定使用了延迟反模式。但是我看不到新代码在哪里失败,您确定 getProjectStatuses() 返回的单个承诺被拒绝了吗?
  • projectTypesprojectStatuses 之间的区别是什么?它们是一样的,还是你在不同的方法上尝试不同的模式?
  • @Bergi,projectTypes 就像“在线”或“移动”,状态是“开放”、“关闭”、“升级为销售”等。我试图让这个问题有点集中在只使用类型,除了我展示它是许多不同请求的一部分的实例。新代码本身并没有“失败”,但它并没有满足我的所有要求。

标签: javascript angularjs promise angular-resource


【解决方案1】:

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

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

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

要求 2:能够将服务方法用作承诺,我可以将 $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. 发布的那个,但我觉得它使我的 load___ 方法比现在更冗长。由于其中有一些略有不同,因此在我的实际代码中会导致大量冗余代码或复杂的“智能”方法。它确实解决了要求 1,也可能解决了要求 2 和 3。

更新(GOTCHA):

所以,我已经使用这种模式几天了,它的工作方式完全符合我的预期。它确实简化了我们的流程;但是,我最近在单一上下文(即:$q.all() 之外)中使用 loadProjectTypes 之类的方法时遇到了一个问题。

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

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

当控制器“刷新”时,你会遇到一种情况。例如,您在控制器 A 中有上面的代码,您更改了使用控制器 B 的“页面”,然后返回到第一个“页面”,控制器 A 刷新并尝试再次运行它。你得到的错误是“没有未定义的.then”。

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

【讨论】:

  • 看起来不错。但是您的原始代码的实际问题是什么,它的哪些部分需要更改?看起来很像你对我已经拥有的东西。
  • 与原始反模式代码的区别:加载方法只有一行,并添加了变压器。从非反模式代码来看,控制器中 $q.all() 中的 .$promise 是新的。这就是将它们链接在一起的原因以及#3 的解决方案。我之前的代码只解决了需求1和2。浏览器显示错误,但是没有触发catch。
  • 我认为存在问题是因为我在 $resource 中使用了 transformResponse。因为我的模型很“胖”,所以我使用这个函数修剪它们,我认为这就是保存为 shared.projectTypes 的内容。否则,当我们返回时,它包含 $promise。哦,好吧。
【解决方案2】:

前段时间我写了一些与此非常相似的东西,但有几个关键区别:

  1. 我只在数据已经在缓存中时创建 Promise,并在发起实际请求时返回本机 Promise。

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

【讨论】:

  • 你为什么不简单地把承诺本身放在缓存中(或者在失败时删除它)?
  • 我也想过这个问题。我有一个使用return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes().$promise) 的版本。我发现的问题是你不能做$scope.stuff = projectService.loadProjectTypes(),因为这将是一个承诺,而不是实际数据。我发现如果我使用$q.all([..., projectService.loadProjectTypes().$promise...]).then(...).catch(...) 实际上会将我的承诺链接在一起,但我仍然可以为函数的结果设置一个变量,它将成为数据,就像承诺的意图一样。
  • 是的,您的要求略有不同,因为我的调用者特别希望得到返回的承诺。到目前为止我还没有使用资源方法。
猜你喜欢
  • 2016-12-08
  • 2017-04-26
  • 2012-07-08
  • 1970-01-01
  • 2016-09-22
  • 2023-03-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多