使用 ngResource、socket.io 和 $q 维护资源集合

Posted

技术标签:

【中文标题】使用 ngResource、socket.io 和 $q 维护资源集合【英文标题】:Maintaining a resource collection with ngResource, socket.io and $q 【发布时间】:2015-09-28 06:23:08 【问题描述】:

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

但是,没有人喜欢变通方法,尤其是当它涉及控制器中的冗余代码时。我正在为可以在工厂内解决此问题的人添加悬赏。

【问题讨论】:

【参考方案1】: 不要在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;'
 * 
 */

现在,看看非角方如何修改您的集合(在本例中为套接字),您需要触发一个$digest 循环来反映集合的新状态。

如果您只对在单个 $scope(或多个,但不是跨范围)中保持集合同步感兴趣,我会将上述 $scope 附加到工厂,然后运行 ​​@987654330 @ 那里而不是$rootScope。这将为您节省一点性能。

here's a jsbin 展示了Object.getter 的使用如何使您的收藏保持同步,并允许您查找最近添加到收藏中的项目。

我在 jsbin 中选择了setTimeout,以免通过使用$interval 触发自动$digests

显然 jsbin 是非常简单的;没有任何承诺被洗牌,没有套接字连接。我只是想展示如何保持同步。


我承认Factory.collection = value 看起来很糟糕,但您可以借助包装函数将其隐藏起来,使其更漂亮/更好读。

【讨论】:

这很有趣并且(我认为)很接近。我正在使用 jsbin,但还没有完成像 $scope.boundCollection = MyFactory.find(id: 1); 这样的事情,这是这个工厂的用例。由于带有id: 1 的项目尚不存在,因此它不会呈现任何内容,即使稍后将该项目添加到集合中也是如此。 嗯。我一定在这里错过了什么;您是说用例是在集合中找到一个尚未添加到所述集合中的单个实体吗?如果是这样,我想我有办法解决这个问题。 更一般地说,目标是从控制器查询集合并让返回的对象是“活的”。即,如果它在集合中更新,则更改会自动反映在视图中(触发 $digest)。例如,$scope.user = MyFactory.find(id: 1); 然后在视图中,<h1>user.name</h1> 目标是如果该用户的名称通过套接字或其他方式更改,<h1> 会自动更新。 啊,我明白了!这使它更容易理解,谢谢。期待尽快对我的回答进行编辑™。 "soon™" - 只要我弄清楚这将如何可行。需要编写的样板数量相当多。现在,我会说当 Socket 命中时使用 $watch 和/或 $broadcast【参考方案2】:

解决方案是工厂方法返回一个空对象/数组以供稍后填充(类似于 ngResource 的工作方式)。然后将套接字侦听器附加到这些返回对象/数组和主 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();

这将返回您可以在视图中使用的所有用户的数组;在 ng-repeat 或其他东西中。它将通过来自套接字的更新自动更新/拼接/推送,您不需要做任何额外的事情来获得它。但是等等,还有更多。

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

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

其他方法也是如此。

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

如果 Jon 的电子邮件发生变化,控制器中的对象也会更新。如果他的名字更改为“Jonathan”,$scope.user 将成为一个空对象。更好的用户体验是软删除或只是以某种方式将用户标记为已删除,但可以稍后添加。

没有$watch$watchCollection$digest$broadcast,是必需的——它可以正常工作。

【讨论】:

那里非常棒的解决方案!我要添加的一件事是在集合和/或$scope 被销毁后清理socket.on 侦听器。很高兴能提供一些帮助:) 谢谢!我也是这么想的,但鉴于工厂没有提供$scope,你将如何检测它的破坏? 我会在工厂创建另一个实用方法。 Factory.attach 或类似的东西,并在工厂内存储对范围的引用。监听$destroy 事件,清理并移除引用。 这是个好主意。我会多考虑一下,看看它是否可能没有额外的控制器代码,因为这不符合我对灯光控制器的过度热情。 :)

以上是关于使用 ngResource、socket.io 和 $q 维护资源集合的主要内容,如果未能解决你的问题,请参考以下文章

与 ngResource 相比,使用 Restangular 有啥优势?

我如何在 angularjs 中使用 Restful。我使用了 ngResource 但它不起作用。如果我使用 ngResource,js 文件 nt 正在执行

链接 ngResource $promise 成功和错误

角度 ngResource 将附加参数传递给 url

ngResource和REST介绍

根据所选项目在 ngResource (angularjs) 中查询数据