AngularJS:使用$ resource上传文件(解决方案)

jua*_*rco 54 upload angularjs angular-resource

我正在使用AngularJS与RESTfulWeb服务进行交互,$resource用于抽象暴露的各种实体.这些实体中的一些是图像,因此我需要能够使用"对象" 的save动作$resource在同一请求中发送二进制数据和文本字段.

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

jua*_*rco 48

我进行了广泛的搜索,虽然我可能错过了它,但我找不到解决此问题的方法:使用$ resource操作上传文件.

让我们举个例子:我们的RESTful服务允许我们通过向/images/端点发出请求来访问图像.每个Image都有标题,描述和指向图像文件的路径.使用RESTful服务,我们可以获取所有这些(GET /images/),单个(GET /images/1)或添加一个(POST /images).Angular允许我们使用$ resource服务轻松完成此任务,但不允许文件上传 - 这是第三个操作所需的 - 开箱即用(他们似乎没有计划随时支持它很快).那么,如果它无法处理文件上传,我们将如何使用非常方便的$ 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>
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,有两个文本input字段被绑定到一个对象的属性,我已经调用了它们newImage.该文件input也绑定到newImage对象的属性,但这次我使用了直接从这里获取的自定义指令.该指令使得每次文件内容input发生更改时,将FileList对象放入binded属性而不是a 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);
        });
    }
}); 
Run Code Online (Sandbox Code Playgroud)

(在这种情况下,我在端口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;
});
Run Code Online (Sandbox Code Playgroud)

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所指出的,一种侵入性较小的解决方案是仅为那些$resource实际需要它的人定义此行为,而不是转换AngularJS发出的每个请求.你可以通过创建$resource这样的方式来做到这一点:

$resource('http://localhost:3000/images/:id', {id: "@_id"}, { 
    save: { 
        method: 'POST', 
        transformRequest: '<THE TRANSFORMATION METHOD DEFINED ABOVE>', 
        headers: '<SEE BELOW>' 
    } 
});
Run Code Online (Sandbox Code Playgroud)

至于标题,您应该创建一个满足您要求的标题.您需要指定的唯一事情是'Content-Type'通过将其设置为属性undefined.

  • 您还可以通过将函数放入设置对象中将其集成到一个$资源中:$ resource('http:// localhost:3000/images /:id',{id:"@_ id"},{save:{方法:'POST',transformRequest:...,header:...}}); (3认同)

tim*_*der 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;
        });
    };

});
Run Code Online (Sandbox Code Playgroud)


Par*_*hal 10

请注意,此方法不适用于1.4.0+.有关更多信息,请查看AngularJS更改日志(搜索$http: due to 5da1256)和此问题.这实际上是AngularJS上的无意(并因此被删除)行为.

我想出了这个功能,可以将表单数据转换(或附加)到FormData对象中.它可能可以用作服务.

下面的逻辑应该在a transformRequest或内部$httpProvider配置中,或者可以用作服务.无论如何,Content-Type标头必须设置为NULL,并且这样做会根据您放置此逻辑的上下文而有所不同.例如,在transformRequest配置资源时在选项内部,您可以:

var headers = headersGetter();
headers['Content-Type'] = undefined;
Run Code Online (Sandbox Code Playgroud)

或者如果配置$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;
      }
    }
  })
}]);
Run Code Online (Sandbox Code Playgroud)

因此,您可以毫无问题地执行以下操作:

var data = { 
  foo: "Bar",
  foobar: {
    baz: true
  },
  fooFile: someFile // instance of File or FileList
}

SomeResource.save(data);
Run Code Online (Sandbox Code Playgroud)