Улучшение этой фабрики AngularJS для использования с socket.io
Я хочу использовать socket.io в AngularJS. Я нашел следующую фабрику:
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.
до сих пор не чувствую себя лучшим решением.
13 ответов
Удалите слушатели сокета всякий раз, когда контроллер разрушен. Вам нужно будет связать $destroy
событие как это:
function MyCtrl($scope, socket) {
socket.on('message', function(data) {
...
});
$scope.$on('$destroy', function (event) {
socket.removeAllListeners();
// or something like
// socket.removeListener(this);
});
};
Для получения дополнительной информации проверьте документацию angularjs.
Вы могли бы справиться с этим с минимальным объемом работы, завернув 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) {
...
});
};
Я бы добавил комментарий к принятому ответу, но не могу. Итак, я напишу ответ. У меня была та же проблема, и самый простой и простой ответ, который я нашел, это тот, который вы можете найти здесь, в другом сообщении, предоставленном 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);
});
});
}
};
});
Это спасло мой день, я надеюсь, что это будет полезно для кого-то еще!
Создать функцию в вашем сервисе или на заводе, как показано ниже.
unSubscribe: function(listener) {
socket.removeAllListeners(listener);
}
а затем вызовите ваш контроллер в событии "$destroy", как показано ниже.
$scope.$on('$destroy', function() {
yourServiceName.unSubscribe('eventName');
});
это решить
Я пробовал разные способы, но ничего не получилось, как ожидалось. В моем приложении я использую socket
завод в обоих MainController
и GameController
, Когда пользователь переключается на другое представление, я хочу удалить только дубликаты событий, сгенерированные 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
};
}
Я только что решил подобную проблему, прежде чем прочитать это. Я сделал все это в Сервисе.
.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;
}])
Я попытался с помощью вышеуказанного кода в моем AngularApp и обнаружил, что события дублируются. С тем же примером из @pootzko, использующим SocketIoFactory
Я добавил unSubscribe(even_name)
внутри $destroy
Контроллера, который удалит / очистит 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>');
});
}]);
В продолжение ответа Брэндона, приведенного выше, я создал сервис, который должен дополнительно: 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);
Я использую что-то вроде кода ниже. socketsService создается только один раз, и я полагаю, что Angular позаботится о GC the $on
Если вам не нравится $broadcast/$on, есть несколько более надежных реализаций Message Bus для 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) {
//...
}]);
Я решил эту проблему, проверив, существует ли уже прослушиватель. Если у вас есть несколько контроллеров, которые все загружены одновременно (представьте себе разные модули страниц, которые все используют 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);
}
});
})
}
};
});
Это может быть улучшено путем отслеживания того, какие прослушиватели были зарегистрированы каким контроллером, и удаления только прослушивателей, принадлежащих разрушенным контроллерам, для очистки памяти.
У меня возникла точно такая же проблема повторяющихся событий после обновления браузера. Я использовал "фабрику", но переключился на "сервис". Вот моя оболочка 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 );
}
}]);
После того, как я переключился с использования фабрики на использование службы, я не получаю дубликаты после обновления браузера.
Я делаю это, чтобы избежать дублирования слушателей и работает довольно хорошо.
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;
});
},
Вместо app.factory создайте сервис (singleton), например, так:
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
}]);