Как я могу расширить конструктор ресурса AngularJS ($ resource)?

У меня есть модель, определенная с помощью $resource, которую я успешно загружаю.

Каждый загруженный экземпляр, как и было обещано, представляет собой экземпляр класса I.

(Ниже приведен пример из Angular docs. В нем User.get выводится объект instanceof User.)

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

Однако представьте, что каждый пользователь приходит через провод следующим образом:

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

Я определил a Preference factory, который добавляет ценные методы к объектам Preference. Но когда пользователь загружает, те preferences arent Preference s, естественно.

Я попытался это сделать:

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
}

Но он не имеет никакого эффекта и никогда ничего не записывает.

Как я могу сделать каждый элемент в моем массиве User s preferences экземпляром Preference?

Ответы

Ответ 1

$resource - простая реализация, и в ней нет таких вещей.

User.prototype.constructor ничего не сделает; angular не пытается действовать как объектно-ориентированный, в отличие от других библиотек. Это просто javascript.

.. Но, к счастью, у вас есть promises и 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);

Ответ 2

Вы можете сделать это, переопределив встроенные действия с ресурсами, чтобы преобразовать запрос и ответ (см. 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
          ]
        }
      }
    );
  };
]);

Ответ 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
    },

    // ...
});

Классический angular пользовательский ресурс:

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

Пользовательский репозиторий, переданный контроллеру службой factory:

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;
    },

    // ...
});

Наиболее заметными являются:

  • класс пользовательских сущностей, который может конструировать/заполнять себя, начиная с экземпляра ресурса (возможно, с возможностями глубокой копии, например, в позициях предложения)
  • пользовательский класс репозитория, который обертывает ресурс. Это не возвращает классический ответ асинхронного ресурса, но вместо этого возвращает экземпляр настраиваемого объекта, а позже он заполняет его только загруженным ресурсом.

Ответ 4

Попытка изменить свойство конструктора объекта-прототипа не будет делать то, что вы ожидаете в любом случае, пожалуйста, взгляните на очень приятное сообщение .

Чтобы понять, что происходит, нужно посмотреть исходный код модуля ngResource - есть много вещей на работе там, но важно то, что $resource factory возвращает простую функцию JavaScript (действительно, что еще). Вызов этой функции с документированными параметрами возвращает объект конструктора Resource, который определен конфиденциально в resourceFactory.

Как вы помните, услуги AngularJS являются одноточечными, что означает, что вызов $resource будет возвращать одну и ту же функцию каждый раз (в этом случае resourceFactory). Важным выводом является то, что каждый раз, когда эта функция оценивается, возвращается новый объект конструктора Resource, что означает, что вы можете безопасно прототипировать свои собственные функции, не опасаясь, что это будет загрязнять все экземпляры Resource глобально.

Вот сервис, который вы можете использовать как оригинальный $resource factory, определяя свои собственные методы, которые будут доступны во всех его экземплярах:

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 и возвращать какой-либо пользовательский класс, который хотите создать.

Ответ 5

transformResponse выполняет задание. Рассмотрим пример (я хотел использовать Autolinker для форматирования содержимого ответа).

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'
} });