15

I have a model, defined using $resource, that I am successfully loading.

Each loaded instance is, as promised, an instance of the class I defined.

(The example below is from the Angular docs. In it, User.get results in an object that is an instanceof User.)

var User = $resource('/user/:userId', {userId:'@id'});

However, imagine each User comes over the wire like this:

{
  "username": "Bob",
  "preferences": [
    {
      "id": 1,
      "title": "foo",
      "value": false
    }
  ] 
}

I defined a Preference factory that adds valuable methods to Preference objects. But when a User loads, those preferences aren’t Preferences, naturally.

I attempted this:

User.prototype.constructor = function(obj) {
  _.extend(this, obj);
  this.items = _.map(this.preferences, function(pref) {
    return new Preference(pref);
  });
  console.log('Our constructor ran'); // never logs anything
}

But it has no effect and never logs anything.

How can I make each item in my Users’ preferences array an instance of Preference?

4

5 回答 5

12

$resource 是一个简单的实现,缺少这样的东西。

User.prototype.constructor不会做任何事;与其他库不同,angular 不会试图表现得像面向对象一样。这只是javascript。

..但幸运的是,你有承诺和 javascript :-)。这是您可以做到的一种方法:

function wrapPreferences(user) {
  user.preferences = _.map(user.preferences, function(p) {
    return new Preference(p);
  });
  return user;
}

var get = User.get;
User.get = function() {
  return get.apply(User, arguments).$then(wrapPreferences);
};
var $get = User.prototype.$get;
User.prototype.$get = function() {
  return $get.apply(this, arguments).$then(wrapPreferences);
};

您可以将其抽象为装饰任何资源方法的方法:它需要一个对象、一个方法名称数组和一个装饰器函数。

function decorateResource(Resource, methodNames, decorator) {
  _.forEach(methodNames, function(methodName) {
    var method = Resource[methodName];
    Resource[methodName] = function() {
      return method.apply(Resource, arguments).$then(decorator);
    };
    var $method = Resource.prototype[methodName];
    Resource.prototype[methodName] = function() {
      return $method.apply(this, arguments).$then(decorator);
    };
  });
}
decorateResource(User, ['get', 'query'], wrapPreferences);
于 2013-05-09T02:48:13.327 回答
5

您可以通过覆盖内置资源操作来转换请求和响应来做到这一点(请参阅文档中的 transformRequest 和 transformResponse。):

var m = angular.module('my-app.resources');
m.factory('User', [
          '$resource',
  function($resource) {

    function transformUserFromServer(user) {
      // Pass Preference directly to map since, in your example, it takes a JSON preference as an argument
      user.preferences = _.map(user.preferences, Preference);
      return user;
    }

    function transformUserForServer(user) {
      // Make a copy so that you don't make your existing object invalid
      // E.g., changes here may invalidate your model for its form, 
      //  resulting in flashes of error messages while the request is 
      //  running and before you transfer to a new page
      var copy = angular.copy(user);
      copy.preferences = _.map(user.preferences, function(pref) {
        // This may be unnecessary in your case, if your Preference model is acceptable in JSON format for your server
        return {
          id: pref.id,
          title: pref.title,
          value: pref.value
        };
      });

      return copy;
    }

    function transformUsersFromServer(users) {
      return _.map(users, transformUserFromServer);
    }

    return $resource('/user/:userId', {
        userId: '@id'
      }, {
        get: {
          method: 'GET',
          transformRequest: [
            angular.fromJson,
            transformUserFromServer
          ]
        },
        query: {
          method: 'GET',
          isArray: true,
          transformRequest: [
            angular.fromJson,
            transformUsersFromServer
          ]
        },
        save: {
          method: 'POST',
          // This may be unnecessary in your case, if your Preference model is acceptable in JSON format for your server
          transformRequest: [
            transformUserForServer,
            angular.toJson
          ],
          // But you'll probably still want to transform the response
          transformResponse: [
            angular.fromJson,
            transformUserFromServer
          ]
        },
        // update is not a built-in $resource method, but we use it so that our URLs are more RESTful
        update: {
          method: 'PUT',
          // Same comments above apply in the update case.
          transformRequest: [
            transformUserForServer,
            angular.toJson
          ],
          transformResponse: [
            angular.fromJson,
            transformUserFromServer
          ]
        }
      }
    );
  };
]);
于 2014-10-24T17:18:48.387 回答
3

我正在寻找与您相同的问题的解决方案。我想出了以下方法。
此示例基于商品而不是用户,作为域实体。另外,请注意这是整个内容的精简版本,在我的情况下,它跨越了一些文件:

域实体自定义类:

function Offer(resource) {
    // Class constructor function
    // ...
}

angular.extend(Offer.prototype, {
    // ...

    _init: function (resource) {
        this._initAsEmpty();

        if (typeof resource == 'undefined') {
            // no resource passed, leave empty
        }
        else {
            // resource passed, copy offer from that
            this.copyFromResource(resource);
        }
    },

    copyFromResource: function (resource) {
        angular.extend(this, resource);
        // possibly some more logic to copy deep references
    },

    // ...
});

经典角度自定义资源:

var offerResource = $resource(/* .. */);

自定义存储库,由服务工厂传递给控制器​​:

function OfferRepository() {  
    // ...
}

angular.extend(OfferRepository.prototype, {
    // ...

    getById: function (offerId, success, error) {

        var asyncResource = offerResource.get({
            offerId: offerId

        }, function (resource) {
            asyncOffer.copyFromResource(resource);

            (success || angular.noop)(asyncOffer);

        }, function (response) {
            (error || angular.noop)(response);

        });

        var asyncOffer = new offerModels.Offer(asyncResource);

        return asyncOffer;
    },

    // ...
});

最引人注目的部分是:

  • 自定义实体类,能够从资源实例开始构建/填充自身(可能具有深度复制功能,例如报价中的职位)
  • 包装资源的自定义存储库类。这不会返回经典的异步资源答案,而是返回一个自定义实体实例,稍后它会用刚刚加载的资源填充它。
于 2013-07-03T23:57:33.833 回答
2

尝试修改原型对象的构造函数属性无论如何都不会达到您的预期,请查看这里的非常好的帖子。

要真正理解发生了什么,应该查看模块的源代码ngResource——那里有很多东西在起作用,但重要的是$resource工厂返回一个普通的 JavaScript 函数(真的,还有什么)。使用记录的参数调用此函数会返回一个Resource构造函数对象,该对象在resourceFactory.

您可能还记得,AngularJS 服务是单例的,这意味着$resource每次调用都会返回相同的函数(在本例中为resourceFactory)。重要的一点是,每次评估此函数时,Resource都会返回一个新的构造函数对象,这意味着您可以安全地在其上创建自己的函数原型,而不必担心这会污染Resource全局所有实例。

这是一个您可以像原始$resource工厂一样使用的服务,同时定义您自己的自定义方法,这些方法将在其所有实例上可用:

angular.module('app').factory('ExtendedResourceFactory', ['$resource',
  function($resource) {                                                        
    function ExtendedResourceFactory() {
      var Resource = $resource.apply(this, arguments);

      Resource.prototype.myCustomFunction = function() {
        ...
      };

      return Resource;
    }

    return ExtendedResourceFactory;
  }
]);

在内部myCustomFunction,您可以访问从服务器返回的数据,因此您可以使用this.preferences和返回您想要构建的任何自定义类。

于 2015-02-12T09:53:24.887 回答
1

transformResponse做这项工作。考虑示例(我想使用自动链接器来格式化响应内容)。

return $resource('posts/:postId', {
    postId: '@_id'
}, {
    get : {
        transformResponse : function(data) {
            var response = angular.fromJson( data );
            response.content = Autolinker.link(response.content);
            return response;
        }
    },
    update: {
        method: 'PUT'
} });
于 2015-01-10T22:38:36.740 回答