Подтвердить что ты не робот

AngularJS: загрузка файлов с использованием $resource (solution)

Я использую AngularJS для взаимодействия с веб-сервисом RESTful, используя $resource для абстрактного отображения различных объектов. Некоторые из этих объектов являются изображениями, поэтому мне нужно иметь возможность использовать save действие объекта $resource "для отправки как двоичных данных, так и текстовых полей в пределах одного запроса.

Как я могу использовать службу AngularJS $resource для отправки данных и загрузки изображений в спокойный веб-сервис в одном запросе POST?

4b9b3361

Ответ 1

Я искал по всему миру, и, хотя я, возможно, пропустил его, я не мог найти решение этой проблемы: загрузка файлов с помощью действия $resource.

Позвольте сделать этот пример: наша служба RESTful позволяет нам получать образы, обращаясь к конечной точке /images/. Каждый Image имеет заголовок, описание и путь, указывающий на файл изображения. Используя службу RESTful, мы можем получить все из них (GET /images/), один (GET /images/1) или добавить один (POST /images). Angular позволяет нам использовать службу $resource для выполнения этой задачи легко, но не позволяет загружать файлы - что требуется для третьего действия - из коробки (и они, похоже, не планируют поддерживать его в ближайшее время). Как же тогда мы будем использовать очень удобную службу $resource, если она не сможет обрабатывать загрузки файлов? Оказывается, это довольно легко!

Мы собираемся использовать привязку данных, потому что это одна из удивительных особенностей AngularJS. У нас есть следующая форма HTML:

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

Как вы можете видеть, есть два текстовых поля input, привязанные каждому к свойству одного объекта, который я назвал newImage. Файл input также привязан к свойству объекта newImage, но на этот раз я использовал пользовательскую директиву, взятую прямо из здесь. Эта директива делает так, чтобы каждый раз, когда содержимое файла input менялось, объект FileList помещается внутри свойства binded вместо fakepath (что будет стандартным поведением Angular).

Наш код контроллера следующий:

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

(В этом случае я запускаю NodeJS-сервер на моей локальной машине на порту 3000, а ответ представляет собой объект json, содержащий поле status и необязательное поле data).

Чтобы загрузка файла работала, нам просто нужно правильно настроить службу $http, например, в вызове .config объекта приложения. В частности, нам нужно преобразовать данные каждого почтового запроса в объект FormData, чтобы он отправлялся на сервер в правильном формате:

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

Заголовок Content-Type установлен на undefined, поскольку установка его вручную в multipart/form-data не будет устанавливать граничное значение, и сервер не сможет правильно разобрать запрос.

Что это. Теперь вы можете использовать объекты $resource to save(), содержащие как стандартные поля данных, так и файлы.

ПРЕДУПРЕЖДЕНИЕ Это имеет некоторые ограничения:

  • Он не работает в старых браузерах. Извините: (
  • Если ваша модель имеет "встроенные" документы, например

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

    то вам необходимо реорганизовать функцию transformRequest соответствующим образом. Вы могли бы, например, JSON.stringify вложенные объекты, если вы можете проанализировать их на другом конце

  • Английский язык не является моим основным языком, поэтому, если мое объяснение неясное, скажите мне, и я попытаюсь перефразировать его:)

  • Это просто пример. Вы можете расширить его, в зависимости от того, что вам нужно делать.

Надеюсь, это поможет, ура!

EDIT:

Как указано @david, менее инвазивным решением было бы определить это поведение только для тех $resource, которые действительно нуждаются в этом, а не для преобразования каждого и каждый запрос, сделанный AngularJS. Вы можете сделать это, создав $resource следующим образом:

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

Что касается заголовка, вы должны создать тот, который удовлетворяет вашим требованиям. Единственное, что вам нужно указать, это свойство 'Content-Type', установив его на undefined.

Ответ 2

Самое минимальное и наименее инвазивное решение для отправки запросов $resource с помощью FormData Я нашел следующее:

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

});

Ответ 3

Обратите внимание, что этот метод не будет работать с 1.4.0+. Для большего проверка информации AngelJS changelog (поиск $http: due to 5da1256) и этот вопрос. Это на самом деле было непреднамеренным (и, следовательно, удаленным) поведением на AngularJS.

Я придумал эту функцию для преобразования (или добавления) данных формы в объект FormData. Его можно было бы использовать как услугу.

Логика ниже должна быть внутри конфигурации transformRequest или внутри $httpProvider или может использоваться как служба. В любом случае заголовок Content-Type должен иметь значение NULL, и это зависит от контекста, в который вы вставляете эту логику. Например, внутри параметра transformRequest при настройке ресурса вы делаете:

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

или при настройке $httpProvider вы можете использовать метод, указанный в ответе выше.

В приведенном ниже примере логика помещается внутри метода transformRequest для ресурса.

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

Таким образом, вы можете сделать следующее без проблем:

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

SomeResource.save(data);