我正在使用AngularJS与RESTful
网络服务进行交互,使用$resource
来抽象暴露的各种实体。其中一些实体是图像,因此我需要能够使用save
“object”的$resource
操作来在同一请求中发送二进制数据和文本字段。
如何使用AngularJS的$resource
服务在单个POST
请求中发送数据并将图片上传到安静的网络服务?
答案 0 :(得分:48)
我进行了广泛的搜索,虽然我可能已经错过了,但我找不到解决此问题的方法:使用$ resource操作上传文件。
让我们举个例子:我们的RESTful服务允许我们通过向/images/
端点发出请求来访问图像。每个Image
都有标题,描述和指向图像文件的路径。使用RESTful服务,我们可以获得所有这些服务(GET /images/
),一个(GET /images/1
)或添加一个(POST /images
)。 Angular允许我们使用$ resource服务轻松完成此任务,但不允许文件上传 - 这是第三个操作所需的 - 开箱即用(和they don't seem to be planning on supporting it anytime soon)。那么,如果它无法处理文件上传,我们将如何使用非常方便的$ 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
对象的属性,但这次我使用了直接从here获取的自定义指令。该指令使得每次文件input
的内容发生更改时,将FileList对象放入绑定属性而不是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);
});
}
});
(在这种情况下,我在端口3000上的本地计算机上运行NodeJS服务器,响应是一个包含status
字段和可选data
字段的json对象。
为了使文件上传起作用,我们只需要正确配置$ http服务,例如在app对象的.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
不会设置边界值,服务器将无法正确解析请求。
就是这样。现在,您可以使用包含标准数据字段和文件的$resource
到save()
个对象。
警告这有一些限制:
如果您的模型有“嵌入”文档,例如
{
title: "A title",
attributes: {
fancy: true,
colored: false,
nsfw: true
},
image: null
}
然后你需要相应地重构transformRequest函数。 例如,您可以JSON.stringify
嵌套对象,前提是您可以在另一端解析它们
英语不是我的主要语言,所以如果我的解释模糊不清,请告诉我,我会尝试改写它:)
我希望这会有所帮助,欢呼!
正如@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
。
答案 1 :(得分:24)
使用$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;
});
};
});
答案 2 :(得分:10)
请注意,此方法不适用于1.4.0+。更多 信息检查AngularJS changelog(搜索
$http: due to 5da1256
)和this issue。这实际上是AngularJS上的一个无意的(因此被删除)行为。
我想出了这个功能,可以将表单数据转换(或附加)到FormData对象中。它可能可以用作服务。
以下逻辑应位于transformRequest
或$httpProvider
内部配置中,或者可用作服务。无论如何,Content-Type
标头必须设置为NULL,并且这样做会有所不同,具体取决于您放置此逻辑的上下文。例如,在配置资源时在transformRequest
选项中,您可以:< / p>
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);