AngularJS: Télécharger des fichiers en utilisant $ resource (solution)

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:

  1. Cela ne fonctionne pas sur les anciens navigateurs. Pardon 🙁
  2. 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é

  3. L’anglais n’est pas ma langue principale, donc si mon explication est obscure dites-le moi et je vais essayer de la reformuler 🙂

  4. C’est juste un exemple. Vous pouvez développer cela en fonction de ce que votre application doit faire.

J’espère que ça aide, bravo!

MODIFIER:

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