Angular: 控制器可以监视服务器属性吗?

Angular: can a controller watch server properties?

我有一个控制器来管理我的数据页面和一个服务,该服务每 30 秒发出一次 HTTP 请求以获取要在页面上显示的新数据。我正在尝试以 "Angular" 可测试并正确利用服务的方式编写此内容。

我能想到两种基本方法,我猜其中一种(或两种)是错误的:

  1. 控制器将数据存储在$scope变量中,并执行setInterval$timeout调用服务的方法以获取新数据,然后更新变量.

  2. 服务将数据存储在它自己的 variables/property 中,并定期调用 它自己的 来获取新数据。并且控制器以某种方式 watches/listens 到服务属性以知道何时更新视图。

为了这个问题的目的,考虑一个具体的例子可能会有所帮助。如果 HTTP 请求失败,我想将错误显示给 view/user。所以假设一个 errorMsg 变量需要存在于某个地方。它应该住在控制器中吗?在这种情况下,服务每次都需要 return 该值。或者它应该存在于服务中,控制器以某种方式监视它。

我已经尝试过第一种方法,它似乎在控制器中产生了很多逻辑,主要是在遵循服务方法的 then() 中。我的直觉是#2 是正确的方法。但是我有点不清楚控制器 如何 应该 listen/watch 服务。提前致谢。

让我们从控制器的角度来看这个:

The controller stores the data and queries the service

这叫做。您正在有效地创建您在控制器中轮询的服务器响应流

The service stores the data and the controller watches it

这叫做推送。您正在有效地创建结果流并通知消费者更改,而不是它寻找它们。


这些都是解决您的问题的有效方法。选择您认为更容易推理的方法。我个人同意第二个更干净,因为您不必在控制器中了解它。这应该让您大致了解:

function getServerState(onState){
    return $http.get("/serverstate").then(function(res){
        onState(res.data);// notify the watcher
    }).catch(function(e){/*handle errors somehow*/})
      .then(function(){
        return getServerState(onState); // poll again when done call
    });
}

你可以这样吃:

getServerState(function(state){
     $scope.foo = state; //since it's in a digest changes will reflect
});

我们的最后一个问题是它泄漏了作用域,因为代码不在控制器上,我们有一个回调注册到一个将不复存在的作用域。由于我们还不能为此使用有趣的 ES6 工具 - 我们必须为 getServerState 方法 return 值提供一个 "I'm done" 句柄,例如 .done 属性您将在范围销毁事件中调用它。

虽然本杰明提供了一个非常好的解决方案,我完全同意他的回答,但为了完整起见,我想补充一点,对于你的问题,还有一个你可能还没有想到的替代方案:

使用 websockets

使用 websockets,您的服务器可以直接调用客户端,因此您无需轮询更新。当然,这是否完全适合您取决于您​​的场景和服务器技术。但这就是如何创建从服务器到客户端的真正 Push

如果你想尝试一下:

如果您使用的是 .Net 服务器,那么一定要尝试 SignalR。它非常好用,并且具有长轮询的回退机制。

还有这个我没有真正使用过的库,所以我不能说很多:https://github.com/wilk/ng-websocket

使用 node.js 你应该看看 Socket.IO


关于实现,您可以同时执行这两种实现,但您的第二个选项可能也更清洁网络套接字。到目前为止,我就是这样做的。

我认为您应该将逻辑保留在控制器中(解决方案 #1),否则服务将 运行 无限,即使没有控制器需要它也会导致整个系统的一些内存和网络开销应用。而且,我不认为逻辑对于控制器来说太复杂了。

此外,将范围函数传递给服务似乎不太优雅。

我会这样做:

  1. 服务:
factory('serverState', ['$http', function($http) {
    var update = function (){
       return $http.get("serverUrl")
    }
   return {
    update:update
   };
 }]);
  1. 控制器:
    function updateSuccess(res){
        $scope.state = res.state;
    }

    function updateFail(res){
        $scope.state = "Not connected";
    }

    $interval(function() {
           serverState.update().then(updateSuccess,updateFail);
          }, 30000);

您可以创建简单的服务(名为 ServerStateService),它使用 $interval 每 30 秒从服务器获取刷新数据。

$interval(function() {
    $http.get(YOUR_URL_PATH).success(function(responseData) {
        $rootScope.$emit('server.state.change', responseData);
    });
}, 30 * 1000);

当您收到来自服务器的响应时,使用来自服务器的 responseData 从名为 'server.state.change' 的 ServerStateService 发出或广播事件。

最后使用 angular 事件系统从控制器处理它。

$rootScope.$on('server.state.change', function(data) {
    //Do somethig with data
    $scope.serverData = data;
});

虽然轮询服务器是一种在服务器和客户端之间共享状态的有效方法,但您还可以利用其他方法来提高服务器性能并减少服务器开销。上面提到了其中之一,WebSockets。然而,WebSockets 相当新,需要大量的服务器端工作来支持它们。

其中一种鲜为人知或提及的方法称为 Server Sent Events。它是 HTML5 标准的一部分,因此您会发现所有浏览器都实现了它(当然,IE 除外。惊喜!)。 SSE 是服务器提醒客户端状态更改的一种非常简单的方法。

我认为 "Angular" 方法是编写一个指令。

该指令将包含一个隔离的 scope/child 控制器。这将允许控制器和您的 html 模板文件之间的双向绑定。

加载指令后,您将启动一个 $timer 对象,该对象会定期调用内部控制器 依次调用 angular 服务(服务器端数据的代理)的函数。

来自服务的响应数据将用于更新绑定到您的 html 模板的控制器上的 $scope 变量。

这还有一个好处,就是允许您在指令范围内通过“$destroy”事件删除指令时取消计时器。

解决方案的概要如下。

'use strict';
/*global angular,console,$:false*/

angular.module('testModule').

directive('testDirective', ['testService', function(testService) {


    var testController = function($scope, testService) {
        $scope.testData = {};

        function serviceResponse(data) {
            $scope.testData = data;
        }

        function serviceError(error) {
            console.log(error);
        }

        var timeoutPromise = null;

        function startTimer() {
            timeoutPromise = $timeout(function() {

               /* 
                * Call the service, which returns a promise that
                * when resolved will follow the success or error path.
                */
               testService.retrieveData().then(serviceResponse, serviceError);
            }, 180000);  // every 3 minutes
        }

        function cancelTimer() {
            if (timeoutPromise !== null) {
                $timeout.cancel(timeoutPromise);
            }
        }

        $scope.$on('$destroy', function() {
            cancelTimer();
        });

        // Start the timer
        startTimer();
    };

    return {
        restrict: 'EA',
        scope: {},
        templateUrl: 'testHTML.html',
        controller: ['$scope', 'testService', testController],
        link: function(scope, element, attrs) {
    }
  };
}
]);