改进这个 AngularJS 工厂以与 socket.io 一起使用

Posted

技术标签:

【中文标题】改进这个 AngularJS 工厂以与 socket.io 一起使用【英文标题】:Improve this AngularJS factory to use with socket.io 【发布时间】:2013-01-01 13:55:58 【问题描述】:

我想在 AngularJS 中使用 socket.io。 我找到了以下工厂:

app.factory('socket', 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);
                    
                );
            )
        
    ;

它在控制器中使用如下:

function MyCtrl($scope, socket) 
    socket.on('message', function(data) 
        ...
    );
;

问题是每次访问控制器时都会添加另一个侦听器,因此当收到消息时会处理多次。

将 socket.io 与 AngularJS 集成的更好策略是什么?

编辑:我知道我不能在工厂返回任何内容并在那里进行监听,然后在控制器中使用 $rootScope.$broadcast 和 $scope.$on,但这看起来不是一个好的解决方案。

EDIT2:添加到工厂

init: function() 
            socket.removeAllListeners();

并在每个使用 socket.io 的控制器的开头调用它。

感觉仍然不是最好的解决方案。

【问题讨论】:

socketio工厂来源:html5rocks.com/en/tutorials/frameworks/angular-websockets 这也是我的问题 【参考方案1】:

每当控制器被销毁时删除套接字侦听器。 您需要像这样绑定$destroy 事件:

function MyCtrl($scope, socket) 
    socket.on('message', function(data) 
        ...
    );

    $scope.$on('$destroy', function (event) 
        socket.removeAllListeners();
        // or something like
        // socket.removeListener(this);
    );
;

更多信息请查看angularjs documentation。

【讨论】:

错误:socket.removeListeners 不是函数 @waqas 您必须将套接字封装到服务中。 我刚刚找到了解决这个问题的好方法。在下面查看我的答案。 @bmleite:如果我有一个像footerCtrl 这样的控制器,它包含在使用ng-include 的每条路线中【参考方案2】:

您可以通过包装一个 Scope 并监视要广播的 $destroy 以最少的工作量来处理这个问题,当它广播时,只从套接字中删除在上下文中添加的侦听器那个范围。请注意:以下内容尚未经过测试——我将其视为伪代码而不是实际代码。 :)

// A ScopedSocket is an object that provides `on` and `emit` methods,
// but keeps track of all listeners it registers on the socket.
// A call to `removeAllListeners` will remove all listeners on the
// socket that were created via this particular instance of ScopedSocket.

var ScopedSocket = function(socket, $rootScope) 
  this.socket = socket;
  this.$rootScope = $rootScope;
  this.listeners = [];
;

ScopedSocket.prototype.removeAllListeners = function() 
  // Remove each of the stored listeners
  for(var i = 0; i < this.listeners.length; i++) 
    var details = this.listeners[i];
    this.socket.removeListener(details.event, details.fn);
  ;
;

ScopedSocket.prototype.on = function(event, callback) 
  var socket = this.socket;
  var $rootScope = this.$rootScope;

  var wrappedCallback = function() 
    var args = arguments;
    $rootScope.$apply(function() 
      callback.apply(socket, args);
    );
  ;

  // Store the event name and callback so we can remove it later
  this.listeners.push(event: event, fn: wrappedCallback);

  socket.on(event, wrappedCallback);
;

ScopedSocket.prototype.emit = function(event, data, callback) 
  var socket = this.socket;
  var $rootScope = this.$rootScope;

  socket.emit(event, data, function() 
    var args = arguments;
    $rootScope.$apply(function() 
      if (callback) 
        callback.apply(socket, args);
      
    );
  );
;

app.factory('Socket', function($rootScope) 
  var socket = io.connect();

  // When injected into controllers, etc., Socket is a function
  // that takes a Scope and returns a ScopedSocket wrapping the
  // global Socket.IO `socket` object. When the scope is destroyed,
  // it will call `removeAllListeners` on that ScopedSocket.
  return function(scope) 
    var scopedSocket = new ScopedSocket(socket, $rootScope);
    scope.$on('$destroy', function() 
      scopedSocket.removeAllListeners();
    );
    return scopedSocket;
  ;
);

function MyController($scope, Socket) 
  var socket = Socket($scope);

  socket.on('message', function(data) 
     ...
  );
;

【讨论】:

你会直接在控制器中使用这个对象吗? 在示例的底部,您可以看到Socket 服务被注入到控制器中,以及如何使用它的示例。这是否回答了您的问题,还是您的意思是别的? 我一直在努力解决同样的问题,我想知道是否更简单的方法是将所有 socket.on 调用提升到 $rootScope 上。就我而言,传入的 socket.io 事件会影响 ng-view,因此将它们移动到 $rootScope 可以防止(我认为)@GalBen-Haim 面临的问题。这种方法的一个问题是,那些被传入的 socket.io 消息修改的模型片段也需要在 $rootScope 中。目前,我更喜欢这样做并保持解决方案简单。将 $rootScope 注入其他控制器允许它们直接访问必要的模型部分。 感谢这个优雅的解决方案!我会尝试一下,希望它有效:) @ManuelAráoz 我确定列出的代码存在问题——这就是为什么它说:“请注意:以下内容尚未经过测试——我将其视为伪代码而不是实际代码。” :) 请随时编辑更新。 (编辑)我已经更新了代码。【参考方案3】:

我会在已接受的答案中添加评论,但我不能。所以,我会写一个回复。 我遇到了同样的问题,我找到的最简单最简单的答案是here, on another post,由michaeljoser 提供。

为了方便,我把它复制在下面:

您必须将 removeAllListeners 添加到您的工厂(见下文),并在每个控制器中包含以下代码:

$scope.$on('$destroy', function (event) 
socket.removeAllListeners();
);

更新的套接字工厂:

var socket = io.connect('url');
    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);
                    
                );
            )
        ,
      removeAllListeners: function (eventName, callback) 
          socket.removeAllListeners(eventName, function() 
              var args = arguments;
              $rootScope.$apply(function () 
                callback.apply(socket, args);
              );
          ); 
      
    ;
);

它拯救了我的一天,我希望它对其他人有用!

【讨论】:

我应用了相同的代码,但它不起作用。发生多次发射问题。你能指导我吗?【参考方案4】:

在您的服务或工厂中创建函数,如下所示。

unSubscribe: function(listener) 
    socket.removeAllListeners(listener);

然后在“$destroy”事件下调用你的控制器,如下所示。

$scope.$on('$destroy', function() 
    yourServiceName.unSubscribe('eventName');
);

解决了

【讨论】:

【参考方案5】:

在阅读本文之前,我刚刚解决了一个类似的问题。我在服务中完成了这一切。

.controller('AlertCtrl', ["$scope", "$rootScope", "Socket", function($scope, $rootScope, Socket) 
    $scope.Socket = Socket;
])

// this is where the alerts are received and passed to the controller then to the view
.factory('Socket', ["$rootScope", function($rootScope) 
    var Socket = 
        alerts: [],
        url: location.protocol+'//'+location.hostname+(location.port ? ':'+location.port: ''),
        // io is coming from socket.io.js which is coming from Node.js
        socket: io.connect(this.url)
    ;
    // set up the listener once
    // having this in the controller was creating a
    // new listener every time the contoller ran/view loaded
    // has to run after Socket is created since it refers to itself
    (function() 
        Socket.socket.on('get msg', function(data) 
            if (data.alert) 
                Socket.alerts.push(data.alert);
                $rootScope.$digest();
            
        );
    ());
    return Socket;
])

【讨论】:

那么表单控制器怎么称呼呢? 看上面代码的前三行。该服务被注入到控制器中,您可以通过$scope.Socket.alerts 访问它 这应该是公认的答案。最干净的代码,解释得最多【参考方案6】:

我尝试了不同的方法,但都没有按预期工作。 在我的应用程序中,我在MainControllerGameController 中都使用了socket 工厂。当用户切换到不同的视图时,我只想删除由GameController 生成的重复事件并让MainController 运行,所以我不能使用removeAllListeners 函数。相反,我发现了一种更好的方法来避免在我的 socket 工厂内创建重复项:

app.factory('socket', function ($rootScope) 
  var socket = io.connect();

  function on(eventName, callback) 
    socket.on(eventName, function () 
      var args = arguments;

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

    // Remove duplicate listeners
    socket.removeListener(eventName, callback);
  

  function emit(eventName, data, callback) 
    socket.emit(eventName, data, function () 
      var args = arguments;

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

    // Remove duplicate listeners
    socket.removeListener(eventName, callback);
  

  return 
    on: on,
    emit: emit
  ;

【讨论】:

【参考方案7】:

不要做 app.factory,而是像这样创建一个 service(单例):

var service = angular.module('socketService', []);
service.factory('$socket', function() 
    // Your factory logic
);

然后您可以简单地将服务注入您的应用程序并在控制器中使用它,就像 $rootScope 一样。

这是一个更完整的示例,说明我是如何设置的:

// App module
var app = angular.module('app', ['app.services']);

// services
var services = angular.module('app.services', []);

// Socket service
services.factory('$socket', ['$rootScope', function(rootScope) 

    // Factory logic here

]);

// Controller
app.controller('someController', ['$scope', '$socket', function(scope, socket) 

    // Controller logic here

]);

【讨论】:

这和我的解决方案有什么区别? 阅读第一行!服务仅创建一个(单例),因此在服务中执行 io.connect 只会创建一个侦听器。我可以确认这行得通。 这根本不正确。 Angular 中的所有服务都是单例的。 Here is proof. 调用 app.serviceapp.factory 等只是服务的构造方式不同(例如,如果您的服务中只需要 $get,则使用 factory)。这个问题的问题不是创建到 Socket.IO 的多个连接,而是当控制器(及其作用域)被销毁时侦听器不会被销毁。【参考方案8】:

扩展上面 Brandon 的回答,我创建了一个服务,该服务还应该 1) 剥离像 .$$hashKey 这样留在元素上的角度标签,以及 2) 允许像 socketsof('..') 这样的命名空间套接字。 on('..'

(function (window, app, undefined) 
    'use strict';


    var ScopedSocket = function (socket, $rootScope) 
        this.socket = socket;
        this.$rootScope = $rootScope;
        this.listeners = [];
        this.childSockets = [];
    ;

    ScopedSocket.prototype.removeAllListeners = function () 
        var i;

        for (i = 0; i < this.listeners.length; i++) 
            var details = this.listeners[i];
            this.socket.removeListener(details.event, details.fn);
        

        for (i = 0; i < this.childSockets.length; i++) 
            this.childSockets[i].removeAllListeners();
        
    ;

    ScopedSocket.prototype.on = function (event, callback) 
        var socket = this.socket;
        var $rootScope = this.$rootScope;

        this.listeners.push(event: event, fn: callback);

        socket.on(event, function () 
            var args = arguments;
            $rootScope.$apply(function () 
                callback.apply(socket, args);
            );
        );
    ;

    ScopedSocket.prototype.emit = function (event, data, callback) 
        var socket = this.socket;
        var $rootScope = this.$rootScope;

        socket.emit(event, angular.fromJson(angular.toJson(data)), function () 
            var args = arguments;
            $rootScope.$apply(function () 
                if (callback) 
                    callback.apply(socket, args);
                
            );
        );
    ;

    ScopedSocket.prototype.of = function (channel) 
        var childSocket = new ScopedSocket(this.socket.of(channel), this.$rootScope);

        this.childSockets.push(childSocket);

        return childSocket;
    ;


    app.factory('Socket', ['$rootScope', function ($rootScope) 
        var socket = $rootScope.socket;

        return function(scope) 
            var scopedSocket = new ScopedSocket(socket, $rootScope);
            scope.$on('$destroy', function() 
                scopedSocket.removeAllListeners();
            );
            return scopedSocket;
        ;
    ]);
)(window, window.app);

【讨论】:

【参考方案9】:

我使用类似下面的代码。 socketsService 只实例化一次,我相信 Angular 会处理 $on 的 GC

如果您不喜欢 $broadcast/$on,可以使用一些更可靠的 Angular 消息总线实现...

app.service('socketsService', ['$rootScope', function ($rootScope) 
    var socket = window.io.connect();

    socket.on('info', function(data) 
        $rootScope.$broadcast("info_received", data);
    );

    socket.emit('ready', "Hello");
]);

app.controller("infoController",['$scope',
    function ($scope) 
        $scope.$root.$on("info_received", function(e,data)
            console.log(data);
        );
        //...
    ]);

app.run(
    ['socketsService',
        function (socketsService) 
        //...
    ]);

【讨论】:

【参考方案10】:

我通过检查监听器是否已经存在解决了这个问题。如果您有多个同时加载的控制器(想想都使用 socketIO 的不同页面模块),删除 $destroy 上的所有注册侦听器将破坏已销毁控制器和所有仍在加载的控制器的功能.

app.factory("SocketIoFactory", function ($rootScope) 
    var socket = null;
    var nodePath = "http://localhost:12345/";

    function listenerExists(eventName) 
        return socket.hasOwnProperty("$events") && socket.$events.hasOwnProperty(eventName);
    

    return 
        connect: function () 
            socket = io.connect(nodePath);
        ,
        connected: function () 
            return socket != null;
        ,
        on: function (eventName, callback) 
            if (!listenerExists(eventName)) 
                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);
                    
                );
            )
        
    ;
);

这可以通过跟踪哪个控制器注册了哪些监听器并仅删除属于已销毁控制器的监听器以清理内存来进一步改进。

【讨论】:

【参考方案11】:

我这样做是为了避免重复的听众并且效果很好。

 on: function (eventName, callback) 
  //avoid duplicated listeners
  if (listeners[eventName] != undefined) return;

  socket.on(eventName, function () 
     var args = arguments;
     $rootScope.$apply(function () 
        callback.apply(socket, args);
     );
     listeners[eventName] = true;
  );
,

【讨论】:

【参考方案12】:

浏览器刷新后,我遇到了完全相同的事件重复问题。我使用的是“工厂”,但改用了“服务”。这是我的 socket.io 包装器:

myApp.service('mysocketio',['$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);
                    
                );
            );
        
    

]);

我在我的控制器中使用这个服务并监听事件:

myApp.controller('myController', ['mysocketio', function(mysocketio)

    mysocketio.on( 'myevent', function(msg)
    
        console.log('received event: ' + msg );
    
]);

一旦我从使用工厂切换到使用服务,在浏览器刷新后我不会收到重复。

【讨论】:

【参考方案13】:

我在 AngularApp 中尝试使用上述代码,发现事件重复。 使用来自@pootzko 的相同示例,使用 SocketIoFactory

我在控制器的$destroy 中添加了一个unSubscribe(even_name),它将删除/清除socketEventListner

var app = angular.module("app", []);
..
..
..
//Create a SocketIoFactory
app.service('SocketIoFactory', function($rootScope)

    console.log("SocketIoFactory....");
    //Creating connection with server
    var protocol = 'ws:',//window.location.protocol,
        host = window.location.host,
        port = 80,
        socket = null;
    var nodePath = protocol+'//'+host+':'+port+'/';

    function listenerExists(eventName) 
        return socket.hasOwnProperty("$events") && socket.$events.hasOwnProperty(eventName);
    

    return 
        connect: function () 
            socket = io.connect(nodePath);
            console.log('SOCKET CONNECTION ... ',nodePath);
        ,
        connected: function () 
            return socket != null;
        ,
        on: function (eventName, callback) 
            if (!listenerExists(eventName)) 
                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);
                    
                );
            )
        ,
        unSubscribe: function(listener) 
            socket.removeAllListeners(listener);
        
    ;
);

..
..
..

//Use in a controller
app.controller("homeControl", ['$scope', 'SocketIoFactory', function ($scope, SocketIoFactory) 

  //Bind the events
  SocketIoFactory.on('<event_name>', function (data) 

  );

  //On destroy remove the eventListner on socketConnection
   $scope.$on('$destroy', function (event) 
        console.log('[homeControl] destroy...');
        SocketIoFactory.unSubscribe('<event_name>');
    );
]);

【讨论】:

以上是关于改进这个 AngularJS 工厂以与 socket.io 一起使用的主要内容,如果未能解决你的问题,请参考以下文章

为啥我应该在 AngularJS 中选择使用服务而不是工厂? [复制]

工厂模式的一些个人改进if else

如何使用具有构造函数参数的 TypeScript 类定义 AngularJS 工厂

AngularJS:何时使用服务而不是工厂

AngularJS:何时使用服务而不是工厂

AngularJS:为啥在说“服务”时使用“工厂”?