使用 ngResource、socket.io 和 $q 维护资源集合
Maintaining a resource collection with ngResource, socket.io and $q
我正在尝试创建一个 AngularJS 工厂,它通过从 API 中检索初始项目来自动维护资源集合,然后侦听套接字更新以保持集合最新。
angular.module("myApp").factory("myRESTFactory", function (Resource, Socket, ErrorHandler, Confirm, $mdToast, $q, $rootScope) {
var Factory = {};
// Resource is the ngResource that fetches from the API
// Factory.collection is where we'll store the items
Factory.collection = Resource.query();
// manually add something to the collection
Factory.push = function(item) {
Factory.collection.push(item);
};
// search the collection for matching objects
Factory.find = function(opts) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
resolve(_.where(Factory.collection, opts || {}));
});
});
};
// search the collection for a matching object
Factory.findOne = function(opts) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
var item = _.findWhere(collection, opts || {});
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
resolve(Factory.collection[idx]);
});
});
};
// create a new item; save to API & collection
Factory.create = function(opts) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
Resource.save(opts).$promise.then(function(item){
Factory.collection.push(item);
resolve(item);
});
});
});
};
Factory.update = function(item) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
Resource.update({_id: item._id}, item).$promise.then(function(item) {
var idx = _.findIndex(collection, function(u) {
return u._id === item._id;
});
Factory.collection[idx] = item;
resolve(item);
});
});
});
};
Factory.delete = function(item) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
Resource.delete({_id: item._id}, item).$promise.then(function(item) {
var idx = _.findIndex(collection, function(u) {
return u._id === item._id;
});
Factory.collection.splice(idx, 1);
resolve(item);
});
});
});
};
// new items received from the wire
Socket.on('new', function(item){
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
if(idx===-1) Factory.collection.push(item);
// this doesn't help
$rootScope.$apply();
});
Socket.on('update', function(item) {
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
Factory.collection[idx] = item;
// this doesn't help
$rootScope.$apply();
});
Socket.on('delete', function(item) {
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
if(idx!==-1) Factory.collection.splice(idx, 1);
});
return Factory;
});
我的后端很可靠,套接字消息可以正确通过。但是,如果使用任何工厂方法,控制器不会响应集合的更新。
即
这有效(响应集合的套接字更新):
$scope.users = User.collection;
这不起作用(它最初加载用户但不知道集合的更新):
User.findOne({ _id: $routeParams.user_id }).then(function(user){
$scope.user = user;
});
如何让我的控制器响应集合更改的更新?
更新:
我能够通过更改以下内容在控制器中实施解决方法:
if($routeParams.user_id) {
User.findOne({ _id: $routeParams.user_id }).then(function(user){
$scope.user = user;
});
}
为此:
$scope.$watchCollection('users', function() {
if($routeParams.user_id) {
User.findOne({ _id: $routeParams.user_id }).then(function(user){
$scope.user = user;
});
}
});
但是,没有人喜欢变通办法,尤其是当它涉及控制器中的冗余代码时。我正在为可以在工厂内解决此问题的人悬赏这个问题。
- 不要在
Factory
上公开 collection
属性,将其保留为局部变量。
- 在 Factory 上创建一个新的暴露的
getter/setter
来代理本地变量。
- 在您的
find
方法内部使用 getter/setter Object。
像这样:
// internal variable
var collection = Resource.query();
// exposed 'proxy' object
Object.defineProperty(Factory, 'collection', {
get: function () {
return collection;
},
set: function (item) {
// If we got a finite Integer.
if (_.isFinite(item)) {
collection.splice(item, 1);
}
// Check if the given item is already in the collection.
var idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
if (idx) {
// Update the item in the collection.
collection[idx] = item;
} else {
// Push the new item to the collection.
collection.push(item);
}
// Trigger the $digest cycle as a last step after modifying the collection.
// Can safely be moved to Socket listeners so as to not trigger unnecessary $digests from an angular function.
$rootScope.$digest();
}
});
/**
* Change all calls from 'Factory.collection.push(item)' to
* 'Factory.collection = item;'
*
* Change all calls from 'Factory.collection[idx] = item' to
* 'Factory.collection = item;'
*
* Change all calls from 'Factory.collection.splice(idx, 1) to
* 'Factory.collection = idx;'
*
*/
现在,看看 非 angular 方 如何修改您的 collection(在这种情况下即 Sockets),您 将 需要触发一个 $digest
循环来反映 collection 的新状态。
如果您只对在单个 $scope
(或多个,但不是 cross-scope)中保持 collection 同步感兴趣,我会附上 $scope
到工厂,那里 运行 $digest
而不是 $rootScope
。这将为您节省一点性能。
here's a jsbin 展示了 Object.getter
的使用如何使您的 collection 保持同步并允许您查找最近添加到的项目collection。
我在 jsbin 中选择了 setTimeout
,这样就不会通过使用 $interval
.[ 触发自动 $digests
=26=]
显然 jsbin 非常准系统;没有被洗牌的承诺,没有套接字连接。我只想展示如何保持同步。
我承认 Factory.collection = value
看起来很糟糕,但您可以借助包装函数将其隐藏起来,使其更漂亮/更好读。
工厂方法的解决方案是 return 一个空的 object/array 稍后填充(类似于 ngResource 的工作方式)。然后将套接字侦听器附加到 return objects/arrays 和主 Factory.collection 数组。
angular.module("myApp").factory("myRESTFactory",
function (Resource, Socket, ErrorHandler, Confirm, $mdToast, $q) {
var Factory = {};
// Resource is the ngResource that fetches from the API
// Factory.collection is where we'll store the items
Factory.collection = Resource.query();
// This function attaches socket listeners to given array
// or object and automatically updates it based on updates
// from the websocket
var socketify = function(thing, opts){
// if attaching to array
// i.e. myRESTFactory.find({name: "John"})
// was used, returning an array
if(angular.isArray(thing)) {
Socket.on('new', function(item){
// push the object to the array only if it
// matches the query object
var matches = $filter('find')([item], opts);
if(matches.length){
var idx = _.findIndex(thing, function(u) {
return u._id === item._id;
});
if(idx===-1) thing.push(item);
}
});
Socket.on('update', function(item) {
var idx = _.findIndex(thing, function(u) {
return u._id === item._id;
});
var matches = $filter('find')([item], opts);
// if the object matches the query obj,
if(matches.length){
// and is already in the array
if(idx > -1){
// then update it
thing[idx] = item;
// otherwise
} else {
// add it to the array
thing.push(item);
}
// if the object doesn't match the query
// object anymore,
} else {
// and is currently in the array
if(idx > -1){
// then splice it out
thing.splice(idx, 1);
}
}
});
Socket.on('delete', function(item) {
...
});
// if attaching to object
// i.e. myRESTFactory.findOne({name: "John"})
// was used, returning an object
} else if (angular.isObject(thing)) {
Socket.on('update', function(item) {
...
});
Socket.on('delete', function(item) {
...
});
}
// attach the socket listeners to the factory
// collection so it is automatically maintained
// by updates from socket.io
socketify(Factory.collection);
// return an array of results that match
// the query object, opts
Factory.find = function(opts) {
// an empty array to hold matching results
var results = [];
// once the API responds,
Factory.collection.$promise.then(function(){
// see which items match
var matches = $filter('find')(Factory.collection, opts);
// and add them to the results array
for(var i = matches.length - 1; i >= 0; i--) {
results.push(matches[i]);
}
});
// attach socket listeners to the results
// array so that it is automatically maintained
socketify(results, opts);
// return results now. initially it is empty, but
// it will be populated with the matches once
// the api responds, as well as pushed, spliced,
// and updated since we socketified it
return results;
};
Factory.findOne = function(opts) {
var result = {};
Factory.collection.$promise.then(function(){
result = _.extend(result, $filter('findOne')(Factory.collection, opts));
});
socketify(result);
return result;
};
...
return Factory;
};
之所以如此出色,是因为您的控制器既简单又强大。例如,
$scope.users = User.find();
这 return 是您可以在您的视图中使用的所有用户的数组;在 ng-repeat 或其他东西中。它会通过套接字的更新自动成为 updated/spliced/pushed,你不需要做任何额外的事情来获得它。但是等等,还有更多。
$scope.users = User.find({status: "active"});
这将 return 所有活动用户的数组。该数组也将由我们的 socketify 函数自动管理和过滤。所以如果一个用户从"active"更新到"inactive",他是自动从数组中拼接出来的。反之亦然。从 "inactive" 更新到 "active" 的用户会自动添加到数组中。
其他方法也是如此。
$scope.user = User.findOne({firstname: "Jon"});
如果 Jon 的电子邮件发生变化,控制器中的对象也会更新。如果他的名字更改为 "Jonathan",$scope.user
将变为空对象。更好的用户体验是进行软删除或仅以某种方式将用户标记为已删除,但可以稍后添加。
不需要 $watch
、$watchCollection
、$digest
、$broadcast
,它只是有效。
我正在尝试创建一个 AngularJS 工厂,它通过从 API 中检索初始项目来自动维护资源集合,然后侦听套接字更新以保持集合最新。
angular.module("myApp").factory("myRESTFactory", function (Resource, Socket, ErrorHandler, Confirm, $mdToast, $q, $rootScope) {
var Factory = {};
// Resource is the ngResource that fetches from the API
// Factory.collection is where we'll store the items
Factory.collection = Resource.query();
// manually add something to the collection
Factory.push = function(item) {
Factory.collection.push(item);
};
// search the collection for matching objects
Factory.find = function(opts) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
resolve(_.where(Factory.collection, opts || {}));
});
});
};
// search the collection for a matching object
Factory.findOne = function(opts) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
var item = _.findWhere(collection, opts || {});
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
resolve(Factory.collection[idx]);
});
});
};
// create a new item; save to API & collection
Factory.create = function(opts) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
Resource.save(opts).$promise.then(function(item){
Factory.collection.push(item);
resolve(item);
});
});
});
};
Factory.update = function(item) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
Resource.update({_id: item._id}, item).$promise.then(function(item) {
var idx = _.findIndex(collection, function(u) {
return u._id === item._id;
});
Factory.collection[idx] = item;
resolve(item);
});
});
});
};
Factory.delete = function(item) {
return $q(function(resolve, reject) {
Factory.collection.$promise.then(function(collection){
Resource.delete({_id: item._id}, item).$promise.then(function(item) {
var idx = _.findIndex(collection, function(u) {
return u._id === item._id;
});
Factory.collection.splice(idx, 1);
resolve(item);
});
});
});
};
// new items received from the wire
Socket.on('new', function(item){
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
if(idx===-1) Factory.collection.push(item);
// this doesn't help
$rootScope.$apply();
});
Socket.on('update', function(item) {
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
Factory.collection[idx] = item;
// this doesn't help
$rootScope.$apply();
});
Socket.on('delete', function(item) {
idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
if(idx!==-1) Factory.collection.splice(idx, 1);
});
return Factory;
});
我的后端很可靠,套接字消息可以正确通过。但是,如果使用任何工厂方法,控制器不会响应集合的更新。
即
这有效(响应集合的套接字更新):
$scope.users = User.collection;
这不起作用(它最初加载用户但不知道集合的更新):
User.findOne({ _id: $routeParams.user_id }).then(function(user){
$scope.user = user;
});
如何让我的控制器响应集合更改的更新?
更新:
我能够通过更改以下内容在控制器中实施解决方法:
if($routeParams.user_id) {
User.findOne({ _id: $routeParams.user_id }).then(function(user){
$scope.user = user;
});
}
为此:
$scope.$watchCollection('users', function() {
if($routeParams.user_id) {
User.findOne({ _id: $routeParams.user_id }).then(function(user){
$scope.user = user;
});
}
});
但是,没有人喜欢变通办法,尤其是当它涉及控制器中的冗余代码时。我正在为可以在工厂内解决此问题的人悬赏这个问题。
- 不要在
Factory
上公开collection
属性,将其保留为局部变量。 - 在 Factory 上创建一个新的暴露的
getter/setter
来代理本地变量。 - 在您的
find
方法内部使用 getter/setter Object。
像这样:
// internal variable
var collection = Resource.query();
// exposed 'proxy' object
Object.defineProperty(Factory, 'collection', {
get: function () {
return collection;
},
set: function (item) {
// If we got a finite Integer.
if (_.isFinite(item)) {
collection.splice(item, 1);
}
// Check if the given item is already in the collection.
var idx = _.findIndex(Factory.collection, function(u) {
return u._id === item._id;
});
if (idx) {
// Update the item in the collection.
collection[idx] = item;
} else {
// Push the new item to the collection.
collection.push(item);
}
// Trigger the $digest cycle as a last step after modifying the collection.
// Can safely be moved to Socket listeners so as to not trigger unnecessary $digests from an angular function.
$rootScope.$digest();
}
});
/**
* Change all calls from 'Factory.collection.push(item)' to
* 'Factory.collection = item;'
*
* Change all calls from 'Factory.collection[idx] = item' to
* 'Factory.collection = item;'
*
* Change all calls from 'Factory.collection.splice(idx, 1) to
* 'Factory.collection = idx;'
*
*/
现在,看看 非 angular 方 如何修改您的 collection(在这种情况下即 Sockets),您 将 需要触发一个 $digest
循环来反映 collection 的新状态。
如果您只对在单个 $scope
(或多个,但不是 cross-scope)中保持 collection 同步感兴趣,我会附上 $scope
到工厂,那里 运行 $digest
而不是 $rootScope
。这将为您节省一点性能。
here's a jsbin 展示了 Object.getter
的使用如何使您的 collection 保持同步并允许您查找最近添加到的项目collection。
我在 jsbin 中选择了 setTimeout
,这样就不会通过使用 $interval
.[ 触发自动 $digests
=26=]
显然 jsbin 非常准系统;没有被洗牌的承诺,没有套接字连接。我只想展示如何保持同步。
我承认 Factory.collection = value
看起来很糟糕,但您可以借助包装函数将其隐藏起来,使其更漂亮/更好读。
工厂方法的解决方案是 return 一个空的 object/array 稍后填充(类似于 ngResource 的工作方式)。然后将套接字侦听器附加到 return objects/arrays 和主 Factory.collection 数组。
angular.module("myApp").factory("myRESTFactory",
function (Resource, Socket, ErrorHandler, Confirm, $mdToast, $q) {
var Factory = {};
// Resource is the ngResource that fetches from the API
// Factory.collection is where we'll store the items
Factory.collection = Resource.query();
// This function attaches socket listeners to given array
// or object and automatically updates it based on updates
// from the websocket
var socketify = function(thing, opts){
// if attaching to array
// i.e. myRESTFactory.find({name: "John"})
// was used, returning an array
if(angular.isArray(thing)) {
Socket.on('new', function(item){
// push the object to the array only if it
// matches the query object
var matches = $filter('find')([item], opts);
if(matches.length){
var idx = _.findIndex(thing, function(u) {
return u._id === item._id;
});
if(idx===-1) thing.push(item);
}
});
Socket.on('update', function(item) {
var idx = _.findIndex(thing, function(u) {
return u._id === item._id;
});
var matches = $filter('find')([item], opts);
// if the object matches the query obj,
if(matches.length){
// and is already in the array
if(idx > -1){
// then update it
thing[idx] = item;
// otherwise
} else {
// add it to the array
thing.push(item);
}
// if the object doesn't match the query
// object anymore,
} else {
// and is currently in the array
if(idx > -1){
// then splice it out
thing.splice(idx, 1);
}
}
});
Socket.on('delete', function(item) {
...
});
// if attaching to object
// i.e. myRESTFactory.findOne({name: "John"})
// was used, returning an object
} else if (angular.isObject(thing)) {
Socket.on('update', function(item) {
...
});
Socket.on('delete', function(item) {
...
});
}
// attach the socket listeners to the factory
// collection so it is automatically maintained
// by updates from socket.io
socketify(Factory.collection);
// return an array of results that match
// the query object, opts
Factory.find = function(opts) {
// an empty array to hold matching results
var results = [];
// once the API responds,
Factory.collection.$promise.then(function(){
// see which items match
var matches = $filter('find')(Factory.collection, opts);
// and add them to the results array
for(var i = matches.length - 1; i >= 0; i--) {
results.push(matches[i]);
}
});
// attach socket listeners to the results
// array so that it is automatically maintained
socketify(results, opts);
// return results now. initially it is empty, but
// it will be populated with the matches once
// the api responds, as well as pushed, spliced,
// and updated since we socketified it
return results;
};
Factory.findOne = function(opts) {
var result = {};
Factory.collection.$promise.then(function(){
result = _.extend(result, $filter('findOne')(Factory.collection, opts));
});
socketify(result);
return result;
};
...
return Factory;
};
之所以如此出色,是因为您的控制器既简单又强大。例如,
$scope.users = User.find();
这 return 是您可以在您的视图中使用的所有用户的数组;在 ng-repeat 或其他东西中。它会通过套接字的更新自动成为 updated/spliced/pushed,你不需要做任何额外的事情来获得它。但是等等,还有更多。
$scope.users = User.find({status: "active"});
这将 return 所有活动用户的数组。该数组也将由我们的 socketify 函数自动管理和过滤。所以如果一个用户从"active"更新到"inactive",他是自动从数组中拼接出来的。反之亦然。从 "inactive" 更新到 "active" 的用户会自动添加到数组中。
其他方法也是如此。
$scope.user = User.findOne({firstname: "Jon"});
如果 Jon 的电子邮件发生变化,控制器中的对象也会更新。如果他的名字更改为 "Jonathan",$scope.user
将变为空对象。更好的用户体验是进行软删除或仅以某种方式将用户标记为已删除,但可以稍后添加。
不需要 $watch
、$watchCollection
、$digest
、$broadcast
,它只是有效。