Flutter网络请求库DIO入门文档 - 掘金

来源: Flutter网络请求库DIO入门文档 – 掘金

一、dart:io提供的HttpClient

1、支持常用的Http操作,比如get,post等

2、异步操作,在io.dart中有相关描述

  • This library allows you to work with files, directories,
  • sockets, processes, HTTP servers and clients, and more.
  • Many operations related to input and output are asynchronous
  • and are handled using [Future]s or [Stream]s, both of which
  • are defined in the [dart:async
  • library](../dart-async/dart-async-library.html).
  • To use the dart:io library in your code:
  • import 'dart:io';
    复制代码

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

创建 client. 构造 Uri. 发起请求, 等待请求,同时您也可以配置请求headers、 body。 关闭请求, 等待响应. 解码响应的内容. 4、示例,(httpbin.org 这个网站能测试 HTTP 请求和响应的各种信息,比如 cookie、ip、headers 和登录验证等,且支持 GET、POST 等多种方法)

_httpTest() async {
    var url = 'https://httpbin.org/ip';
    var httpClient = new HttpClient();
 
    String result;
    try {
      var request = await httpClient.getUrl(Uri.parse(url));
      var response = await request.close();
      if (response.statusCode == HttpStatus.ok) {
        var json = await response.transform(utf8.decoder).join();
        var data = jsonDecode(json);
        result = data['origin'];
        print(result);
      } else {
        result =
            'Error getting IP address:\nHttp status ${response.statusCode}';
      }
    } catch (exception) {
      result = 'Failed getting IP address';
    }
  }
输出结果:122.70.159.214

复制代码

二、dio

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

1、安装

dependencies:

dio: ^3.0.10

2、常用请求

以下实验基于Charles的MapLocal,json文件内容为{“animal”:”dog”}

– get

Response response;
Dio dio = _dio();
response = await dio.get("http://test?id=1&name=dio1&method=get");
 
// 请求参数也可以通过对象传递,上面的代码等同于:
response = await dio.get("http://test", queryParameters: {"id": 2, "name": "dio2"});
 
print(response.data['animal']);
复制代码

– post

Response response;
Dio dio = _dio();
response = await dio.post("http://test?id=1&name=dio1&method=post");
 
// 请求参数也可以通过对象传递,上面的代码等同于:
response = await dio.post("http://test", queryParameters: {"id": 2, "name": "dio2"});
 
print(response.data['animal']);
 
//监听接收数据进度
response = await dio.post(
    "http://test",
    queryParameters: {"id": 2, "name": "dio2"},
    onReceiveProgress: (int receive, int total) {
      print("$receive $total");
   },
);
复制代码

– 发起多个请求

Dio dio = _dio();
Future.wait([dio.post("http://test/test1"), dio.get("http://test/test2")]).then((e) {
   print(e);
}).catchError((e) {});
 
 
结果为[{"animal":"dog"}, {"animal":"dog"}]
复制代码

– 下载文件

Dio dio = _dio();
Response response =
await dio.download("https://www.baidu.com/", "assets/data/test.html");
复制代码

– 上传文件

Response response;
Dio dio = _dio();
FormData formData;
formData = FormData.fromMap({
   "animal": "dog",
});
response = await dio.post("http/test/upload", data: formData);
 
//上传多个文件
formData = FormData.fromMap({
   "animal": "dog",
   "files": [
       await MultipartFile.fromFile("assets/data/test1.json", filename: "test1.json"),
       await MultipartFile.fromFile("assets/data/test2.json", filename: "test2.json"),
     ]
});
response = await dio.post("http/test/upload", data: formData);
复制代码

3、配置dio

Dio dio = Dio();
// 你可以使用默认配置或传递一个可选 BaseOptions参数来创建一个Dio实例 :
// 配置dio实例
dio.options.baseUrl = "https://www.xx.com/api";
dio.options.connectTimeout = 5000; //5s
dio.options.receiveTimeout = 3000;
 
// 或者通过传递一个 `options`来创建dio实例
BaseOptions options = BaseOptions(
  baseUrl: "https://www.xx.com/api",
  connectTimeout: 5000,
  receiveTimeout: 3000,
);
dio = Dio(options);
复制代码

4、请求配置

BaseOptions描述的是Dio实例发起网络请求的的公共配置,而Options类描述了每一个Http请求的配置信息,每一次请求都可以单独配置,单次请求的Options中的配置信息可以覆盖BaseOptions中的配置,下面是BaseOptions的配置项:
{
  /// 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,默认值是"application/json; charset=utf-8".
  /// 如果您想以"application/x-www-form-urlencoded"格式编码请求数据,
  /// 可以设置此选项为 `Headers.formUrlEncodedContentType`,  这样[Dio]
  /// 就会自动编码请求体.
  String 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;
 
  /// Common query parameters
  Map<String, dynamic /*String|Iterable<String>*/ > queryParameters;
}
复制代码

5、响应数据

当请求成功时会返回一个Response对象,它包含如下字段:
{
  /// 响应数据,可能已经被转换了类型, 详情请参考Options中的[ResponseType].
  T data;
  /// 响应头
  Headers headers;
  /// 本次请求信息
  Options request;
  /// Http status code.
  int statusCode;
  /// 是否重定向(Flutter Web不可用)
  bool isRedirect;
  /// 重定向信息(Flutter Web不可用)
  List<RedirectInfo> redirects ;
  /// 真正请求的url(重定向最终的uri)
  Uri realUri;
  /// 响应对象的自定义字段(可以在拦截器中设置它),调用方可以在`then`中获取.
  Map<String, dynamic> extra;
}
复制代码

6、拦截器

我们可以通过继承Interceptor来实现自定义的拦截器

每个 Dio 实例都可以添加任意多个拦截器,他们组成一个队列,拦截器队列的执行顺序是FIFO。通过拦截器你可以在请求之前或响应之后(但还没有被 then 或 catchError处理)做一些统一的预处理操作。
dio.interceptors.add(InterceptorsWrapper(
    onRequest:(RequestOptions options) async {
     // 在请求被发送之前做一些事情
     return options; //continue
     // 如果你想完成请求并返回一些自定义数据,可以返回一个`Response`对象或返回`dio.resolve(data)`。
     // 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义数据data.
     //
     // 如果你想终止请求并触发一个错误,你可以返回一个`DioError`对象,或返回`dio.reject(errMsg)`,
     // 这样请求将被中止并触发异常,上层catchError会被调用。
    },
    onResponse:(Response response) async {
     // 在返回响应数据之前做一些预处理
     return response; // continue
    },
    onError: (DioError e) async {
      // 当请求失败时做一些预处理
     return e;//continue
    }
));
复制代码

7、拦截器中可以进行其他异步操作

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
    }
));
复制代码

8、Lock/unlock拦截器

你可以通过调用拦截器的 lock()/unlock 方法来锁定/解锁拦截器。一旦请求/响应拦截器被锁定,接下来的请求/响应将会在进入请求/响应拦截器之前排队等待,直到解锁后,这些入队的请求才会继续执行(进入拦截器)。这在一些需要串行化请求/响应的场景中非常实用,后面我们将给出一个示例。
tokenDio = Dio(); //Create a instance to request the token.
tokenDio.options = dio.options;
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 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
    }
));
假设这么一个场景:我们需要给每个请求头中设置token,如果token不存在我们需要先请求token,获取到再继续请求,由于请求token过程是异步的,所以我们需要先锁定拦截器防止其他请求在没有获取到token的情况下进行网络请求,获取到token再解锁
复制代码

9、clear()方法来清空等待队列

dio.interceptors.clear()
复制代码

10、日志(开启后会打印request和response相关信息)

//由于拦截器队列的执行顺序是FIFO,如果把log拦截器添加到了最前面,则后面拦截器对options的更改就不会被打印(但依然会生效), 所以建议把log拦截添加到队尾。
dio.interceptors.add(LogInterceptor(responseBody: false)); //开启请求日志
复制代码

11、DioError

{
  /// Request info.
  RequestOptions request;
 
  /// Response info, it may be `null` if the request can't reach to
  /// the http server, for example, occurring a dns error, network is not available.
  Response response;
 
  /// 错误类型,见下文
  DioErrorType type;
 
  ///原始的error或exception对象,通常type为DEFAULT时存在。
  dynamic error;
}
 
enum DioErrorType {
  /// It occurs when url is opened timeout.
  CONNECT_TIMEOUT,
 
  /// It occurs when url is sent timeout.
  SEND_TIMEOUT,
 
  ///It occurs when receiving timeout.
  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
}

复制代码

12、CancelToken,取消请求

CancelToken token = CancelToken();
dio.post("/testpost?id=1&name=dio1&method=post",cancelToken: token).catchError((e) {
//我们会发现CancelToken提供了错误类型的判断,即此时CancelToken.isCancel(err)是true
//如
if (CancelToken.isCancel(err)) {
   print("被取消啦");
}
 
 
}).then((data) {
  return data;
});
token.cancel();
 
 
cancel_token.dart中源码也是判断DioErrorType
static bool isCancel(DioError e) {
  return e.type == DioErrorType.CANCEL;
}
复制代码

13、dio和HttpClient关系

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类来实现这个接口,Adapter类中是对接口中方法的不同实现,上层的调用代码不需要改变就可以随意切换对底层不同的功能调用。

14、设置代理

DefaultHttpClientAdapter 提供了一个onHttpClientCreate 回调来设置底层 HttpClient的代理,我们想使用代理,可以参考下面代码:
import 'package:dio/dio.dart';
import 'package:dio/adapter.dart';
...
(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 HttpClient to dio
    // return HttpClient();
};
复制代码

15、部分源码分析

dio.dart中
 
网络请求最终会调用到_request方法
当Response的泛型类为String且声明的ResponseType不为bytes和stream时
mergeOptions是将Dio的BaseOptions属性结合请求参数Options来生成一个RequestOptions对象,是最终发起网络请求的Options
Future<Response<T>> _request<T>(
    String path, {
    data,
    Map<String, dynamic> queryParameters,
    CancelToken cancelToken,
    Options options,
    ProgressCallback onSendProgress,
    ProgressCallback onReceiveProgress,
  }) async {
    if (_closed) {
      throw DioError(error: "Dio can't establish new connection after closed.");
    }
    options ??= Options();
    if (options is RequestOptions) {
      data = data ?? options.data;
      queryParameters = queryParameters ?? options.queryParameters;
      cancelToken = cancelToken ?? options.cancelToken;
      onSendProgress = onSendProgress ?? options.onSendProgress;
      onReceiveProgress = onReceiveProgress ?? options.onReceiveProgress;
    }
    var requestOptions = mergeOptions(options, path, data, queryParameters);
    requestOptions.onReceiveProgress = onReceiveProgress;
    requestOptions.onSendProgress = onSendProgress;
    requestOptions.cancelToken = cancelToken;
    if (T != dynamic &&
        !(requestOptions.responseType == ResponseType.bytes ||
            requestOptions.responseType == ResponseType.stream)) {
      if (T == String) {
        requestOptions.responseType = ResponseType.plain;
      } else {
        requestOptions.responseType = ResponseType.json;
      }
    }
 
 
 
拦截器会判断
checkIfNeedEnqueue方法的作用就是判断是否有未处理完(判断是否处理完从而加锁是通过Completer实现的)的请求,如果有的话,本次请求需要排队等待之前的请求的完成(值得学习的是由于一次请求返回的是Future,所以这里利用了future.then((Callback))返回的还是一个Future对象的特点,巧妙的实现了多次请求的顺序执行而相互之间不会干扰),这里的then中的Callback回调就是checkIfNeedEnqueue的第二个参数。
 
// Convert the request/response interceptor to a functional callback in which
    // we can handle the return value of interceptor callback.
    Function _interceptorWrapper(interceptor, bool request) {
      return (data) async {
        var type = request ? (data is RequestOptions) : (data is Response);
        var lock =
            request ? interceptors.requestLock : interceptors.responseLock;
        if (_isErrorOrException(data) || type) {
          return listenCancelForAsyncTask(
            cancelToken,
            Future(() {
              return checkIfNeedEnqueue(lock, () {
                if (type) {
                  if (!request) data.request = data.request ?? requestOptions;
                  return interceptor(data).then((e) => e ?? data);
                } else {
                  throw assureDioError(data, requestOptions);
                }
              });
            }),
          );
        } else {
          return assureResponse(data, requestOptions);
        }
      };
    }
 
 
//如果拦截器被锁,接下来的request/response任务会进入一个队列,否则继续执行
 
FutureOr checkIfNeedEnqueue(Lock lock, EnqueueCallback callback) {
    if (lock.locked) {
      return lock.enqueue(callback);
    } else {
      return callback();
    }
  }
 
 
  Future enqueue(EnqueueCallback callback) {
    if (locked) {
      // we use a future as a queue
      return _lock.then((d) => callback());
    }
    return null;
  }
 
 
//真正的网络请求,使用httpClientAdapter
//请求结果拿到后会查看interceptors.responseLock
//会监听是否有取消操作
// Initiate Http requests
  Future<Response<T>> _dispatchRequest<T>(RequestOptions options) async {
    var cancelToken = options.cancelToken;
    ResponseBody responseBody;
    try {
      var stream = await _transformData(options);
      responseBody = await httpClientAdapter.fetch(
        options,
        stream,
        cancelToken?.whenCancel,
      );
      responseBody.headers = responseBody.headers ?? {};
      var headers = Headers.fromMap(responseBody.headers ?? {});
      var ret = Response(
        headers: headers,
        request: options,
        redirects: responseBody.redirects ?? [],
        isRedirect: responseBody.isRedirect,
        statusCode: responseBody.statusCode,
        statusMessage: responseBody.statusMessage,
        extra: responseBody.extra,
      );
      var statusOk = options.validateStatus(responseBody.statusCode);
      if (statusOk || options.receiveDataWhenStatusError) {
        var forceConvert = !(T == dynamic || T == String) &&
            !(options.responseType == ResponseType.bytes ||
                options.responseType == ResponseType.stream);
        String contentType;
        if (forceConvert) {
          contentType = headers.value(Headers.contentTypeHeader);
          headers.set(Headers.contentTypeHeader, Headers.jsonContentType);
        }
        ret.data = await transformer.transformResponse(options, responseBody);
        if (forceConvert) {
          headers.set(Headers.contentTypeHeader, contentType);
        }
      } else {
        await responseBody.stream.listen(null).cancel();
      }
      checkCancelled(cancelToken);
      if (statusOk) {
        return checkIfNeedEnqueue(interceptors.responseLock, () => ret);
      } else {
        throw DioError(
          response: ret,
          error: 'Http status error [${responseBody.statusCode}]',
          type: DioErrorType.RESPONSE,
        );
      }
    } catch (e) {
      throw assureDioError(e, options);
    }
  }
 
 
 
//在io_adapter中
//dio中默认的adapter是DefaultHttpClientAdapter,其中HttpClient _defaultHttpClient,所以dio中的网络请求与底层HttpClient是通过DefaultHttpClientAdapter中对HttpClient网络请求的实现过程
 
class DefaultHttpClientAdapter implements HttpClientAdapter {
  /// [Dio] will create HttpClient when it is needed.
  /// If [onHttpClientCreate] is provided, [Dio] will call
  /// it when a HttpClient created.
  OnHttpClientCreate onHttpClientCreate;
 
  HttpClient _defaultHttpClient;
 
  bool _closed = false;
 
  @override
  Future<ResponseBody> fetch(
    RequestOptions options,
    Stream<List<int>> requestStream,
    Future cancelFuture,
  ) async {
    if (_closed) {
      throw Exception(
          "Can't establish connection after [HttpClientAdapter] closed!");
    }
    var _httpClient = _configHttpClient(cancelFuture, options.connectTimeout);
    Future requestFuture = _httpClient.openUrl(options.method, options.uri);
.....(略)
}
复制代码

16、扩展:为什么需要通过代码设置代理才可以使用charles抓包呢?

因为当我们启动 Charles就是启动了一个HTTP代理服务器,这类工具会通知操作系统,“现在我在系统上创建了一个HTTP代理,IP为XXXXXX端口为XX。这个时候运行在系统上的http客户端再去发送请求的时候,他就不会再去进行DNS解析,去连接目标服务器,而是直接连接系统告诉他代理所在的地址。然后代理服务器会与客户端建立连接,再然后代理服务器根据请求信息再去连接真正的服务器。而在Flutter中的http.dart有如下说明

/**

  • Sets the function used to resolve the proxy server to be used for
  • opening a HTTP connection to the specified [url]. If this
  • function is not set, direct connections will always be used.
  • The string returned by [f] must be in the format used by browser
  • PAC (proxy auto-config) scripts. That is either
  • "DIRECT"
    复制代码
  • for using a direct connection or
  • "PROXY host:port"
    复制代码
  • for using the proxy server [:host:] on port [:port:].
  • A configuration can contain several configuration elements
  • separated by semicolons, e.g.
  • "PROXY host:port; PROXY host2:port2; DIRECT"
    复制代码
  • The static function [findProxyFromEnvironment] on this class can
  • be used to implement proxy server resolving based on environment
  • variables. */ set findProxy(String f(Uri url));

//所以如果我们没有通过代码进行设置,就会直接请求到真正的服务器而不会走代理服务器

作者:wjq821
链接:https://juejin.cn/post/6959736964597678116
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏