用ngResource、socket维护一个资源集合.IO和$q

Maintaining a resource collection with ngResource, socket.io and $q

本文关键字:集合 资源 一个 IO ngResource socket 维护      更新时间:2023-09-26

我正在尝试创建一个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;
});

我的后端是可靠的,套接字消息正确通过。但是,如果使用了任何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对象。

像这样:

// 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;'
 * 
 */
现在,看看非角方如何修改集合(在本例中即socket),您需要触发$digest循环以反映集合的新状态。

如果您只对在单个$scope(或多个,但不是跨作用域)中保持集合同步感兴趣,我会将$scope附加到工厂,并在那里运行$digest而不是$rootScope。这将为您节省一点性能。

这里有一个jsbin展示了Object.getter的使用将如何保持你的集合同步,并允许你找到最近添加到集合中的项目。

我在jsbin中选择了setTimeout,这样就不会通过使用$interval来触发自动的$digests

显然jsbin是非常简陋的;没有承诺被打乱,没有套接字连接。我只是想展示一下如何保持同步。


我承认Factory.collection = value看起来很糟糕,但是你可以在包装函数的帮助下隐藏它,使它更漂亮/更好读。

解决方案是让工厂方法返回一个空的对象/数组以便稍后填充(类似于ngResource的工作方式)。然后将套接字侦听器连接到这些返回对象/数组和主工厂。数组集合。

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();

返回一个包含所有用户的数组,可以在视图中使用;在一个n -repeat或别的什么。它将自动被来自套接字的更新更新/拼接/推送,你不需要做任何额外的事情来获得它。但是等等,还有更多。

$scope.users = User.find({status: "active"});

返回一个包含所有活动用户的数组。该数组也将由socketify函数自动管理和过滤。因此,如果一个用户从"活跃"更新为"不活跃",他将自动从数组中拼接出来。反之亦然;从"非活动"更新为"活动"的用户将自动添加到数组中。

其他方法也是如此。

$scope.user = User.findOne({firstname: "Jon"});

如果Jon的电子邮件更改,则控制器中的对象更新。如果他的名字变成了"Jonathan",$scope.user就变成了一个空对象。更好的用户体验应该是软删除,或者只是以某种方式标记用户已删除,但这可以稍后添加。

不需要$watch, $watchCollection, $digest, $broadcast -它只是工作