web-dev-qa-db-fra.com

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

J'utilise AngularJS pour interagir avec un _service web 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 save de $resource "Objet" pour envoyer des champs de données binaires et des champs de texte au sein d'une même demande.

Comment puis-je utiliser le service $resource D'AngularJS pour envoyer des données et télécharger des images sur un service Web reposant en une seule demande POST?

53
juandemarco

J'ai cherché loin et, bien que j'aie pu le manquer, je n'ai pas trouvé de solution à ce problème: télécharger des fichiers à l'aide d'une action $ resource.

Faisons cet exemple: notre service RESTful nous permet d’accéder aux images en adressant des demandes au point de terminaison /images/. Chaque Image a un titre, une description et le chemin qui pointe vers le fichier image. En utilisant le service RESTful, nous pouvons tous les obtenir (GET /images/), Un seul (GET /images/1) Ou en ajouter un (POST /images). Angular nous permet d’utiliser le service $ resource pour accomplir cette tâche facilement, mais ne permet pas le téléchargement de fichiers - ce qui est requis pour la troisième action - prête à l'emploi (et ils ne semblent pas avoir l’intention de le supporter de si tôt ). Comment pourrions-nous alors utiliser le service très utile $ resource 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 les plus impressionnantes d'AngularJS. Nous avons le formulaire HTML suivant:

<form class="form" name="form" novalidate ng-submit="submit()">
    <div class="form-group">
        <input class="form-control" ng-model="newImage.title" placeholder="Title" required>
    </div>
    <div class="form-group">
        <input class="form-control" ng-model="newImage.description" placeholder="Description">
    </div>
    <div class="form-group">
        <input type="file" files-model="newImage.image" required >
    </div>

    <div class="form-group clearfix">
        <button class="btn btn-success pull-right" type="submit" ng-disabled="form.$invalid">Save</button>
    </div>
</form>

Comme vous pouvez le constater, deux champs de texte input sont liés à une propriété d’un objet unique, que j’ai appelée newImage. Le fichier input est également lié à une propriété de l'objet newImage, mais cette fois-ci, j'ai utilisé une directive personnalisée tirée directement de ici . Cette directive fait en sorte que chaque fois que le contenu du fichier input change, un objet FileList soit placé dans la propriété liée au lieu d'un fakepath (ce 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 objet json contenant un champ status et un champ optionnel data).

Pour que le téléchargement de fichier fonctionne, il suffit de configurer correctement le service $ http, par exemple dans l'appel .config sur l'objet app. Plus précisément, nous devons transformer les données de chaque demande de publication en un objet FormData, afin qu’elles soient envoyées au serveur dans le 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-tête Content-Type Est défini sur undefined car le définir manuellement sur multipart/form-data Ne définirait pas la valeur limite et le serveur ne pourrait pas analyser correctement la demande.

C'est ça. Vous pouvez maintenant utiliser $resource Pour save() objets contenant à la fois des champs de données standard et des fichiers.

[~ # ~] warning [~ # ~] Ceci a quelques limitations:

  1. Cela ne fonctionne pas sur les anciens navigateurs. Désolé :(
  2. Si votre modèle contient des documents "intégrés", tels que

    { title: "A title", attributes: { fancy: true, colored: false, nsfw: true }, image: null }

    alors vous devez refactoriser la fonction transformRequest en conséquence. Vous pouvez, par exemple, JSON.stringify Les objets imbriqués, à condition que vous puissiez les analyser à l'autre bout

  3. L'anglais n'est pas ma langue principale, alors si mon explication est obscure, dites-le-moi et j'essaierai 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 cela aide, à la vôtre!

MODIFIER:

Comme indiqué par @ david , une solution moins invasive consisterait à définir ce comportement uniquement pour les $resource Qui en ont réellement besoin, et non à transformer chacune des demandes formulées 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: '<THE TRANSFORMATION METHOD DEFINED ABOVE>', 
        headers: '<SEE BELOW>' 
    } 
});

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.

48
juandemarco

La solution la plus minimale et la moins invasive pour envoyer $resource demandes avec FormData j'ai trouvé ceci:

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

});
24
timetowonder

Veuillez noter que cette méthode ne fonctionnera pas sur 1.4.0+. Pour plus d'informations, consultez AngularJS changelog (recherchez $http: due to 5da1256) et ce numéro . C’était en fait un comportement imprévu (et donc supprimé) sur AngularJS.

Je suis venu avec cette fonctionnalité pour convertir (ou ajouter) des données de formulaire en un objet FormData. Il pourrait probablement être utilisé comme service.

La logique ci-dessous devrait être soit à l'intérieur d'un transformRequest, soit à l'intérieur de $httpProvider configuration, ou pourrait être utilisé en tant que service. De quelque manière que, Content-Type en-tête 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 effectuez les opérations suivantes:

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 cela, vous pouvez faire ce qui suit sans problèmes:

var data = { 
  foo: "Bar",
  foobar: {
    baz: true
  },
  fooFile: someFile // instance of File or FileList
}

SomeResource.save(data);
10
Parziphal