在 angularjs 中为 promise 设置超时处理程序

Posted

技术标签:

【中文标题】在 angularjs 中为 promise 设置超时处理程序【英文标题】:Setting a timeout handler on a promise in angularjs 【发布时间】:2014-05-24 13:30:50 【问题描述】:

我正在尝试在我的控制器中设置超时,以便如果在 250 毫秒内没有收到响应,它应该会失败。我已经将我的单元测试设置为超时 10000 以便满足这个条件,谁能指出我正确的方向? (编辑我试图在不使用我知道提供超时功能的 $http 服务的情况下实现这一点)

(编辑 - 我的其他单元测试失败,因为我没有对它们调用 timeout.flush,现在我只需要在 promiseService.getPromise() 返回未定义的承诺时启动超时消息。我'已经从问题中删除了早期代码)。

promiseService(promise 是一个测试套件变量,允许我在应用之前对每个测试套件中的 Promise 使用不同的行为,例如在一个中拒绝,在另一个中成功)

    mockPromiseService = jasmine.createSpyObj('promiseService', ['getPromise']);
    mockPromiseService.getPromise.andCallFake( function() 
        promise = $q.defer();
        return promise.promise;
    )

正在测试的控制器功能 -

$scope.qPromiseCall = function() 
    var timeoutdata = null;
    $timeout(function() 
        promise = promiseService.getPromise();
        promise.then(function (data) 
                timeoutdata = data;
                if (data == "promise success!") 
                    console.log("success");
                 else 
                    console.log("function failure");
                
            , function (error) 
                console.log("promise failure")
            

        )
    , 250).then(function (data) 
        if(typeof timeoutdata === "undefined" ) 
            console.log("Timed out")
        
    ,function( error )
        console.log("timed out!");
    );

测试(通常我在这里解决或拒绝承诺,但不设置它我正在模拟超时)

it('Timeout logs promise failure', function()
    spyOn(console, 'log');
    scope.qPromiseCall();
    $timeout.flush(251);
    $rootScope.$apply();
    expect(console.log).toHaveBeenCalledWith("Timed out");
)

【问题讨论】:

你能告诉我们promiseService.getPromise()吗? 还没实现,我想先设计一下,应该和promise服务的实现挂钩吗? 如果你还没有实现它,你怎么知道它不起作用 这是一个单元测试,所以我正在注入 promiseService.getPromise...当我输入这个时,我意识到我没有注入代码,现在添加 sry 我在您的代码中的任何地方都没有看到数字“250”...您希望它如何在 250 毫秒后执行某些操作? 【参考方案1】:

首先,我想说你的控制器实现应该是这样的:

$scope.qPromiseCall = function() 

    var timeoutPromise = $timeout(function() 
      canceler.resolve(); //aborts the request when timed out
      console.log("Timed out");
    , 250); //we set a timeout for 250ms and store the promise in order to be cancelled later if the data does not arrive within 250ms

    var canceler = $q.defer();
    $http.get("data.js", timeout: canceler.promise ).success(function(data)
      console.log(data);

      $timeout.cancel(timeoutPromise); //cancel the timer when we get a response within 250ms
    );
  

你的测试:

it('Timeout occurs', function() 
    spyOn(console, 'log');
    $scope.qPromiseCall();
    $timeout.flush(251); //timeout occurs after 251ms
    //there is no http response to flush because we cancel the response in our code. Trying to  call $httpBackend.flush(); will throw an exception and fail the test
    $scope.$apply();
    expect(console.log).toHaveBeenCalledWith("Timed out");
  )

  it('Timeout does not occur', function() 
    spyOn(console, 'log');
    $scope.qPromiseCall();
    $timeout.flush(230); //set the timeout to occur after 230ms
    $httpBackend.flush(); //the response arrives before the timeout
    $scope.$apply();
    expect(console.log).not.toHaveBeenCalledWith("Timed out");
  )

DEMO

promiseService.getPromise 的另一个例子:

app.factory("promiseService", function($q,$timeout,$http) 
  return 
    getPromise: function() 
      var timeoutPromise = $timeout(function() 
        console.log("Timed out");

        defer.reject("Timed out"); //reject the service in case of timeout
      , 250);

      var defer = $q.defer();//in a real implementation, we would call an async function and 
                             // resolve the promise after the async function finishes

      $timeout(function(data)//simulating an asynch function. In your app, it could be
                              // $http or something else (this external service should be injected
                              //so that we can mock it in unit testing)
        $timeout.cancel(timeoutPromise); //cancel the timeout 

         defer.resolve(data);
      );

      return defer.promise;
    
  ;
);

app.controller('MainCtrl', function($scope, $timeout, promiseService) 

  $scope.qPromiseCall = function() 

    promiseService.getPromise().then(function(data) 
      console.log(data); 
    );//you could pass a second callback to handle error cases including timeout

  
);

您的测试与上面的示例类似:

it('Timeout occurs', function() 
    spyOn(console, 'log');
    spyOn($timeout, 'cancel');
    $scope.qPromiseCall();
    $timeout.flush(251); //set it to timeout
    $scope.$apply();
    expect(console.log).toHaveBeenCalledWith("Timed out");
  //expect($timeout.cancel).not.toHaveBeenCalled(); 
  //I also use $timeout to simulate in the code so I cannot check it here because the $timeout is flushed
  //In real app, it is a different service
  )

it('Timeout does not occur', function() 
    spyOn(console, 'log');
    spyOn($timeout, 'cancel');
    $scope.qPromiseCall();
    $timeout.flush(230);//not timeout
    $scope.$apply();
    expect(console.log).not.toHaveBeenCalledWith("Timed out");
    expect($timeout.cancel).toHaveBeenCalled(); //also need to check whether cancel is called
  )

DEMO

【讨论】:

谢谢,不幸的是我不小心编辑了我的问题的一部分,因为我知道它有一个内置的超时处理程序,所以我知道这需要 $http 服务不处理的承诺,你能更新吗对于这种情况? @LinuxN00b:你看我的第二个例子了吗?符合您的要求吗? 谢谢你,我早上还在喝咖啡,所以我完全错过了第二个演示。在这个示例中,在现实生活中,您将如何以一种干净的方式处理 promiseService 承诺的超时,并防止承诺稍后在意外时间返回并触发问题? @LinuxN00b:检查我更新的答案,看看你是否满意。在此示例中,我将所有代码移至一个函数。这只是这个想法的一个演示。如果超时发生,则键拒绝承诺,如果响应在超时之前到达,则取消超时。 抱歉,我没有意识到您必须点击才能奖励我仍然满意的赏金。编辑 - 说 3 小时后我可以接受,然后会接受,非常感谢您【参考方案2】:

“除非在指定的时间范围内解决,否则承诺失败”的行为似乎非常适合重构为单独的服务/工厂。这应该使新服务/工厂和控制器中的代码更清晰,更可重用。

控制器,我假设它只是在作用域上设置成功/失败:

app.controller('MainCtrl', function($scope, failUnlessResolvedWithin, myPromiseService) 
  failUnlessResolvedWithin(function() 
    return myPromiseService.getPromise();
  , 250).then(function(result) 
    $scope.result = result;
  , function(error) 
    $scope.error = error;
  );
);

工厂failUnlessResolvedWithin 创建了一个新的promise,它有效地“拦截”了来自传入函数的promise。它返回一个复制其解析/拒绝行为的新的,除了如果它在超时内没有被解析,它也会拒绝承诺:

app.factory('failUnlessResolvedWithin', function($q, $timeout) 

  return function(func, time) 
    var deferred = $q.defer();

    $timeout(function() 
      deferred.reject('Not resolved within ' + time);
    , time);

    $q.when(func()).then(function(results) 
      deferred.resolve(results);
    , function(failure) 
      deferred.reject(failure);
    );

    return deferred.promise;
  ;
);

这些测试有点棘手(而且很长),但您可以在 http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview 看到它们。测试的要点是

控制器的测试通过调用$timeout 模拟failUnlessResolvedWithin

$provide.value('failUnlessResolvedWithin', function(func, time) 
  return $timeout(func, time);
);

这是可能的,因为 'failUnlessResolvedWithin' 在语法上(故意)等同于 $timeout,并且是因为 $timeout 提供了 flush 函数来测试各种情况。

服务本身的测试使用调用 $timeout.flush 来测试在超时之前/之后解决/拒绝原始承诺的各种情况的行为。

beforeEach(function() 
  failUnlessResolvedWithin(func, 2)
  .catch(function(error) 
    failResult = error;
  );
);

beforeEach(function() 
  $timeout.flush(3);
  $rootScope.$digest();
);

it('the failure callback should be called with the error from the service', function() 
  expect(failResult).toBe('Not resolved within 2');
);   

您可以在http://plnkr.co/edit/3e4htwMI5fh595ggZY7h?p=preview 看到所有这些操作

【讨论】:

谢谢,不幸的是我不小心编辑了我的问题的一部分,因为我知道它有一个内置的超时处理程序,所以我知道这需要 $http 服务不处理的承诺,你能更新吗对于这种情况? 我认为我的回答不需要与$http 服务相关的任何内容。 getPromise 可以是任何返回承诺的函数。你能澄清为什么你认为它会吗?或者,如果我有误解,请在问题中阐明您的要求。 答案将超时提供给处理超时的 $http 调用,不是吗? 我不确定你所说的 feed 是什么意思,但我使用的唯一 Angular 服务是 $q$timeout。然后他们都没有打电话给$http(据我所知)。 @MichalCharemza +1 我添加了一个源自您原始答案的答案。【参考方案3】:

我使用真实示例实现 @Michal Charemza 的 failUnlessResolvedWithin。 通过将延迟对象传递给函数,它减少了在使用代码“ByUserPosition”中实例化承诺的必要性。帮助我处理 Firefox 和地理定位。

.factory('failUnlessResolvedWithin', ['$q', '$timeout', function ($q, $timeout) 

    return function(func, time) 
        var deferred = $q.defer();

        $timeout(function() 
            deferred.reject('Not resolved within ' + time);
        , time);

        func(deferred);

        return deferred.promise;
    
])



            $scope.ByUserPosition = function () 
                var resolveBy = 1000 * 30;
                failUnlessResolvedWithin(function (deferred) 
                    navigator.geolocation.getCurrentPosition(
                    function (position) 
                        deferred.resolve( latitude: position.coords.latitude, longitude: position.coords.longitude );
                    ,
                    function (err) 
                        deferred.reject(err);
                    , 
                        enableHighAccuracy : true,
                        timeout: resolveBy,
                        maximumAge: 0
                    );

                , resolveBy).then(findByPosition, function (data) 
                    console.log('error', data);
                );
            ;

【讨论】:

以上是关于在 angularjs 中为 promise 设置超时处理程序的主要内容,如果未能解决你的问题,请参考以下文章

在 UseEffect 中为两个 API 调用设置 Promise

如何在 Rails 4 中为 AngularJS 更正设置根路由?

在AngularJS中为jQuery datepicker动态设置最大限制

在angularjs中为多个控制器路由?

在angularjs中为多个控制器路由?

AngularJS 组件模式在 Promise 后不更新