Node.js + AngularJS + socket.io:连接状态和手动断开

5
我使用nodejs在客户端上集成了socket.io和angularjs。我从互联网上取得了angular-socketio的示例,并在其中添加了“disconnect”方法。
Socket服务:
angular.module('app')
  .factory('socket', ['$rootScope', function ($rootScope) {

    var socket = io.connect();

    return {
      on: function (eventName, callback) {
        socket.on(eventName, function () {  
          var args = arguments;
          $rootScope.$apply(function () {
            callback.apply(socket, args);
          });
        });
      },
      emit: function (eventName, data, callback) {
        socket.emit(eventName, data, function () {
          var args = arguments;
          $rootScope.$apply(function () {
            if (callback) {
              callback.apply(socket, args);
            }
          });
        })
      },
      disconnect: function () {
        socket.disconnect();
      },
      socket: socket
    };

  }]);

控制器:
angular.module('app')
  .controller('Controller', ['$scope', 'socket', function ($scope, socket) {

    socket.emit('register')

    socket.on('connect', function () {
        console.log('Socket connected');
    });

    socket.on('disconnect', function () {
        console.log('Socket disconnected');
    });

    socket.on('register', function (reginfo) {
        console.log('Register: %s, cname=%s', reginfo.ok, reginfo.cname);
        socket.disconnect(); // <-- this line throw Error
    });

    socket.on('last', updateSnapshot);

    socket.on('state', updateSnapshot);

    function updateSnapshot(snapshot) { ... }

}]);

但是,当我尝试使用这个方法断开连接时,出现了错误:
Error: $apply already in progress
  at Error (<anonymous>)
  at beginPhase (http://localhost:4000/scripts/vendor/angular.js:8182:15)
  at Object.$get.Scope.$apply (http://localhost:4000/scripts/vendor/angular.js:7984:11)
  at SocketNamespace.on (http://localhost:4000/scripts/services/socket.js:10:32)
  at SocketNamespace.EventEmitter.emit [as $emit] (http://localhost:4000/socket.io/socket.io.js:633:15)
  at Socket.publish (http://localhost:4000/socket.io/socket.io.js:1593:19)
  at Socket.onDisconnect (http://localhost:4000/socket.io/socket.io.js:1970:14)
  at Socket.disconnect (http://localhost:4000/socket.io/socket.io.js:1836:12)
  at SocketNamespace.<anonymous> (http://localhost:4000/scripts/controllers/controller.js:38:34)
  at on (http://localhost:4000/scripts/services/socket.js:11:34)

“我不明白该挖哪里…”
2个回答

8

[更新]

$$phase 是Angular内部的私有变量,因此您不应该依赖它来处理像这样的事情。Igor在另一个答案中提出了一些建议,应该使用这些建议而不是依赖$$phase(我听说他对Angular知道一些东西;)


当模型发生更改并从Angular框架内部触发事件时,Angular可以根据需要进行脏跟踪并更新任何必要的视图。当您想与Angular之外的代码交互时,必须将必要的函数调用包装在作用域的$apply方法中,以便Angular知道正在发生某些事情。这就是代码的原因。

$rootScope.$apply(function () {
  callback.apply(socket, args);
});

等等。它告诉Angular:“采取这段通常不会触发Angular视图更新的代码,并像应该一样对待它。”

问题是当您在$apply调用中已经存在时调用$apply。例如,以下内容将引发$apply already in progress错误:

$rootScope.$apply(function() {
  $rootScope.$apply(function() {
    // some stuff
  });
});

基于你的堆栈跟踪,看起来某个对于emit的调用(它已经使用了$apply),触发了一个对于on的调用(它也使用了$apply)。为了解决这个问题,我们需要只在没有$apply正在进行时才调用$apply。值得庆幸的是,作用域中有一个叫做$$phase的属性,可以告诉我们是否正在进行脏检查。
我们可以轻松构建一个函数,接收作用域和要运行的函数,并且只有当没有$apply正在进行时才使用$apply运行该函数:
var safeApply = function(scope, fn) {
  if (scope.$$phase) {
    fn(); // digest already in progress, just run the function
  } else {
    scope.$apply(fn); // no digest in progress, run the function with $apply
  }
};

现在我们可以替换对于 的调用。
$rootScope.$apply(function...);

to

safeApply($rootScope, function...);

例如,要修改你上面的代码,请按以下步骤进行:
angular.module('app')
  .factory('socket', ['$rootScope', function ($rootScope) {

    var safeApply = function(scope, fn) {
      if (scope.$$phase) {
        fn(); // digest already in progress, just run the function
      } else {
        scope.$apply(fn); // no digest in progress, run with $apply
      }
    };

    var socket = io.connect();

    return {
      on: function (eventName, callback) {
        socket.on(eventName, function () {  
          var args = arguments;
          safeApply($rootScope, function () {
            callback.apply(socket, args);
          });
        });
      },
      emit: function (eventName, data, callback) {
        socket.emit(eventName, data, function () {
          var args = arguments;
          safeApply($rootScope, function () {
            if (callback) {
              callback.apply(socket, args);
            }
          });
        })
      },
      disconnect: function () {
        socket.disconnect();
      },
      socket: socket
    };

  }]);

感谢Brandon!看起来像个hack,但它能用。你觉得这个检查应该放在Angular核心中吗? - Maxim Grach
核心中有一个检查,但它会抛出错误!我认为“安全应用程序”不在核心中的原因是通常没有理由嵌套$apply - 通常这意味着您在某个地方存在错误。然而,这种情况有点独特,因为您正在包装一个默认不与Angular集成的第三方库。 - Michelle Tilley
有很多很酷的库默认情况下不会集成,还有很多关于如何在Angular中使用它们的问题。 - Maxim Grach
我知道这有点简化了问题,但在许多情况下,包装第三方库的好方法是将它们放在指令中,并在$apply中包装它们的回调函数。例如,可以查看我为另一个SO问题编写的与jQuery颜色选择器的集成:http://jsfiddle.net/BinaryMuse/x2uwQ/ - Michelle Tilley
1
一个快速的更新:$$phase是 Angular 的内部私有变量,因此您不应该真的依赖它来处理像这样的事情。在另一个答案中,Igor 描述了一些应该使用的替代方案。 - Michelle Tilley

4
在这种情况下(与大多数其他情况类似),问题的核心在于on方法大多数情况下是异步调用的(好!),但有时也会同步调用(不好!)。

当您从应用程序中调用socket.disconnect()时(来自在“angular context”中存在的控制器),它将同步触发断开连接事件,该事件然后传播到on方法,该方法旨在打开边界进入角度上下文。但由于您已经在角度上下文中,角度会使用您提到的错误进行投诉。

由于此问题特定于断开连接调用,因此最佳选项是:

  • 通过使用setTimeout或$timeout(使用设置为false的invokeApply参数)使断开连接异步化,或者
  • 保留一个内部标志,告诉您是否处于断开连接阶段,在那种情况下,跳过$apply

示例代码:

angular.module('app')
  .factory('socket', ['$rootScope', function ($rootScope, $timeout) {

    var socket = io.connect();

    return {
      on: function (eventName, callback) {
        socket.on(eventName, function () {  
          var args = arguments;
          $rootScope.$apply(function () {
            callback.apply(socket, args);
          });
        });
      },
      emit: function (eventName, data, callback) {
        socket.emit(eventName, data, function () {
          var args = arguments;
          $rootScope.$apply(function () {
            if (callback) {
              callback.apply(socket, args);
            }
          });
        })
      },
      disconnect: function () {
        $timeout(socket.disconnect, 0, false);
      },
      socket: socket
    };

  }]);

或者

angular.module('app')
  .factory('socket', ['$rootScope', function ($rootScope) {

    var socket = io.connect(),
        disconnecting = false;

    return {
      on: function (eventName, callback) {
        socket.on(eventName, function () {  
          var args = arguments;
          if (!disconnecting) {
            $rootScope.$apply(function () {
              callback.apply(socket, args);
            });
          } else {
            callback.apply(socket, args);
          }
        });
      },
      emit: function (eventName, data, callback) {
        socket.emit(eventName, data, function () {
          var args = arguments;
          $rootScope.$apply(function () {
            if (callback) {
              callback.apply(socket, args);
            }
          });
        })
      },
      disconnect: function () {
        disconnecting = true;
        socket.disconnect();
      },
      socket: socket
    };

  }]);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接