AngularJS: Upload de arquivos usando $ resource (solution)

Estou usando o AngularJS para interagir com um webservice RESTful , usando $resource para abstrair as diversas entidades expostas. Algumas dessas entidades são imagens, portanto, preciso poder usar a ação de save $resource “object” para enviar dados binários e campos de texto dentro da mesma solicitação.

Como posso usar o serviço de $resource do AngularJS para enviar dados e fazer upload de imagens para um webservice tranquilo em uma única solicitação POST ?

Eu pesquisei por toda a parte e, embora eu tenha perdido, não consegui encontrar uma solução para esse problema: carregar arquivos usando uma ação de recurso $.

Vamos fazer este exemplo: nosso serviço RESTful nos permite acessar imagens fazendo solicitações ao /images/ endpoint. Cada Image tem um título, uma descrição e o caminho apontando para o arquivo de imagem. Usando o serviço RESTful, podemos obter todos eles ( GET /images/ ), um único ( GET /images/1 ) ou adicionar um ( POST /images ). O Angular nos permite usar o serviço $ resource para realizar essa tarefa facilmente, mas não permite o upload de arquivos – que é necessário para a terceira ação – fora da checkbox (e eles não parecem estar planejando dar suporte a ela a qualquer momento em breve ). Como, então, poderíamos usar o serviço $ resource muito útil se ele não pudesse lidar com uploads de arquivos? Acontece que é bem fácil!

Vamos usar a vinculação de dados, porque é um dos resources impressionantes do AngularJS. Nós temos o seguinte formulário HTML:

 

Como você pode ver, existem dois campos de input texto que são vinculados a cada uma de uma propriedade de um único object, que eu chamei de newImage . A input arquivo também é vinculada a uma propriedade do object newImage , mas desta vez utilizei uma diretiva personalizada tirada diretamente daqui . Esta diretiva faz com que toda vez que o conteúdo da input do arquivo seja alterado, um object FileList seja colocado dentro da propriedade binded em vez de um fakepath (que seria o comportamento padrão do Angular).

Nosso código de controlador é o seguinte:

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

(Nesse caso, estou executando um servidor NodeJS em minha máquina local na porta 3000 e a resposta é um object json contendo um campo de status e um campo de data opcional).

Para que o upload do arquivo funcione, só precisamos configurar corretamente o serviço $ http, por exemplo, dentro da chamada .config no object do aplicativo. Especificamente, precisamos transformar os dados de cada solicitação de postagem em um object FormData, para que seja enviada ao servidor no formato correto:

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

O header Content-Type está definido como undefined porque defini-lo manualmente para multipart/form-data não definiria o valor do limite e o servidor não seria capaz de analisar a solicitação corretamente.

É isso aí. Agora você pode usar $resource para save() objects save() que contenham arquivos e campos de dados padrão.

ATENÇÃO Isto tem algumas limitações:

  1. Não funciona em navegadores mais antigos. Desculpa 🙁
  2. Se o seu modelo tiver documentos “incorporados”, como

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

    então você precisa refatorar a function transformRequest de acordo. Você poderia, por exemplo, JSON.stringify os objects nesteds, desde que você possa analisá-los na outra extremidade

  3. Inglês não é a minha língua principal, por isso, se a minha explicação for obscura, diga-me e vou tentar reformulá-la 🙂

  4. Este é apenas um exemplo. Você pode expandir isso dependendo do que seu aplicativo precisa fazer.

Espero que isso ajude, vivas!

EDITAR:

Como apontado por @david , uma solução menos invasiva seria definir esse comportamento apenas para os $resource que realmente precisam dele, e não para transformar toda e qualquer solicitação feita pelo AngularJS. Você pode fazer isso criando seu $resource assim:

 $resource('http://localhost:3000/images/:id', {id: "@_id"}, { save: { method: 'POST', transformRequest: '', headers: '' } }); 

Quanto ao header, você deve criar um que satisfaça seus requisitos. A única coisa que você precisa especificar é a propriedade 'Content-Type' , definindo-a como undefined .

A solução mais mínima e menos invasiva para enviar solicitações de $resource com FormData eu achei ser isso:

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

Por favor, note que este método não funcionará em 1.4.0+. Para obter mais informações, consulte o changelog do AngularJS (procure por $http: due to 5da1256 ) e esse problema . Este foi realmente um comportamento não intencional (e, portanto, removido) no AngularJS.

Eu criei essa funcionalidade para converter (ou append) dados de formulário em um object FormData. Provavelmente poderia ser usado como um serviço.

A lógica abaixo deve estar dentro de uma configuração transformRequest , ou dentro da configuração $httpProvider , ou pode ser usada como um serviço. De qualquer forma, o header Content-Type deve ser definido como NULL, e isso varia dependendo do contexto em que você insere essa lógica. Por exemplo, dentro de uma opção transformRequest ao configurar um recurso, você faz:

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

ou se configurando $httpProvider , você poderia usar o método anotado na resposta acima.

No exemplo abaixo, a lógica é colocada dentro de um método transformRequest para um recurso.

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

Então, com isso, você pode fazer o seguinte sem problemas:

 var data = { foo: "Bar", foobar: { baz: true }, fooFile: someFile // instance of File or FileList } SomeResource.save(data); 
Intereting Posts