使用 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,它只是有效。