AngularJSを使用してRESTful
Webサービスとやり取りし、$resource
を使用して公開されているさまざまなエンティティを抽象化します。このエンティティの一部は画像なので、$resource
"object"のsave
アクションを使用して、同じリクエスト内でバイナリデータとテキストフィールドの両方を送信できる必要があります。
単一のPOST
リクエストで、AngularJSの$resource
サービスを使用してデータを送信し、安らかなWebサービスに画像をアップロードするにはどうすればよいですか?
私は広範囲に検索しましたが、見逃したかもしれませんが、この問題の解決策を見つけることができませんでした:$ 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
と呼びます。ファイルinput
もnewImage
オブジェクトのプロパティにバインドされていますが、今回は 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 [〜#〜]これにはいくつかの制限があります:
モデルに「埋め込み」ドキュメントがある場合、たとえば
_{ title: "A title", attributes: { fancy: true, colored: false, nsfw: true }, image: null }
_
それに応じて、transformRequest関数をリファクタリングする必要があります。 たとえば、ネストされたオブジェクトを_JSON.stringify
_とすることができます。ただし、反対側で解析できる場合は
英語は私の主要言語ではないので、説明があいまいな場合は教えてください。
これがお役に立てば幸いです!
@ 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'
_プロパティのみです。
送信するための最も最小限で低侵襲のソリューション$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;
});
};
});
この方法は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);