J’utilise AngularJS pour interagir avec un service RESTful
, en utilisant $resource
pour résumer les différentes entités exposées. Certaines de ces entités sont des images. Je dois donc pouvoir utiliser l’action de save
de $resource
object “object” pour envoyer des données binarys et des champs de texte dans la même requête.
Comment puis-je utiliser le service de $resource
d’AngularJS pour envoyer des données et télécharger des images vers un service Web reposant dans une seule requête POST
?
J’ai cherché loin et, même si je l’avais raté, je n’ai pas trouvé de solution à ce problème: télécharger des fichiers en utilisant une action $ resource.
Faisons cet exemple: notre service RESTful nous permet d’accéder aux images en faisant des requêtes au sharepoint terminaison /images/
. Chaque Image
a un titre, une description et le chemin d’access au fichier image. En utilisant le service RESTful, nous pouvons tous les obtenir ( GET /images/
), un seul ( GET /images/1
) ou en append un ( POST /images
). Angular nous permet d’utiliser le service de ressources $ pour accomplir cette tâche facilement, mais ne permet pas le téléchargement de fichiers – ce qui est nécessaire pour la troisième action – et ils ne semblent pas vouloir le supporter à tout moment bientôt ). Comment, alors, utiliserions-nous le service très pratique de ressources $ s’il ne peut pas gérer les téléchargements de fichiers? Il s’avère que c’est assez facile!
Nous allons utiliser la liaison de données, car c’est l’une des fonctionnalités impressionnantes d’AngularJS. Nous avons le formulaire HTML suivant:
Comme vous pouvez le voir, il existe deux champs de input
texte liés chacun à une propriété d’un object unique, que j’ai appelée newImage
. L’ input
fichier est également liée à une propriété de l’object newImage
, mais cette fois, j’ai utilisé une directive personnalisée prise directement à partir d’ ici . Cette directive fait en sorte que chaque fois que le contenu de l’ input
du fichier change, un object FileList est placé dans la propriété fakepath
au lieu d’un fakepath
(qui serait le comportement standard d’Angular).
Notre code de contrôleur est le suivant:
angular.module('clientApp') .controller('MainCtrl', function ($scope, $resource) { var Image = $resource('http://localhost:3000/images/:id', {id: "@_id"}); Image.get(function(result) { if (result.status != 'OK') throw result.status; $scope.images = result.data; }) $scope.newImage = {}; $scope.submit = function() { Image.save($scope.newImage, function(result) { if (result.status != 'OK') throw result.status; $scope.images.push(result.data); }); } });
(Dans ce cas, j’exécute un serveur NodeJS sur mon ordinateur local sur le port 3000 et la réponse est un object json contenant un champ d’ status
et un champ de data
facultatif).
Pour que le téléchargement du fichier fonctionne, il suffit de configurer correctement le service $ http, par exemple dans l’appel .config sur l’object app. Plus précisément, nous devons transformer les données de chaque demande de publication en un object FormData, afin qu’il soit envoyé au serveur au format correct:
angular.module('clientApp', [ 'ngCookies', 'ngResource', 'ngSanitize', 'ngRoute' ]) .config(function ($httpProvider) { $httpProvider.defaults.transformRequest = function(data) { if (data === undefined) return data; var fd = new FormData(); angular.forEach(data, function(value, key) { if (value instanceof FileList) { if (value.length == 1) { fd.append(key, value[0]); } else { angular.forEach(value, function(file, index) { fd.append(key + '_' + index, file); }); } } else { fd.append(key, value); } }); return fd; } $httpProvider.defaults.headers.post['Content-Type'] = undefined; });
L’en Content-Type
tête Content-Type
est défini sur undefined
car sa définition manuelle sur multipart/form-data
ne définira pas la valeur de limite et le serveur ne pourra pas parsingr correctement la demande.
C’est tout. Vous pouvez maintenant utiliser $resource
pour save()
objects save()
contenant à la fois des champs de données standard et des fichiers.
AVERTISSEMENT Ceci a quelques limitations:
Si votre modèle a des documents “incorporés”, comme
{ title: "A title", atsortingbutes: { fancy: true, colored: false, nsfw: true }, image: null }
vous devez ensuite restructurer la fonction transformRequest en conséquence. Vous pourriez, par exemple, JSON.ssortingngify
les objects nesteds, à condition que vous puissiez les parsingr à l’autre extrémité
L’anglais n’est pas ma langue principale, donc si mon explication est obscure dites-le moi et je vais essayer de la reformuler 🙂
J’espère que ça aide, bravo!
Comme l’a souligné @david , une solution moins invasive consisterait à définir ce comportement uniquement pour les $resource
qui en ont réellement besoin, et à ne pas transformer chaque requête effectuée par AngularJS. Vous pouvez le faire en créant votre $resource
comme ceci:
$resource('http://localhost:3000/images/:id', {id: "@_id"}, { save: { method: 'POST', transformRequest: '', headers: '' } });
En ce qui concerne l’en-tête, vous devez en créer un qui réponde à vos exigences. La seule chose que vous devez spécifier est la propriété 'Content-Type'
en la définissant sur undefined
.
La solution la plus minimale et la moins invasive pour envoyer $resource
requêtes de $resource
avec FormData
Je l’ai trouvé:
angular.module('app', [ 'ngResource' ]) .factory('Post', function ($resource) { return $resource('api/post/:id', { id: "@id" }, { create: { method: "POST", transformRequest: angular.identity, headers: { 'Content-Type': undefined } } }); }) .controller('PostCtrl', function (Post) { var self = this; this.createPost = function (data) { var fd = new FormData(); for (var key in data) { fd.append(key, data[key]); } Post.create({}, fd).$promise.then(function (res) { self.newPost = res; }).catch(function (err) { self.newPostError = true; throw err; }); }; });
Veuillez noter que cette méthode ne fonctionnera pas avec 1.4.0+. Pour plus d’informations, consultez le changelog AngularJS (recherchez
$http: due to 5da1256
) et ce problème . C’était en fait un comportement involontaire (et donc supprimé) sur AngularJS.
Je suis venu avec cette fonctionnalité pour convertir (ou append) des données de formulaire dans un object FormData. Il pourrait probablement être utilisé comme un service.
La logique ci-dessous doit se trouver dans une configuration transformRequest
, ou dans la configuration $httpProvider
, ou peut être utilisée en tant que service. De toute façon, l’en Content-Type
tête Content-Type
doit être défini sur NULL et cela varie en fonction du contexte dans lequel vous placez cette logique. Par exemple, dans une option transformRequest
lors de la configuration d’une ressource, vous procédez comme suit:
var headers = headersGetter(); headers['Content-Type'] = undefined;
ou si vous configurez $httpProvider
, vous pouvez utiliser la méthode indiquée dans la réponse ci-dessus.
Dans l’exemple ci-dessous, la logique est placée dans une méthode transformRequest
pour une ressource.
appServices.factory('SomeResource', ['$resource', function($resource) { return $resource('some_resource/:id', null, { 'save': { method: 'POST', transformRequest: function(data, headersGetter) { // Here we set the Content-Type header to null. var headers = headersGetter(); headers['Content-Type'] = undefined; // And here begins the logic which could be used somewhere else // as noted above. if (data == undefined) { return data; } var fd = new FormData(); var createKey = function(_keys_, currentKey) { var keys = angular.copy(_keys_); keys.push(currentKey); formKey = keys.shift() if (keys.length) { formKey += "[" + keys.join("][") + "]" } return formKey; } var addToFd = function(object, keys) { angular.forEach(object, function(value, key) { var formKey = createKey(keys, key); if (value instanceof File) { fd.append(formKey, value); } else if (value instanceof FileList) { if (value.length == 1) { fd.append(formKey, value[0]); } else { angular.forEach(value, function(file, index) { fd.append(formKey + '[' + index + ']', file); }); } } else if (value && (typeof value == 'object' || typeof value == 'array')) { var _keys = angular.copy(keys); _keys.push(key) addToFd(value, _keys); } else { fd.append(formKey, value); } }); } addToFd(data, []); return fd; } } }) }]);
Donc, avec ceci, vous pouvez faire les choses suivantes sans problèmes:
var data = { foo: "Bar", foobar: { baz: true }, fooFile: someFile // instance of File or FileList } SomeResource.save(data);