文章系列

Flutter Dio源码剖析(一)--Dio介绍

Flutter Dio源码剖析(二)--HttpClient、Http、Dio比照

Flutter Dio源码剖析(三)--深度分析

Flutter Dio源码剖析(四)--封装

视频系列

Flutter Dio源码剖析(一)--Dio介绍视频教程

Flutter Dio源码剖析(二)--HttpClient、Http、Dio比照视频教程

Flutter Dio源码剖析(三)--深度分析视频教程

Flutter Dio源码剖析(四)--封装视频教程

源码仓库地址

github仓库地址

前言

本文会手把手教你该怎么去封装一个类库,平时在咱们的工作中都是拿着他人的造好的轮子在应用,这篇文章将带你怎么去本人造轮子,当前再碰到别的类库须要对其进行封装的时候提供一个的思路和办法。

为什么须要封装Dio?

在后面的文章中,咱们对Dio的根本应用、申请库比照、源码剖析,咱们晓得Dio 的应用十分的简略,那为什么还须要进行封装呢?有两点如下:

1、代码迁徙

当组件库办法产生重要扭转须要迁徙的时候如果有多处中央用到,那么须要对应用到的每个文件都进行批改,十分的繁琐而且很容易出问题。

2、申请库切换

当不须要Dio 库的时候,咱们能够随时不便切换到别的网络申请库,当然Dio 目前内置反对应用第三方库的适配器。

3、对立配置

因为一个应用程序根本都是对立的配置形式,所以咱们能够针对拦截器转换器缓存对立处理错误代理配置证书校验 等多个配置进行对立治理。

应用单例模式进行Dio封装

为什么应用单例模式?

因为咱们的应用程序在每个页面中都会用到网络申请,那么如果咱们每次申请的时候都去实例化一个Dio,无非是减少了零碎不必要的开销,而应用单例模式对象一旦创立每次拜访都是同一个对象,不须要再次实例化该类的对象。

创立单例类

这是通过动态变量的公有结构器来创立的单例模式

class DioUtil {  factory DioUtil() => _getInstance();  static DioUtil get instance => _getInstance();  static DioUtil _instance;  DioUtil._init() {    // 初始化  }  static DioUtil _getInstance() {    if (_instance == null) {      _instance = DioUtil._init();    }    return _instance;  }}

对Dio申请进行初始化

咱们对 超时工夫响应工夫BaseUrl 进行对立设置

/// 连贯超时工夫static const int CONNECT_TIMEOUT = 60*1000;/// 响应超时工夫static const int RECEIVE_TIMEOUT = 60*1000;/// 申明Dio变量Dio _dio;DioUtil._init() {  if (_dio == null) {    /// 初始化根本选项    BaseOptions options = BaseOptions(      baseUrl: "http://localhost:8080",      connectTimeout: CONNECT_TIMEOUT,      receiveTimeout: RECEIVE_TIMEOUT    );    /// 初始化dio    _dio = Dio(options);  }}

对Restful APi格调进行对立封装

因为不论是get()还是post()申请,Dio 外部最终都会调用request 办法,只是传入的method 不一样,所以咱们这里定义一个枚举类型在一个办法中进行解决

enum DioMethod {  get,  post,  put,  delete,  patch,  head,}/// 申请类Future<T> request<T>(String path, {  DioMethod method = DioMethod.get,  Map<String, dynamic> params,  data,  CancelToken cancelToken,  Options options,  ProgressCallback onSendProgress,  ProgressCallback onReceiveProgress,}) async {  const _methodValues = {    DioMethod.get: 'get',    DioMethod.post: 'post',    DioMethod.put: 'put',    DioMethod.delete: 'delete',    DioMethod.patch: 'patch',    DioMethod.head: 'head'  };  options ??= Options(method: _methodValues[method]);  try {    Response response;    response = await _dio.request(path,                                  data: data,                                  queryParameters: params,                                  cancelToken: cancelToken,                                  options: options,                                  onSendProgress: onSendProgress,                                  onReceiveProgress: onReceiveProgress                                 );    return response.data;  } on DioError catch (e) {    throw e;  }}

拦截器

介绍

咱们曾经把Restful API 格调简化成了一个办法,通过DioMethod 来表明不同的申请形式。在咱们平时开发的过程中,须要在申请前、响应前、谬误时对某一些接口做非凡的解决,那咱们就须要用到拦截器。Dio 为咱们提供了自定义拦截器性能,很容易轻松的实现对申请、响应、谬误时进行拦挡

谬误对立解决

咱们发现尽管Dio框架曾经封装了一个DioError类库,但如果须要对返回的谬误进行对立弹窗解决或者路由跳转等就只能自定义了

申请前对立解决

在咱们发送申请的时候会碰到几种状况,比方须要对非open结尾的接口主动加上一些特定的参数,获取须要在申请头减少对立的token

响应前对立解决

在咱们申请接口前能够对响应数据进行一些根底的解决,比方对响应的后果进行自定义封装,还能够针对独自的url 做非凡解决等。

自定义拦截器实现

import 'package:dio/dio.dart';import 'package:flutter_dio/dio_util/dio_response.dart';class DioInterceptors extends Interceptor {  @override  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {    // 对非open的接口的申请参数全副减少userId    if (!options.path.contains("open")) {      options.queryParameters["userId"] = "xxx";    }    // 头部增加token    options.headers["token"] = "xxx";    // 更多业务需要    handler.next(options);    // super.onRequest(options, handler);  }  @override  void onResponse(Response response, ResponseInterceptorHandler handler) {    // 申请胜利是对数据做根本解决    if (response.statusCode == 200) {      response.data = DioResponse(code: 0, message: "申请胜利啦", data: response);    } else {      response.data = DioResponse(code: 1, message: "申请失败啦", data: response);    }    // 对某些独自的url返回数据做非凡解决    if (response.requestOptions.baseUrl.contains("???????")) {      //....    }    // 依据公司的业务需要进行定制化解决    // 重点    handler.next(response);  }  @override  void onError(DioError err, ErrorInterceptorHandler handler) {    switch(err.type) {        // 连贯服务器超时      case DioErrorType.connectTimeout:        {          // 依据本人的业务需要来设定该如何操作,能够是弹出框提醒/或者做一些路由跳转解决        }        break;        // 响应超时      case DioErrorType.receiveTimeout:        {          // 依据本人的业务需要来设定该如何操作,能够是弹出框提醒/或者做一些路由跳转解决        }        break;        // 发送超时      case DioErrorType.sendTimeout:        {          // 依据本人的业务需要来设定该如何操作,能够是弹出框提醒/或者做一些路由跳转解决        }        break;        // 申请勾销      case DioErrorType.cancel:        {          // 依据本人的业务需要来设定该如何操作,能够是弹出框提醒/或者做一些路由跳转解决        }        break;        // 404/503谬误      case DioErrorType.response:        {          // 依据本人的业务需要来设定该如何操作,能够是弹出框提醒/或者做一些路由跳转解决        }        break;        // other 其余谬误类型      case DioErrorType.other:        {        }        break;    }    super.onError(err, handler);  }}class DioResponse<T> {  /// 音讯(例如胜利音讯文字/谬误音讯文字)  final String message;  /// 自定义code(可依据外部定义形式)  final int code;  /// 接口返回的数据  final T data;  /// 须要增加更多  /// .........  DioResponse({    this.message,    this.data,    this.code,  });  @override  String toString() {    StringBuffer sb = StringBuffer('{');    sb.write("\"message\":\"$message\"");    sb.write(",\"errorMsg\":\"$code\"");    sb.write(",\"data\":\"$data\"");    sb.write('}');    return sb.toString();  }}class DioResponseCode {  /// 胜利  static const int SUCCESS = 0;  /// 谬误  static const int ERROR = 1;  /// 更多}

转换器

介绍

转换器Transformer 用于对申请数据和响应数据进行编解码解决。Dio实现了一个默认转换器DefaultTransformer作为默认的 Transformer. 如果想对申请/响应数据进行自定义编解码解决,能够提供自定义转换器

为什么须要转换器?

咱们看了转换器的介绍,发现和拦截器的性能差不多,那为什么还要存在转换器,有两点:

  1. 和拦截器解耦
  2. 不批改原始申请数据

执行流程:申请拦截器 >> 申请转换器 >> 发动申请 >> 响应转换器 >> 响应拦截器 >> 最终后果

申请转换器

只会被用于 'PUT'、 'POST'、 'PATCH'办法,因为只有这些办法才能够携带申请体(request body)

响应转换器

会被用于所有申请办法的返回数据。

自定义转换器实现

import 'dart:async';import 'package:dio/dio.dart';class DioTransformer extends DefaultTransformer {  @override  Future<String> transformRequest(RequestOptions options) async {    // 如果申请的数据接口是List<String>那咱们间接抛出异样    if (options.data is List<String>) {      throw DioError(        error: "你不能间接发送List数据到服务器",        requestOptions: options,      );    } else {      return super.transformRequest(options);    }  }  @override  Future transformResponse(RequestOptions options, ResponseBody response) async {    // 例如咱们响应选项外面没有自定义某些头部数据,那咱们就能够自行添加    options.extra['myHeader'] = 'abc';    return super.transformResponse(options, response);  }}

刷新Token

在开发过程中,客户端和服务器打交道的时候,往往会用一个token来做校验,因为每个公司解决刷新token的逻辑都不一样,我这里举一个简略的例子

咱们须要给所有的申请头中增加一个refreshToken,如果refreshToken不存在,咱们先去申请refreshToken,获取到refreshToken后,再发动后续申请。 因为申请refreshToken的过程是异步的,咱们须要在申请过程中锁定后续申请(因为它们须要refreshToken), 直到refreshToken申请胜利后,再解锁
import 'package:dio/dio.dart';import 'package:flutter_dio/dio_util/dio_util.dart';class DioTokenInterceptors extends Interceptor {  @override  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {    if (options.headers['refreshToken'] == null) {      DioUtil.instance.dio.lock();      Dio _tokenDio = Dio();      _tokenDio..get("http://localhost:8080/getRefreshToken").then((d) {        options.headers['refreshToken'] = d.data['data']['token'];        handler.next(options);      }).catchError((error, stackTrace) {        handler.reject(error, true);      }) .whenComplete(() {        DioUtil.instance.dio.unlock();      }); // unlock the dio    } else {      options.headers['refreshToken'] = options.headers['refreshToken'];      handler.next(options);    }  }  @override  void onResponse(Response response, ResponseInterceptorHandler handler) async {    // 响应前须要做刷新token的操作    super.onResponse(response, handler);  }  @override  void onError(DioError err, ErrorInterceptorHandler handler) {    super.onError(err, handler);  }}

勾销申请

为什么咱们须要有勾销申请的性能,如果当咱们的页面在发送申请时,用户被动退出以后界面或者app应用程序退出的时候数据还没有响应,那咱们就须要勾销该网络申请,避免不必要的谬误。

/// 勾销申请tokenCancelToken _cancelToken = CancelToken();/// 勾销网络申请void cancelRequests({CancelToken token}) {  token ?? _cancelToken?.cancel("cancelled");}

cookie治理

cookie介绍

服务器生成一小段文本信息,发送给浏览器,浏览器把 cookie 以kv模式保留到本地某个目录下的文本文件内,下一次申请同一网站时会把该 cookie 发送给服务器。

原理

  1. 客户端发送一个申请(http申请+用户认证信息)到服务器
  2. 认证胜利,服务器发送一个HttpResponse响应到客户端,其中蕴含Set-Cookie的头部
  3. 客户端提取并保留 cookie 于内存或磁盘
  4. 再次申请时,HttpRequest申请中会蕴含一个已认证的 Cookie 的头部
  5. 服务器解析cookie,获取 cookie 中客户端的相干信息
  6. 服务器返回响应数据

应用

cookie 的应用须要用到两个第三方组件 dio_cookie_managercookie_jar

  • cookie_jar:Darthttp 申请的 cookie 管理器,通过它您能够轻松解决简单的 cookie 策略和长久化 cookie
  • dio_cookie_manager: CookieManager 拦截器能够帮忙咱们主动治理申请/响应 cookie。 CookieManager 依赖于 cookieJar 包

导入文件

dio_cookie_manager: ^2.0.0cookie_jar: ^3.0.1
/// cookieCookieJar cookieJar = CookieJar();/// 增加cookie管理器_dio.interceptors.add(CookieManager(cookieJar));List<Cookie> cookies = [  Cookie("xxx", xxx),  // ....];//Save cookies            DioUtil.instance.cookieJar.saveFromResponse(Uri.parse(BaseUrl.url), cookies);//Get cookies   List<Cookie> cookies = DioUtil.instance.cookieJar.loadForRequest(Uri.parse(BaseUrl.url));

网络接口缓存

为什么应用缓存?

因为在咱们平时的开发过程中,会碰到一种状况,在进行网络申请时,咱们心愿能失常拜访到上次的数据,对于用户的体验比拟好,而不是展现一个空白的页面,该缓存次要是 《Flutter实战》网络接口缓存 提供参考。

应用shared_preferences长久化

咱们在程序退出后内存缓存将会隐没,所以咱们用shared_preferences 进行磁盘缓存数据。

import 'dart:collection';import 'package:dio/dio.dart';import 'package:flutter_dio/dio_util/dio_util.dart';class CacheObject {  CacheObject(this.response)    : timeStamp = DateTime.now().millisecondsSinceEpoch;  Response response;  int timeStamp;  @override  bool operator ==(other) {    return response.hashCode == other.hashCode;  }  @override  int get hashCode => response.realUri.hashCode;}class DioCacheInterceptors extends Interceptor {  // 为确保迭代器程序和对象插入工夫统一程序统一,咱们应用LinkedHashMap  var cache = LinkedHashMap<String, CacheObject>();  @override  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {    if (!DioUtil.CACHE_ENABLE) return super.onRequest(options, handler);    // 通过refresh字段来判断是否刷新缓存    bool refresh = options.extra["refresh"] == true;    if (refresh) {      // 删除本地缓存      delete(options.uri.toString());    }    // 只有get申请才开启缓存    if (options.extra["noCache"] != true &&        options.method.toLowerCase() == 'get') {      String key = options.extra["cacheKey"] ?? options.uri.toString();      var ob = cache[key];      if (ob != null) {        //若缓存未过期,则返回缓存内容        if ((DateTime.now().millisecondsSinceEpoch - ob.timeStamp) / 1000 <            DioUtil.MAX_CACHE_AGE) {          return handler.resolve(cache[key].response);        } else {          //若已过期则删除缓存,持续向服务器申请          cache.remove(key);        }      }    }    super.onRequest(options, handler);  }  @override  void onResponse(Response response, ResponseInterceptorHandler handler) {    // 把响应的数据保留到缓存    if (DioUtil.CACHE_ENABLE) {      _saveCache(response);    }    super.onResponse(response, handler);  }  @override  void onError(DioError err, ErrorInterceptorHandler handler) {    // TODO: implement onError    super.onError(err, handler);  }  _saveCache(Response object) {    RequestOptions options = object.requestOptions;    if (options.extra["noCache"] != true &&        options.method.toLowerCase() == "get") {      // 如果缓存数量超过最大数量限度,则先移除最早的一条记录      if (cache.length == DioUtil.MAX_CACHE_COUNT) {        cache.remove(cache[cache.keys.first]);      }      String key = options.extra["cacheKey"] ?? options.uri.toString();      cache[key] = CacheObject(object);    }  }  void delete(String key) {    cache.remove(key);  }}

代理配置

在咱们用flutter进行抓包的时候须要配置Dio代理。由DefaultHttpClientAdapter 提供了一个onHttpClientCreate 回调来设置底层 HttpClient的代理。

/// 设置Http代理(设置即开启)void setProxy({  String proxyAddress,  bool enable = false}) {  if (enable) {    (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =      (HttpClient client) {      client.findProxy = (uri) {        return proxyAddress;      };      client.badCertificateCallback =        (X509Certificate cert, String host, int port) => true;    };  }}

证书校验

用于验证正在拜访的网站是否实在。提供安全性,因为证书和域名绑定,并且由根证书机构签名确认。

/// 设置https证书校验void setHttpsCertificateVerification({  String pem,  bool enable = false}) {  if (enable) {    (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate  = (client) {      client.badCertificateCallback=(X509Certificate cert, String host, int port){        if(cert.pem==pem){ // 验证证书          return true;        }        return false;      };    };  }}

对立日志打印

日志打印次要是帮忙咱们开发时进行辅助排错

/// 开启日志打印void openLog() {    _dio.interceptors.add(LogInterceptor(responseBody: true));}DioUtil().openLog();