【问题标题】:AngularJS: Upload files using $resource (solution)AngularJS:使用 $resource 上传文件(解决方案)
【发布时间】:2014-02-02 15:27:20
【问题描述】:

我正在使用 AngularJS 与 RESTful 网络服务进行交互,使用 $resource 来抽象暴露的各种实体。其中一些实体是图像,因此我需要能够使用$resource“对象”的save 操作在同一请求中发送二进制数据和文本字段。

如何使用 AngularJS 的 $resource 服务在单个 POST 请求中发送数据并将图像上传到一个 RESTful Web 服务?

【问题讨论】:

    标签: angularjs upload angular-resource


    【解决方案1】:

    我进行了广泛的搜索,虽然我可能错过了它,但我找不到解决这个问题的方法:使用 $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 服务器,响应是一个 json 对象,其中包含一个 status 字段和一个可选的 data 字段)。

    为了使文件上传工作,我们只需要正确配置 $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 不会设置边界值,服务器将无法正确解析请求。

    就是这样。现在您可以使用包含标准数据字段和文件的 $resourcesave() 对象。

    警告这有一些限制:

    1. 它不适用于旧版浏览器。对不起:(
    2. 如果您的模型具有“嵌入式”文档,例如

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

      那么您需要相应地重构 transformRequest 函数。 例如,您可以JSON.stringify 嵌套对象,前提是您可以在另一端解析它们

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

    4. 这只是一个例子。您可以根据您的应用需要做什么来扩展它。

    我希望这会有所帮助,干杯!

    编辑:

    正如@david 所指出的,侵入性较小的解决方案是仅为那些真正需要它的$resources 定义此行为,而不是转换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

    【讨论】:

    • 你也可以通过将函数放入设置对象中将其集成到一个 $resource 中:$resource('localhost:3000/images/:id', {id: "@_id"}, { save: { method : 'POST', transformRequest: ..., header: ... } });
    • 这实际上是一个非常好的主意,我会更新答案!
    • 我继续并修复了您在帖子中提到的嵌套对象问题,如果您想将其包含在您的帖子中 - transformRequest: function(data, headersGetter) { 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 {if (value !== null && typeof value === 'object'){fd.append(key, JSON.stringify(value)); } else {fd.append(key, value);}}});return fd;}
    • 指令的files-model="newImage.image" 中的文件模型是什么?
    【解决方案2】:

    FormData 发送$resource 请求的最小和侵入性最小的解决方案我发现是这样的:

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

    【讨论】:

    • 这是有道理的。考虑到我一年多前写的原始帖子,我不能说这在当时是否可行,但可能是。我唯一的问题是如何将文件放入数据中?
    • 好吧,基本上你在文件输入元素上收听change 事件。您可以将其包装到指令中,something like this(适用于图像)
    • 这是我见过的最清晰、最干净的解决方案,包括带有角度形式的简单文件上传。仍然需要担心 FormData 支持 (caniuse.com/#search=formData) 但幸运的是这对我的项目来说不是问题。如果您使用 Rails 作为后端(我使用的是 Rails 4.2),请注意:由于 Content-Type 现在将是多部分表单数据,因此您必须修改 wrap_parameters 因为它默认情况下仅适用于 Content-Type json (见stackoverflow.com/questions/16380893/…
    • 非常感谢!这应该是标记的答案。
    • 您的数据从何而来?它来自表单名称属性吗?像这样`
      `
    【解决方案3】:

    请注意,此方法不适用于 1.4.0+。更多 信息检查AngularJS changelog(搜索$http: due to 5da1256)和this issue。这实际上是 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);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-09-27
      • 2013-10-25
      • 2012-11-08
      • 2016-08-13
      • 1970-01-01
      • 1970-01-01
      • 2011-08-20
      相关资源
      最近更新 更多