陈建华的博客
专注web开发
Flutter HTTP上传文件使用详解
2020-07-01 11:38:56   阅读91次

Flutter HTTP上传文件详解

最近使用Flutter开发新App,需要使用Http上传文件,对Flutter中Http上传进行一些总结


multipart/form-data

一个 HTML 表单中的 enctype 有三种类型

  • application/x-www-urlencoded

  • multipart/form-data

  • text-plain

默认情况下是 application/x-www-urlencoded,当表单使用 POST 请求时,数据会被以 x-www-urlencoded方式编码到 Body 中来传送。


如果要发送大量的二进制数据(non-ASCII),application/x-www-form-urlencoded显然是低效的,因为它需要用 3 个字符来表示一个 non-ASCII 的字符。因此,这种情况下,应该使用multipart/form-data格式。


我们需求中正是需要使用multipart/form-data格式来上传文件。


multipart/form-data请求的内容格式如下:


POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryyb1zYhTI38xpQxBK
------WebKitFormBoundaryyb1zYhTI38xpQxBK
Content-Disposition: form-data; name="city_id"
------WebKitFormBoundaryyb1zYhTI38xpQxBK
Content-Disposition: form-data; name="company_id"
------WebKitFormBoundaryyb1zYhTI38xpQxBK
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png
PNG ... content of chrome.png ...
------WebKitFormBoundaryyb1zYhTI38xpQxBK--

每个请求的body中可以包含多个字段,如上面的请求中就包含“city_id”、“company_id”、“file”三个字段,前两个字段的值是字符串,“chrome.png”则是上传的文件,文件以二进制数组转换成字符串来传递,如果文件较大,会分为多个POST请求传递给服务器,前面的POST请求会设置为keep-alive,最后一个POST请求才close。


“------WebKitFormBoundaryyb1zYhTI38xpQxBK”是分隔符(boundary),用于分割body中的每个字段,boundary可以自定义,在header中的boundary字段说明,在body中会以如下格式添加进去:


...
Content-Type: multipart/form-data; boundary=${boundary} 
--${boundary}
...
...
--${boundary}--


Flutter Http插件

Flutter中Http请求方式可以直接使用Dart:io中的HttpClient,但是目前不支持multipart/form-data格式,为了方便,我们使用了dart官方封装的http插件


官方文档中介绍了一些简单的使用方法,这里就不再赘述,比较蛋疼的是官方文档中只介绍了一些基础功能的用法,像multipart/form-data的用法都没有介绍。这里主要是介绍一下multipart/form-data的用法。


由于我们的需求是一定要使用multipart/form-data,一开始纠结了好久找不到在Flutter中该如何写代码;网上有人介绍可以使用dio这个库来使用multipart/form-data格式上传文件,但是我们的项目里已经引入了http插件,不想再换成dio,所以还是想最好能够用http插件实现multipart上传功能。


MultipartRequest使用

经过一段时间搜索,终于在http的Github的Issue里找到了一些线索,有人提问关于multipart的问题,我这才发现在http的包里有一个MultipartRequest的类,这个类正是对multipart/form-data格式的实现。


这个类的使用在源码的注释里有简单的说明,具体用法如下:


var uri = Uri.parse("http://pub.dartlang.org/packages/create");
var request = new http.MultipartRequest("POST", uri);
request.fields['user'] = 'nweiz@google.com';
request.files.add(new http.MultipartFile.fromPath(
     'package',
     'build/package.tar.gz',
     contentType: new MediaType('application', 'x-tar'));
var response = await request.send();
if (response.statusCode == 200) print('Uploaded!');


在MultipartRequest中,fields是一个Map;files是一个MultipartFile的List:


/// The form fields to send for this request.
final Map<String, String> fields;
/// The private version of [files].
final List<MultipartFile> _files;
/// The list of files to upload for this request.
List<MultipartFile> get files => _files;


fields里存储的key-value就是body中的文本字段,key是name,value是内容。files里存储的就是需要上传的文件,MultipartFile有两个命名构造方法和一个静态方法:


/// Creates a new [MultipartFile] from a byte array.
///
/// [contentType] currently defaults to `application/octet-stream`, but in the
/// future may be inferred from [filename].
factory MultipartFile.fromBytes(String field, List<int> value, {String filename, MediaType contentType})
  /// Creates a new [MultipartFile] from a string.
  ///
  /// The encoding to use when translating [value] into bytes is taken from
  /// [contentType] if it has a charset set. Otherwise, it defaults to UTF-8.
  /// [contentType] currently defaults to `text/plain; charset=utf-8`, but in
  /// the future may be inferred from [filename].
factory MultipartFile.fromString(String field, String value, {String filename, MediaType contentType})
// TODO(nweiz): Infer the content-type from the filename.
/// Creates a new [MultipartFile] from a path to a file on disk.
///
/// [filename] defaults to the basename of [filePath]. [contentType] currently
/// defaults to `application/octet-stream`, but in the future may be inferred
/// from [filename].
///
/// Throws an [UnsupportedError] if `dart:io` isn't supported in this
/// environment.
static Future<MultipartFile> fromPath(String field, String filePath, {String filename, MediaType contentType})

最后调用MultipartRequest中的send方法会将fields和files中的内容按照格式生成body,然后发送POST请求。


需要注意的是request.send()返回的是StreamedResponse,和普通的Response还不一样,需要用如下方法才能读取内容:


var respStr = await response.stream.transform(utf8.decoder).join();
LogUtil.i("upload response is $respStr");


Flutter请求抓包问题

一般做HTTP请求都会想要抓包来看一下请求的格式和内容对不对,但是这次连上代理以后发现其他请求都能抓到,只有Flutter里的请求抓不到…

上网搜索了一下,发现已经有人遇到过这个问题,并给出了解决方案,具体的分析这里就不再贴了,详情请看Flutter中http请求抓包解决方案。这里只写一下结论,增加如下代码就可以抓包了,"http_proxy"填代理PC的IP和端口即可。


var httpClient = new HttpClient();
httpClient.findProxy = (url) {
    return HttpClient.findProxyFromEnvironment(url, environment: {"http_proxy": 'http://192.168.124.7:8888',});
};


但是有一个问题,我们用的是http插件,不是原生的HttpClient,这又应该怎么设置呢?

遇事不决读源码,在http中的client.dart里,我看到这样的注释:


/// Creates a new client.
///
/// Currently this will create an `IOClient` if `dart:io` is available and
/// a `BrowserClient` if `dart:html` is available, otherwise it will throw
/// an unsupported error.


意思是如果有dart:io,就会创建一个IOClient;如果有dart:html,就会创建一个BrowserClient。HttpClient正是dart:io中的一员,所以我们来看看IOClient的实现:


/// The underlying `dart:io` HTTP client.
  HttpClient _inner;
  /// Creates a new HTTP client.
  IOClient([HttpClient inner]) : _inner = inner ?? new HttpClient();


这就很清晰了,IOClient实际就是HttpClient的封装,那我们只要自己创建一个HttpClient设置好代理后再创建IOClient就可以了,所以我们完整的上传代码就是:


  static Future<bool> upload(BaseUploadData data) async {
    var request = await data.getRequest();
    HttpClient httpClient = new HttpClient();
    httpClient.findProxy = (url) {
      return HttpClient.findProxyFromEnvironment(url, environment: {"http_proxy": 'http://10.45.109.70:8088',});
    };
    IOClient client = IOClient(httpClient);
    var response = await client.send(request);
    var respStr = await response.stream.transform(utf8.decoder).join();
    LogUtil.i("upload response is $respStr");
    return response.statusCode == 200;
  }




-----------------------------------------------------
转载请注明来源此处
原地址:#

-----网友评论----
暂无评论
-----发表评论----
微网聚博客乐园 ©2014 blog.mn886.net 鲁ICP备14012923号   网站导航