web-dev-qa-db-ja.com

AngularJS:$ resourceを使用してファイルをアップロードする(ソリューション)

AngularJSを使用してRESTful Webサービスとやり取りし、$resourceを使用して公開されているさまざまなエンティティを抽象化します。このエンティティの一部は画像なので、$resource "object"のsaveアクションを使用して、同じリクエスト内でバイナリデータとテキストフィールドの両方を送信できる必要があります。

単一のPOSTリクエストで、AngularJSの$resourceサービスを使用してデータを送信し、安らかなWebサービスに画像をアップロードするにはどうすればよいですか?

53
juandemarco

私は広範囲に検索しましたが、見逃したかもしれませんが、この問題の解決策を見つけることができませんでした:$ resourceアクションを使用してファイルをアップロードします。

この例を作ってみましょう。RESTfulサービスを使用すると、_/images/_エンドポイントにリクエストを送信して画像にアクセスできます。各Imageには、タイトル、説明、および画像ファイルを指すパスがあります。 RESTfulサービスを使用すると、それらすべて(_GET /images/_)、単一のもの(_GET /images/1_)、または追加(_POST /images_)できます。 Angularを使用すると、$ resourceサービスを使用してこのタスクを簡単に実行できますが、3番目のアクションに必要なファイルのアップロードは許可されていません-および 彼らはすぐにそれをサポートすることを計画していないようです )。それでは、ファイルのアップロードを処理できない場合、非常に便利な$ resourceサービスをどのように使用しますか?それは非常に簡単です!

これは、AngularJSの素晴らしい機能の1つであるため、データバインディングを使用します。次の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>
_

ご覧のとおり、2つのテキストinputフィールドがあり、それぞれが単一のオブジェクトのプロパティにバインドされています。これをnewImageと呼びます。ファイルinputnewImageオブジェクトのプロパティにバインドされていますが、今回は here から直接取得したカスタムディレクティブを使用しました。このディレクティブは、ファイルのコンテンツinputが変更されるたびに、fakepath(Angularの標準動作)ではなく、バインドされたプロパティ内にFileListオブジェクトが配置されるようにします。

コントローラーコードは次のとおりです。

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

(この場合、ローカルマシンでポート3000でNodeJSサーバーを実行しています。応答は、statusフィールドとオプションのdataフィールドを含むjsonオブジェクトです)。

ファイルのアップロードが機能するためには、たとえばappオブジェクトの.config呼び出し内で、$ httpサービスを適切に構成する必要があります。具体的には、各投稿リクエストのデータを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_をsave()オブジェクトに使用できます。

[〜#〜] warning [〜#〜]これにはいくつかの制限があります:

  1. 古いブラウザでは機能しません。ごめんなさい :(
  2. モデルに「埋め込み」ドキュメントがある場合、たとえば

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

    それに応じて、transformRequest関数をリファクタリングする必要があります。 たとえば、ネストされたオブジェクトを_JSON.stringify_とすることができます。ただし、反対側で解析できる場合は

  3. 英語は私の主要言語ではないので、説明があいまいな場合は教えてください。

  4. これは単なる例です。アプリケーションで何をする必要があるかに応じて、これを拡張できます。

これがお役に立てば幸いです!

編集:

@ david によって指摘されているように、より侵襲性の低いソリューションは、実際にそれを必要とする_$resource_ sに対してのみこの動作を定義し、AngularJSによって行われたすべてのリクエストを変換しないことです。これを行うには、次のように_$resource_を作成します。

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

ヘッダーについては、要件を満たすものを作成する必要があります。指定する必要があるのは、undefinedに設定することによる_'Content-Type'_プロパティのみです。

48
juandemarco

送信するための最も最小限で低侵襲のソリューション$resourceFormDataを含むリクエスト

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

この方法は1.4.0以降では機能しないことに注意してください。詳細については、 AngularJS 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);
10
Parziphal