(十二)Dart网络编程

一、Uri

Uri 类 提供了 编码和解码 URI(URL) 字符的功能。 这些函数处理 URI 特殊的字符,例如 &=。 Uri 类还可以解析和处理 URI 的每个部分,比如 host, port, scheme 等。

1.1.Encoding and decoding fully qualified URIs(编码解码URI)

要编码和解码除了 URI 中特殊意义(例如 /, :, &,#)的字符, 则可以使用 encodeFull()decodeFull() 函数。这两个函数可以用来编码和解码整个 URI,并且保留 URI 特殊意义的字符不变。

var uri = 'http://example.org/api?foo=some message';

var encoded = Uri.encodeFull(uri);
assert(encoded ==
    'http://example.org/api?foo=some%20message');

var decoded = Uri.decodeFull(encoded);
assert(uri == decoded);

注意:上面 somemessage 之间的空格被编码了。

1.2.Encoding and decoding URI components(编码解码URI组件)

使用encodeComponent()decodeComponent() 可以编码 和解码 URI 中的所有字符,特殊意义的字符(/, &, 和:等) 也会编码,

var uri = 'http://example.org/api?foo=some message';

var encoded = Uri.encodeComponent(uri);
assert(encoded ==
    'http%3A%2F%2Fexample.org%2Fapi%3Ffoo%3Dsome%20message');

var decoded = Uri.decodeComponent(encoded);
assert(uri == decoded);

注意:上面特殊字符也被编码了,比如 / 编码为 %2F

1.3.Parsing URIs

如果有个 Uri 对象或者 URI 字符串,使用 Uri 的属性 可以获取每个部分,比如 path。使用 parse() 静态 函数可以从字符串中解析一个 Uri 对象:

var uri = Uri.parse('http://example.org:8080/foo/bar#frag');

assert(uri.scheme   == 'http');
assert(uri.host     == 'example.org');
assert(uri.path     == '/foo/bar');
assert(uri.fragment == 'frag');
assert(uri.origin   == 'http://example.org:8080');

1.4.Building URIs

使用 Uri() 构造函数可以从 URI 的 各个部分来构造一个 Uri 对象:

var uri = new Uri(scheme: 'http', host: 'example.org',
                  path: '/foo/bar', fragment: 'frag');
assert(uri.toString() ==
    'http://example.org/foo/bar#frag');

更多信息参考 Uri API 文档

二、dart原生网络请求API

原生API使用的是用dart:io中的HttpClient发起的请求,但HttpClient本身功能较弱,很多常用功能都不支持。

HTTP API 在返回值中使用了Dart Futures。 建议使用async/await语法来调用API。

网络调用通常遵循如下步骤:

  • 1、创建 client.
  • 2、构造 Uri.
  • 3、发起请求, 等待请求,同时您也可以配置请求headers、 body。
  • 4、关闭请求, 等待响应.
  • 5、解码响应的内容.

以下示例对HTTPS GET请求返回的JSON数据进行解码:

import 'dart:async';
import 'dart:io';
import 'dart:convert'; // 使用dart:convert内置库可以简单解码和编码JSON



_getIPAddress() async {
    var url = 'https://httpbin.org/ip';
    // 1.创建 client.
    var httpClient = new HttpClient();
    // 2.构造 Uri.
    var uri = Uri.parse(url);

    String result;
    try {
      // 3.发起请求, 等待请求,同时您也可以配置请求headers、 body。
      var request = await httpClient.getUrl(uri);
      // 4.关闭请求, 等待响应.
      var response = await request.close();
      if (response.statusCode == HttpStatus.ok) {
       // 5.解码响应的内容.
        var json = await response.transform(utf8.decoder).join();
        var data = jsonDecode(json);
        result = data['origin'];
      } else {
        result =
        'Error getting IP address:\nHttp status ${response.statusCode}';
      }
    } catch (exception) {
      result = 'Failed getting IP address';
    }
    print(result); // 14.147.104.242, 14.147.104.242

三、使用第三方库http

http包含一组高级函数和类,可以方便地使用HTTP资源。它与平台无关,可以在命令行和浏览器上使用。

3.1.库的安装

  • 1、在pubspec.yaml文件添加这个包的依赖:
dependencies:
  http: ^0.12.0+2
  • 2、通过命令行pub命令安装包
pub get

如果你装了Flutter插件也可以用以下令(也可通过用户交互界面安装,如Visual Studio Code、Android Studio装了Flutter插件):

flutter packages get

3.2.库的使用

使用这个库最简单的方法是通过顶级函数。它们允许你以最便捷的方式发出单独的HTTP请求:

import 'package:http/http.dart' as http;

var url = 'http://example.com/whatsit/create';
var response = await http.post(url, body: {'name': 'doodle', 'color': 'blue'});
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');

print(await http.read('http://example.com/foobar.txt'));

如果向同一服务器发出多个请求,可以使用Client而不是一次性请求来保持打开持久连接。如果您这样做,请确保在完成时关闭client。

var client = new http.Client();
try {
  var uriResponse = await client.post('http://example.com/whatsit/create',
      body: {'name': 'doodle', 'color': 'blue'});
  print(await client.get(uriResponse.bodyFields['uri']));
} finally {
  client.close();
}

您还可以通过自己创建RequestStreamedRequest 对象并将其传递给Client.send来对请求和响应施加更详细的控制。

这个包被设计成可组合的。这使得外部库可以很容易地相互协作,向其添加行为。希望添加行为的库应该创建BaseClient的子类,该类包装另一个Client并添加所需的行为:

class UserAgentClient extends http.BaseClient {
  final String userAgent;
  final http.Client _inner;

  UserAgentClient(this.userAgent, this._inner);

  Future<StreamedResponse> send(BaseRequest request) {
    request.headers['user-agent'] = userAgent;
    return _inner.send(request);
  }
}

四、使用第三方库dio(推荐)

dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等...

4.1.添加依赖

  • 1、在pubspec.yaml文件添加这个包的依赖:
dependencies:
  dio: ^2.1.5  // 请使用pub上2.1分支的最新版本
  • 2、通过命令行pub命令安装包
pub get

如果你装了Flutter插件也可以用以下令(也可通过用户交互界面安装,如Visual Studio Code、Android Studio装了Flutter插件):

flutter packages get

4.2.示例

  • 一个极简的示例
import 'package:dio/dio.dart';
void getHttp() async {
  try {
    Response response = await Dio().get("http://www.baidu.com");
    print(response);
  } catch (e) {
    print(e);
  }
}
  • 发起一个 GET 请求 :
Response response;
Dio dio = new Dio();
response = await dio.get("/test?id=12&name=wendu")
print(response.data.toString());
// 请求参数也可以通过对象传递,上面的代码等同于:
response = await dio.get("/test", queryParameters: {"id": 12, "name": "wendu"});
print(response.data.toString());
  • 发起一个 POST 请求:
response = await dio.post("/test", data: {"id": 12, "name": "wendu"});
  • 发起多个并发请求:
response = await Future.wait([dio.post("/info"), dio.get("/token")]);
  • 下载文件:
response = await dio.download("https://www.google.com/", "./xx.html");
  • 以流的方式接收响应数据:
Response<ResponseBody> rs = await Dio().get<ResponseBody>(url,
  options: Options(responseType: ResponseType.stream), //设置接收类型为stream
);
print(rs.data.stream); //响应流
  • 以二进制数组的方式接收响应数据:
Response<List<int>> rs = await Dio().get<List<int>>(url,
 options: Options(responseType: ResponseType.bytes), //设置接收类型为bytes
);
print(rs.data); //二进制数组
  • 发送 FormData:
FormData formData = new FormData.from({
    "name": "wendux",
    "age": 25,
  });
response = await dio.post("/info", data: formData);
  • 通过FormData上传多个文件:
FormData formData = new FormData.from({
    "name": "wendux",
    "age": 25,
    "file1": new UploadFileInfo(new File("./upload.txt"), "upload1.txt"),
    //支持直接上传字节数组 (List<int>) ,方便直接上传内存中的内容
    "file2": new UploadFileInfo.fromBytes(
        utf8.encode("hello world"), "word.txt"),
    // 支持文件数组上传
    "files": [
        new UploadFileInfo(new File("./example/upload.txt"), "upload.txt"),
        new UploadFileInfo(new File("./example/upload.txt"), "upload.txt")
    ]
});
response = await dio.post("/info", data: formData);
  • 监听发送(上传)数据进度:
response = await dio.post(
  "http://www.dtworkroom.com/doris/1/2.0.0/test",
  data: {"aa": "bb" * 22},
  onSendProgress: (int sent, int total) {
    print("$sent $total");
  },
);
  • 以流的形式提交二进制数据:
// 二进制数据
List<int> postData = <int>[...];
await dio.post(
  url,
  data: Stream.fromIterable(postData.map((e) => [e])), //创建一个Stream<List<int>>
  options: Options(
    headers: {
      HttpHeaders.contentLengthHeader: postData.length, // 设置content-length
    },
  ),
);

注意:如果要监听提交进度,则必须设置content-length,反之则是可选的。

4.3.Dio APIs

建议:在项目中使用Dio单例,这样便可对同一个dio实例发起的所有请求进行一些统一的配置,比如设置公共header、请求基地址、超时时间等;这里有一个在Flutter工程中使用Dio单例(定义为top level变量)的示例供开发者参考。

你可以使用默认配置或传递一个可选 BaseOptions参数来创建一个Dio实例 :

Dio dio = new Dio(); // 使用默认配置

// 配置dio实例
dio.options.baseUrl = "https://www.xx.com/api";
dio.options.connectTimeout = 5000; //5s
dio.options.receiveTimeout = 3000;

// 或者通过传递一个 `BaseOptions`来创建dio实例
BaseOptions options = new BaseOptions(
    baseUrl: "https://www.xx.com/api",
    connectTimeout: 5000,
    receiveTimeout: 3000,
);
Dio dio = new Dio(options);

Dio实例的核心API是 :

Future request(String path, {data,Map queryParameters, Options options,CancelToken cancelToken, ProgressCallback onSendProgress, ProgressCallback onReceiveProgress)

response = await request(
      "/test",
      data: {"id": 12, "name": "xx"},
      options: Options(method: "GET"),
  );

请求方法别名

为了方便使用,Dio提供了一些其它的Restful API, 这些API都是request的别名。

  • Future get(...)

  • Future post(...)

  • Future put(...)

  • Future delete(...)

  • Future head(...)

  • Future put(...)

  • Future path(...)

  • Future download(...)

4.4.请求配置

下面是所有的请求配置选项。 如果请求method没有指定,则默认为GET :

{
  /// Http method.
  String method;

  /// 请求基地址,可以包含子路径,如: "https://www.google.com/api/".
  String baseUrl;

  /// Http请求头.
  Map<String, dynamic> headers;

  /// 连接服务器超时时间,单位是毫秒.
  int connectTimeout;
  /// 2.x中为接收数据的最长时限.
  int receiveTimeout;

  /// 请求路径,如果 `path` 以 "http(s)"开始, 则 `baseURL` 会被忽略; 否则,
  /// 将会和baseUrl拼接出完整的的url.
  String path = "";

  /// 请求的Content-Type,默认值是[ContentType.JSON].
  /// 如果您想以"application/x-www-form-urlencoded"格式编码请求数据,
  /// 可以设置此选项为 `ContentType.parse("application/x-www-form-urlencoded")`,  这样[Dio]
  /// 就会自动编码请求体.
  ContentType contentType;

  /// [responseType] 表示期望以那种格式(方式)接受响应数据。
  /// 目前 [ResponseType] 接受三种类型 `JSON`, `STREAM`, `PLAIN`.
  ///
  /// 默认值是 `JSON`, 当响应头中content-type为"application/json"时,dio 会自动将响应内容转化为json对象。
  /// 如果想以二进制方式接受响应数据,如下载一个二进制文件,那么可以使用 `STREAM`.
  ///
  /// 如果想以文本(字符串)格式接收响应数据,请使用 `PLAIN`.
  ResponseType responseType;

  /// `validateStatus` 决定http响应状态码是否被dio视为请求成功, 返回`validateStatus`
  ///  返回`true` , 请求结果就会按成功处理,否则会按失败处理.
  ValidateStatus validateStatus;

  /// 用户自定义字段,可以在 [Interceptor]、[Transformer] 和 [Response] 中取到.
  Map<String, dynamic> extra;

  /// 公共query参数
  Map<String, dynamic /*String|Iterable<String>*/ > queryParameters;
}

这里有一个完成的示例.

4.5.响应数据

当请求成功时会返回一个Response对象,它包含如下字段:

{
  /// 响应数据,可能已经被转换了类型, 详情请参考Options中的[ResponseType].
  var data;
  /// 响应头
  HttpHeaders headers;
  /// 本次请求信息
  Options request;
  /// Http status code.
  int statusCode;
  /// 是否重定向
  bool isRedirect;  
  /// 重定向信息   
  List<RedirectInfo> redirects ;
  /// 最终真正的请求地址(因为可能会重定向)
  Uri realUri;   
  /// 响应对象的自定义字段(可以在拦截器中设置它),调用方可以在`then`中获取.
  Map<String, dynamic> extra;
}

示例如下:

 Response response = await dio.get("https://www.google.com");
  print(response.data);
  print(response.headers);
  print(response.request);
  print(response.statusCode);
4.5.1.泛型支持

2.0.18版本后可以通过泛型来指定对响应数据

假如有一个url返回的是json数据,返回数据在默认情况下(options.responseType为json)会被自动转为Json对象(Map或List)的:

Response response = await dio.get("/test");
print(response.data is Map); //true,自动转为了map

上面的代码在IDE里面输入时,IDE是无法推断出response.data的真实类型,所以对于Map的方法和属性给不出提示,这时我们只需要指定Response的泛型参数为Map即可:

Response<Map<String,dynamic>> r = await dio.get("/test");
print(r.data.containsKey("errCode")); // IDE可以给出代码提示

有时我们如果想以字符串方式接收json文本的话,我们可以通过制定responseTypeplain来禁止自动转化:

Response response = await dio.get("/test", options: Options(responseType: ResponseType.plain));

现在,我们也可以通过指定泛型参数来做到这一点了:

Response response = await dio.get<String>("/test");

是不是很简单,但是,上面的写法有个瑕疵就是有些编辑器无法推断出response.data的类型,所以当你对输入response.data时,字符串的方法和属性不会被推荐出来,要解决这个问题很简单,我们只需要指定Response的泛型参数为String即可:

Response<String> response = await dio.get<String>("/test");

同理,如果我们在BaseOptions里设置了responseTypeResponseType.plain,那么我们需要对某一个接口返回的数据转为Map的化,可以指定泛型参数为Map:

dio.options.responseType = ResponseType.plain;
Response<Map> r= await dio.get<Map>("/test");

注意:当responseType类型为plain或json时,泛型参数只能是String、Map和List三种类型,所有的请求内容都可以String形式接收,但只有Json数据可以转为Map和List,所以如果泛型参数传入Map或List时,则会强制将响应内容转为Map或List,如果转换失败,则会抛出异常。

详细示例请参见这里

4.6.拦截器

每个 Dio 实例都可以添加任意多个拦截器,通过拦截器你可以在请求之前或响应之后(但还没有被 thencatchError处理)做一些统一的预处理操作。

dio.interceptors.add(InterceptorsWrapper(
    onRequest:(RequestOptions options){
     // 在请求被发送之前做一些事情
     return options; //continue
     // 如果你想完成请求并返回一些自定义数据,可以返回一个`Response`对象或返回`dio.resolve(data)`。
     // 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义数据data.
     //
     // 如果你想终止请求并触发一个错误,你可以返回一个`DioError`对象,或返回`dio.reject(errMsg)`,
     // 这样请求将被中止并触发异常,上层catchError会被调用。
    },
    onResponse:(Response response) {
     // 在返回响应数据之前做一些预处理
     return response; // continue
    },
    onError: (DioError e) {
      // 当请求失败时做一些预处理
     return e;//continue
    }
));
4.6.1.完成和终止请求/响应

在所有拦截器中,你都可以改变请求执行流, 如果你想完成请求/响应并返回自定义数据,你可以返回一个 Response 对象或返回 dio.resolve(data)的结果。 如果你想终止(触发一个错误,上层catchError会被调用)一个请求/响应,那么可以返回一个DioError 对象或返回 dio.reject(errMsg) 的结果.

dio.interceptors.add(InterceptorsWrapper(
  onRequest:(RequestOptions options){
   return dio.resolve("fake data")
  },
));
Response response = await dio.get("/test");
print(response.data);//"fake data"
4.6.2.拦截器中支持异步任务

拦截器中不仅支持同步任务,而且也支持异步任务, 下面是在请求拦截器中发起异步任务的一个实例:

dio.interceptors.add(InterceptorsWrapper(
    onRequest:(Options options) async{
        //...If no token, request token firstly.
        Response response = await dio.get("/token");
        //Set the token to headers
        options.headers["token"] = response.data["data"]["token"];
        return options; //continue
    }
));
4.6.3.Lock/unlock 拦截器

你可以通过调用拦截器的lock()/unlock 方法来锁定/解锁拦截器。一旦请求/响应拦截器被锁定,接下来的请求/响应将会在进入请求/响应拦截器之前排队等待,直到解锁后,这些入队的请求才会继续执行(进入拦截器)。这在一些需要串行化请求/响应的场景中非常实用,后面我们将给出一个示例。

tokenDio = new Dio(); //Create a new instance to request the token.
tokenDio.options = dio;
dio.interceptors.add(InterceptorsWrapper(
    onRequest:(Options options) async {
        // If no token, request token firstly and lock this interceptor
        // to prevent other request enter this interceptor.
        dio.interceptors.requestLock.lock();
        // We use a new Dio(to avoid dead lock) instance to request token.
        Response response = await tokenDio.get("/token");
        //Set the token to headers
        options.headers["token"] = response.data["data"]["token"];
        dio.interceptors.requestLock.unlock();
        return options; //continue
    }
));

Clear()方法

你也可以调用拦截器的clear()方法来清空等待队列。

4.6.4.别名

请求拦截器被锁定时,接下来的请求将会暂停,这等价于锁住了dio实例,因此,Dio示例上提供了请求拦截器lock/unlock的别名方法:

dio.lock() == dio.interceptors.requestLock.lock()

dio.unlock() == dio.interceptors.requestLock.unlock()

dio.clear() == dio.interceptors.requestLock.clear()

4.6.5.示例

假设这么一个场景:出于安全原因,我们需要给所有的请求头中添加一个csrfToken,如果csrfToken不存在,我们先去请求csrfToken,获取到csrfToken后,再发起后续请求。 由于请求csrfToken的过程是异步的,我们需要在请求过程中锁定后续请求(因为它们需要csrfToken), 直到csrfToken请求成功后,再解锁,代码如下:

dio.interceptors.add(InterceptorsWrapper(
    onRequest: (Options options) {
        print('send request:path:${options.path},baseURL:${options.baseUrl}');
        if (csrfToken == null) {
            print("no token,request token firstly...");
            //lock the dio.
            dio.lock();
            return tokenDio.get("/token").then((d) {
                options.headers["csrfToken"] = csrfToken = d.data['data']['token'];
                print("request token succeed, value: " + d.data['data']['token']);
                print(
                    'continue to perform request:path:${options.path},baseURL:${options.path}');
                return options;
            }).whenComplete(() => dio.unlock()); // unlock the dio
        } else {
            options.headers["csrfToken"] = csrfToken;
            return options;
        }
    }
));

完整的示例代码请点击 这里.

4.6.6.日志

我们可以添加 LogInterceptor 拦截器来自动打印请求、响应日志, 如:

dio.interceptors.add(LogInterceptor(responseBody: false)); //开启请求日志

由于拦截器队列的执行顺序是FIFO,如果把log拦截器添加到了最前面,则后面拦截器对options的更改就不会被打印(但依然会生效), 所以建议把log拦截添加到队尾。

4.7.Cookie管理

我们可以通过添加CookieManager拦截器来自动管理请求/响应 cookie。CookieManager 依赖 cookieJar package

dio cookie 管理 API 是基于开源库 cookie_jar.

你可以创建一个CookieJarPersistCookieJar 来帮您自动管理cookie, dio 默认使用 CookieJar , 它会将cookie保存在内存中。 如果您想对cookie进行持久化, 请使用 PersistCookieJar , 示例代码如下:

var dio = new Dio();
dio.interceptors.add(CookieManager(CookieJar()))

PersistCookieJar实现了RFC中标准的cookie策略.PersistCookieJar 会将cookie保存在文件中,所以 cookies 会一直存在除非显式调用 delete 删除.

注意: 在Flutter中,传给 PersistCookieJar 的路径必须是有效的,必须是设备中存在的路径并且路径拥有写权限,你可以通过 path_provider 包来获取正确的路径。

更多关于 cookie_jar 请参考 : https://github.com/flutterchina/cookie_jar .

4.7.1.自定义拦截器

开发者可以通过继承Interceptor 类来实现自定义拦截器,这是一个简单的缓存示例拦截器。

4.8.错误处理

当请求过程中发生错误时, Dio 会包装 Error/Exception 为一个 DioError:

try {
    //404
    await dio.get("https://wendux.github.io/xsddddd");
  } on DioError catch (e) {
    // The request was made and the server responded with a status code
    // that falls out of the range of 2xx and is also not 304.
    if (e.response) {
      print(e.response.data);
      print(e.response.headers);
      print(e.response.request);
    } else {
      // Something happened in setting up or sending the request that triggered an Error
      print(e.request);
      print(e.message);
    }
  }
4.8.1.DioError 字段
{
  /// 响应信息, 如果错误发生在在服务器返回数据之前,它为 `null`
  Response response;

  /// 错误描述.
  String message;

  /// 错误类型,见下文
  DioErrorType type;

  ///原始的error或exception对象,通常type为DEFAULT时存在。
  dynamic error;

  /// 错误栈信息,可能为null
  StackTrace stackTrace;
}
4.8.2.DioErrorType
enum DioErrorType {
  /// When opening  url timeout, it occurs.
  CONNECT_TIMEOUT,

  ///  Whenever more than [receiveTimeout] (in milliseconds) passes between two events from response stream,
  ///  [Dio] will throw the [DioError] with [DioErrorType.RECEIVE_TIMEOUT].
  ///
  ///  Note: This is not the receiving time limitation.
  RECEIVE_TIMEOUT,

  /// When the server response, but with a incorrect status, such as 404, 503...
  RESPONSE,

  /// When the request is cancelled, dio will throw a error with this type.
  CANCEL,

  /// Default error type, Some other Error. In this case, you can
  /// read the DioError.error if it is not null.
  DEFAULT
}

4.9.使用application/x-www-form-urlencoded编码

默认情况下, Dio 会将请求数据(除过String类型)序列化为 JSON. 如果想要以 application/x-www-form-urlencoded格式编码, 你可以显式设置contentType :

//Instance level
dio.options.contentType=ContentType.parse("application/x-www-form-urlencoded");
//or works once
dio.post("/info",data:{"id":5}, options: new Options(contentType:ContentType.parse("application/x-www-form-urlencoded")));

这里有一个示例.

4.10.FormData

Dio支持发送 FormData, 请求数据将会以 multipart/form-data方式编码, FormData中可以一个或多个包含文件 .

FormData formData = new FormData.from({
    "name": "wendux",
    "age": 25,
    "file": new UploadFileInfo(new File("./example/upload.txt"), "upload.txt")
});
response = await dio.post("/info", data: formData);

注意: 只有 post 方法支持发送 FormData.

这里有一个完整的示例.

4.11.转换器

转换器Transformer 用于对请求数据和响应数据进行编解码处理。Dio实现了一个默认转换器DefaultTransformer作为默认的 Transformer. 如果你想对请求/响应数据进行自定义编解码处理,可以提供自定义转换器,通过 dio.transformer设置。

请求转换器 Transformer.transformRequest(...) 只会被用于 'PUT'、 'POST'、 'PATCH'方法,因为只有这些方法才可以携带请求体(request body)。但是响应转换器 Transformer.transformResponse() 会被用于所有请求方法的返回数据。

4.11.1.Flutter中设置

如果你在开发Flutter应用,强烈建议json的解码通过compute方法在后台进行,这样可以避免在解析复杂json时导致的UI卡顿。

// 必须是顶层函数
_parseAndDecode(String response) {
  return jsonDecode(response);
}

parseJson(String text) {
  return compute(_parseAndDecode, text);
}

void main() {
  ...
  // 自定义 jsonDecodeCallback
  (dio.transformer as DefaultTransformer).jsonDecodeCallback = parseJson;
  runApp(MyApp());
}
4.11.2.其它示例

这里有一个 自定义Transformer的示例.

4.11.3.执行流

虽然在拦截器中也可以对数据进行预处理,但是转换器主要职责是对请求/响应数据进行编解码,之所以将转化器单独分离,一是为了和拦截器解耦,二是为了不修改原始请求数据(如果你在拦截器中修改请求数据(options.data),会覆盖原始请求数据,而在某些时候您可能需要原始请求数据). Dio的请求流是:

请求拦截器 >> 请求转换器 >> 发起请求 >> 响应转换器 >> 响应拦截器 >> 最终结果

这是一个自定义转换器的示例.

4.12.HttpClientAdapter

HttpClientAdapter是 Dio 和 HttpClient之间的桥梁。2.0抽象出adapter主要是方便切换、定制底层网络库。Dio实现了一套标准的、强大API,而HttpClient则是真正发起Http请求的对象。我们通过HttpClientAdapter将Dio和HttpClient解耦,这样一来便可以自由定制Http请求的底层实现,比如,在Flutter中我们可以通过自定义HttpClientAdapter将Http请求转发到Native中,然后再由Native统一发起请求。再比如,假如有一天OKHttp提供了dart版,你想使用OKHttp发起http请求,那么你便可以通过适配器来无缝切换到OKHttp,而不用改之前的代码。

Dio 使用DefaultHttpClientAdapter作为其默认HttpClientAdapter,DefaultHttpClientAdapter使用dart:io:HttpClient 来发起网络请求。

这里 有一个简单的自定义Adapter的示例,读者可以参考。另外本项目的自动化测试用例全都是通过一个自定义的MockAdapter来模拟服务器返回数据的。

4.12.1.设置Http代理

DefaultHttpClientAdapter 提供了一个onHttpClientCreate 回调来设置底层 HttpClient的代理,我们想使用代理,可以参考下面代码:

(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
    // config the http client
    client.findProxy = (uri) {
        //proxy all request to localhost:8888
        return "PROXY localhost:8888";
    };
    // you can also create a new HttpClient to dio
    // return new HttpClient();
};

完整的示例请查看这里.

4.12.2.Https证书校验

有两种方法可以校验https证书,假设我们的后台服务使用的是自签名证书,证书格式是PEM格式,我们将证书的内容保存在本地字符串中,那么我们的校验逻辑如下:

String PEM = "XXXXX"; // certificate content
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate  = (client) {
    client.badCertificateCallback=(X509Certificate cert, String host, int port){
        if(cert.pem==PEM){ // Verify the certificate
            return true;
        }
        return false;
    };
};

X509Certificate是证书的标准格式,包含了证书除私钥外所有信息,读者可以自行查阅文档。另外,上面的示例没有校验host,是因为只要服务器返回的证书内容和本地的保存一致就已经能证明是我们的服务器了(而不是中间人),host验证通常是为了防止证书和域名不匹配。

对于自签名的证书,我们也可以将其添加到本地证书信任链中,这样证书验证时就会自动通过,而不会再走到badCertificateCallback回调中:

(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate  = (client) {
    SecurityContext sc = new SecurityContext();
    //file is the path of certificate
    sc.setTrustedCertificates(file);
    HttpClient httpClient = new HttpClient(context: sc);
    return httpClient;
};

注意,通过setTrustedCertificates()设置的证书格式必须为PEM或PKCS12,如果证书格式为PKCS12,则需将证书密码传入,这样则会在代码中暴露证书密码,所以客户端证书校验不建议使用PKCS12格式的证书。

4.13.请求取消

你可以通过 cancel token 来取消发起的请求:

CancelToken token = new CancelToken();
dio.get(url, cancelToken: token)
    .catchError((DioError err){
        if (CancelToken.isCancel(err)) {
            print('Request canceled! '+ err.message)
        }else{
            // handle error.
        }
    });
// cancel the requests with "cancelled" message.
token.cancel("cancelled");

注意: 同一个cancel token 可以用于多个请求,当一个cancel token取消时,所有使用该cancel token的请求都会被取消。

完整的示例请参考取消示例.

参考资料:
https://pub.dev/packages/http
https://pub.dev/packages/dio

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 6.1 公钥密钥加密原理 6.1.1 基础知识 密钥:一般就是一个字符串或数字,在加密或者解密时传递给加密/解密算...
    AndroidMaster阅读 4,003评论 1 8
  • iOS网络编程读书笔记 Facade Tester客户端门面模式的实例(被动版本化) 被动版本化,所以硬编码URL...
    melouverrr阅读 1,600评论 3 7
  • dio是Flutter中文网开源的一个强大的Dart Http请求库,支持Restful API、FormData...
    lazydu阅读 69,058评论 10 56
  • API定义规范 本规范设计基于如下使用场景: 请求频率不是非常高:如果产品的使用周期内请求频率非常高,建议使用双通...
    有涯逐无涯阅读 2,519评论 0 6
  • 沙乙 稚子启门扉,迟疑客所窥。 缘来琴人谷,剑影舞相随。 黎夏藤瓜绕,暮冬山雪飞。 人皆京邑往,隐者武当归。
    沙乙阅读 386评论 0 0