it-swarm.com.de

AngularJS: Hochladen von Dateien mit $ resource (Lösung)

Ich verwende AngularJS, um mit einem RESTful Webservice zu interagieren, und benutze $resource, Um die verschiedenen Entitäten zu abstrahieren. Einige dieser Entitäten sind Bilder, daher muss ich die Aktion save von $resource "Object" verwenden können, um sowohl Binärdaten- als auch Textfelder innerhalb derselben Anforderung zu senden.

Wie kann ich den AngularJS-Dienst $resource Verwenden, um Daten zu senden und Bilder in einer einzigen POST Anfrage auf einen erholsamen Webservice hochzuladen?

53
juandemarco

Ich habe weit und breit gesucht und, obwohl ich es vielleicht verpasst habe, konnte ich keine Lösung für dieses Problem finden: Hochladen von Dateien mit einer $ resource-Aktion.

Nehmen wir folgendes Beispiel: Mit unserem RESTful-Service können wir auf Bilder zugreifen, indem wir Anforderungen an den Endpunkt /images/ Senden. Jedes Image hat einen Titel, eine Beschreibung und den Pfad zur Bilddatei. Mit dem RESTful-Service können wir alle (GET /images/), Eine einzelne (GET /images/1) Oder eine weitere (POST /images) Erhalten. Angular ermöglicht es uns, den $ resource-Dienst zu verwenden, um diese Aufgabe einfach zu erledigen, aber das Hochladen von Dateien, das für die dritte Aktion erforderlich ist, ist nicht sofort möglich (und Anscheinend planen sie nicht, es bald zu unterstützen. ) Wie würden wir dann den sehr praktischen $ resource-Service nutzen, wenn er keine Datei-Uploads verarbeiten kann? Es stellt sich heraus, dass es ziemlich einfach ist!

Wir werden Datenbindung verwenden, da dies eine der fantastischen Funktionen von AngularJS ist. Wir haben das folgende HTML-Formular:

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

Wie Sie sehen, gibt es zwei Textfelder input, die jeweils an eine Eigenschaft eines einzelnen Objekts gebunden sind, das ich newImage genannt habe. Die Datei input ist ebenfalls an eine Eigenschaft des Objekts newImage gebunden, aber dieses Mal habe ich eine benutzerdefinierte Direktive verwendet, die direkt aus hier stammt. Mit dieser Anweisung wird festgelegt, dass jedes Mal, wenn sich der Inhalt der Datei input ändert, ein FileList-Objekt anstelle eines fakepath in die gebundene Eigenschaft eingefügt wird (dies wäre Angulars Standardverhalten).

Unser Controller-Code lautet wie folgt:

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

(In diesem Fall führe ich einen NodeJS-Server auf meinem lokalen Computer an Port 3000 aus und die Antwort ist ein json-Objekt, das ein status -Feld und ein optionales data -Feld enthält.).

Damit der Datei-Upload funktioniert, müssen wir nur den $ http-Dienst ordnungsgemäß konfigurieren, z. B. innerhalb des Aufrufs .config für das App-Objekt. Insbesondere müssen wir die Daten jeder Post-Anfrage in ein FormData-Objekt umwandeln, damit sie im richtigen Format an den Server gesendet werden:

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

Der Header Content-Type Wird auf undefined gesetzt, da durch manuelles Setzen auf multipart/form-data Der Grenzwert nicht festgelegt wird und der Server die Anforderung nicht korrekt analysieren kann.

Das ist es. Jetzt können Sie $resource Für save() Objekte verwenden, die sowohl Standarddatenfelder als auch Dateien enthalten.

[~ # ~] Warnung [~ # ~] Dies hat einige Einschränkungen:

  1. Es funktioniert nicht mit älteren Browsern. Es tut uns leid :(
  2. Wenn Ihr Modell "eingebettete" Dokumente enthält, z

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

    dann müssen Sie die transformRequest-Funktion entsprechend umgestalten. Sie könnten zum Beispiel JSON.stringify Die verschachtelten Objekte, vorausgesetzt, Sie können sie am anderen Ende analysieren

  3. Englisch ist nicht meine Hauptsprache, also wenn meine Erklärung undeutlich ist, sag es mir und ich werde versuchen, es anders zu formulieren :)

  4. Dies ist nur ein Beispiel. Sie können dies erweitern, je nachdem, was Ihre Anwendung tun muss.

Ich hoffe das hilft, Prost!

BEARBEITEN:

Wie von @ david hervorgehoben, besteht eine weniger invasive Lösung darin, dieses Verhalten nur für diejenigen $resource Zu definieren, die es tatsächlich benötigen, und nicht jede einzelne Anforderung von AngularJS zu transformieren . Sie können dies tun, indem Sie Ihren $resource Wie folgt erstellen:

$resource('http://localhost:3000/images/:id', {id: "@_id"}, { 
    save: { 
        method: 'POST', 
        transformRequest: '<THE TRANSFORMATION METHOD DEFINED ABOVE>', 
        headers: '<SEE BELOW>' 
    } 
});

Für den Header sollten Sie einen erstellen, der Ihren Anforderungen entspricht. Sie müssen nur die Eigenschaft 'Content-Type' Angeben, indem Sie sie auf undefined setzen.

48
juandemarco

Die minimalste und am wenigsten invasive Lösung zum Senden von $resource Anfragen mit FormData Ich habe folgendes herausgefunden:

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

Bitte beachten Sie, dass diese Methode mit 1.4.0+ nicht funktioniert. Weitere Informationen finden Sie unter AngularJS Changelog (Suche nach $http: due to 5da1256) und diese Ausgabe . Dies war eigentlich ein unbeabsichtigtes (und daher entferntes) Verhalten bei AngularJS.

Ich habe mir diese Funktionalität ausgedacht, um Formulardaten in ein FormData-Objekt zu konvertieren (oder anzufügen). Es könnte wahrscheinlich als Service genutzt werden.

Die Logik unten sollte entweder in einem transformRequest oder in $httpProvider Konfiguration oder kann als Dienst verwendet werden. In irgendeiner Weise Content-Type -Header muss auf NULL gesetzt werden. Dies hängt vom Kontext ab, in den Sie diese Logik einfügen. Beispiel: In einer transformRequest -Option beim Konfigurieren einer Ressource gehen Sie wie folgt vor:

var headers = headersGetter();
headers['Content-Type'] = undefined;

oder bei der Konfiguration von $httpProvider, können Sie die in der obigen Antwort angegebene Methode verwenden.

Im folgenden Beispiel befindet sich die Logik in einer transformRequest - Methode für eine 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;
      }
    }
  })
}]);

Damit können Sie also ohne Probleme Folgendes tun:

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

SomeResource.save(data);
10
Parziphal