Как я могу расширить конструктор ресурса 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'
} });