美文网首页
Flutter之Dio拦截器 2024-06-19 周三

Flutter之Dio拦截器 2024-06-19 周三

作者: 松哥888 | 来源:发表于2024-06-20 22:20 被阅读0次

简介

简单使用,直接封装Dio就可以了,本人的文章Flutter之Dio封装一 2024-06-11 周二就是一次实际练习,确实可行。整个过程下来,感觉Dio相比于AFNetworking和Alamofire甚至是Moya都简单好用,确实不错。
一些听上去比较厉害的功能,都是通过拦截器来实现的,可以随时添加功能,确实是一个很好的设计思路。目前,还出现了配合Dio的一些插件,很多也是通过拦截器实现的,真的很不错。

Interceptor简介

  • 简单讲就是定义了网络传输前,传输成功,传输失败三个方法;并且定义了这三个方法的默认实现方式都是handler.next(),也就是顺序执行下一个拦截器
class Interceptor {
  /// The constructor only helps sub-classes to inherit from.
  /// Do not use it directly.
  const Interceptor();

  /// Called when the request is about to be sent.
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) {
    handler.next(options);
  }

  /// Called when the response is about to be resolved.
  void onResponse(
    Response response,
    ResponseInterceptorHandler handler,
  ) {
    handler.next(response);
  }

  /// Called when an exception was occurred during the request.
  void onError(
    DioException err,
    ErrorInterceptorHandler handler,
  ) {
    handler.next(err);
  }
}
  • InterceptorsWrapper继承Interceptor,以一种声明的方式定义这三个方法,把这三个方法放在构造参数的位置,是一个帮助方法,适合在Dio管理类中直接加拦截器。感觉还是尽量少用,推出拦截器的目的是解耦,把一些功能分散到其他类中,这样写又回去了,代码又堆积在管理类中,感觉不怎么好。

  • 自定义拦截器,直接继承Interceptor,然后自定义三个方法;不需要三个方法都重写,根据实际需求来就行。每个拦截器实现一个单一功能,符合单职原则,推荐用这种方式

import 'package:dio/dio.dart';
class CustomInterceptors extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    print('REQUEST[${options.method}] => PATH: ${options.path}');
    ///super.onRequest(options, handler);
    handler.next(options);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    print('RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}');
    ///super.onResponse(response, handler);
    handler.next(response);
  }

  @override
  Future onError(DioException err, ErrorInterceptorHandler handler) async {
    print('ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}');
    ///super.onError(err, handler);
    handler.next(err);
  }
}

super.xxx()的写法似是而非,应该用handler.xxx()的形式更直观。除了next之外,还有resolve和reject等方式,类似前端的Promise概念,可以根据实际业务的需要进行选择。

  • 感觉应该创建一个单例,统一管理这些拦截器,进一步简化Dio管理单例中的代码。

常见的拦截器

1. Token

  • 为了鉴权,为了验证身份,当前Token是常见的一种方式。比如上篇文章提到的登录接口,其返回就包含Token信息,登录接口成功之后,客户短会缓存本地,然后在后续个人相关的接口,一般都要在头部加上这个Token,不然的话就会被认为是越权访问而被拒绝。

  • 一般情况下,登录成功之后,就会把Token存本地,然后在请求的时候带上就可以了,一般会加在headers中。后端根据需要,去headers中取对应的值就可以了。

  • 根据以上的需求,只要重写onRequest方法,往headers中添加几个字段就可以了。

class TokenInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    options.headers['Authorization'] = LocalStorageUtil.getToken();

    handler.next(options);
  }
}

这里Authorization字段就是Token;一般在登录成功之后会存本地,而这里只要从本地缓存读取就好了,有就设置,没有就给个空字符串。具体的处理,交给后端就可以了。
默认的super.onRequest(options, handler);这里改成了handler.next(options);意图更加明确;

2. Log

  • 为了调试网络,log能带来很大的帮助。不过在release版本,最好能关闭log,流量只是小事,信息泄露风险才是大事。

  • 在Flutter中debugPrint这个千万不要用,这个字面意思有严重的误导,在生产模式,这个也会输出log,是个严重脑残的设计。

选项A:Dio提供deLogInterceptor

  • 虽然简陋了点,但是方便省事;log方法用一个Assert包了一下,算是比较取巧的方法。默认参数设置得不合理,需要改一下。
  LogInterceptor({
    this.request = true,      /// 这个一般要用
    this.requestHeader = true,   /// 这个很鸡肋,很多信息在request中有;不过一般都要开,前后端有些约定是放在头部的,比如Token之类的
    this.requestBody = false,  /// 这个要改成true,post的参数能显示出来,但是FormData的就不行了
    this.responseHeader = true,  /// 这个要改成false,大多数情况这个不会管
    this.responseBody = false,  /// 这个要改成true,这个是最重要的。响应数据都不看的话,你log个啥,脑子抽风了才会设置为false
    this.error = true,  /// 这个保持true不动
    this.logPrint = _debugPrint,   
  });

/// assert包一下,保证release不输出,比较取巧的做法
void _debugPrint(Object? object) {
  assert(() {
    print(object);
    return true;
  }());
}
  • 用这个的话很简单,只要一句话,把这个拦截器加入就可以了,简单省事。并且Dio的说明文档中建议log的拦截器加在最后,这样有利于把其他拦截器所做的修改也能打印出来,说得还是挺有道理的。
/// Note: LogInterceptor should always be the last interceptor added, otherwise modifications by following interceptors will not be logged.
_dio.interceptors.add(LogInterceptor(responseBody: true, requestBody: true, responseHeader: false, requestHeader: true));
  • 除了简单,其他的优点很少,缺点很多。图省事的情况下,勉强可用。效果如下,可以自己感受下:
flutter: *** Request ***
flutter: uri: http://47.92.232.69:8080/sign/signUp/password
flutter: method: POST
flutter: responseType: ResponseType.json
flutter: followRedirects: true
flutter: persistentConnection: true
flutter: connectTimeout: 0:00:05.000000
flutter: sendTimeout: null
flutter: receiveTimeout: 0:00:03.000000
flutter: receiveDataWhenStatusError: true
flutter: extra: {}
flutter: headers:
flutter:  Accept: application/json,*/*
flutter:  ContentType: application/json; charset=utf-8
flutter:  platform: ios
flutter:  content-type: application/json; charset=utf-8
flutter: data:
flutter: {phone: 138xxxxxxxx, password: 123456}
flutter:
flutter: *** Response ***
flutter: uri: http://47.92.232.69:8080/sign/signUp/password
flutter: Response Text:
flutter: {"code":-1,"data":null,"errMsg":"账号不存在"}

选项B:插件pretty_dio_logger

  • 使用简单,和LogInterceptor差不多,默认参数稍微改一下就可以了
  PrettyDioLogger(
      {this.request = true,       /// 保持true
      this.requestHeader = false,  /// 需要调Header参数的时候改为true,稳定后可以保持false
      this.requestBody = false,   /// 改为true,post参数,表单数据也能看
      this.responseHeader = false,  /// 保持false
      this.responseBody = true,  /// 保持true
      this.error = true, /// 保持默认
      this.maxWidth = 90,  /// 保持默认
      this.compact = true,  /// 保持默认
      this.logPrint = print}); /// 这个要改,系统的print不应该直接用,release模式不应该有log
  • 打印函数直接用print不合适,用判断条件包一下就好,用Assert那种取巧的方法没有必要
  /// Debug模式才输出log
  void _debugLogPrint(Object object) {
    if (kDebugMode) {
      print(object);
    }
  }
  • 使用和LogInterceptor一样简单,只需要一句话,添加拦截器就好了,最好遵循建议,加在最后。
      _dio.interceptors.add(PrettyDioLogger(
        request: true,
        requestHeader: true,
        requestBody: true,
        responseHeader: false,
        responseBody: true,
        logPrint: _debugLogPrint,
      ));
  • 输出的效果比LogInterceptor好很多,表单数据的Post请求参数也能打印出来。一个例子如下,可以对比一下:
flutter: ╔╣ Request ║ POST
flutter: ║  http://47.92.232.69:8080/sign/signUp/password
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter: ╔ Headers
flutter: ╟ Accept: application/json,*/*
flutter: ╟ ContentType: application/json; charset=utf-8
flutter: ╟ platform: ios
flutter: ╟ content-type: application/json; charset=utf-8
flutter: ╟ contentType: application/json; charset=utf-8
flutter: ╟ responseType: ResponseType.json
flutter: ╟ followRedirects: true
flutter: ╟ connectTimeout: 0:00:05.000000
flutter: ╟ receiveTimeout: 0:00:03.000000
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter: ╔ Form data | --dio-boundary-2480454291
flutter: ╟ phone: 15858109291
flutter: ╟ password: 123456
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter:
flutter: ╔╣ Response ║ POST ║ Status: 200
flutter: ║  http://47.92.232.69:8080/sign/signUp/password
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter: ╔ Body
flutter: ║
flutter: ║    {
flutter: ║         code: 0,
flutter: ║         "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjdXJyZW50IjoxNzE4ODUzNzQzOTkyLCJ1c2VyTmFt
flutter: ║         ZSI6IjE1ODU4MTA5MjkxIiwiZXhwIjoxNzUwMzg5NzQzLCJpYXQiOjE3MTg4NTM3NDN9.M45Ie_Wyr_Hni
flutter: ║         mmtp8-W6NpwH9ueaR-qymKAGXjT9iQ"
flutter: ║         errMsg: "处理成功"
flutter: ║    }
flutter: ║
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
  • 可选:Header在一开始的时候会看看,有些字段前后端可能会约定放在Header部分。成熟之后,可以考虑把Header隐藏掉,会更简洁。url, 参数,method,响应内容,这几个是最常用,最关心的。比如上面的例子隐藏Header之后的效果如下:
flutter: ╔╣ Request ║ POST
flutter: ║  http://47.92.232.69:8080/sign/signUp/password
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter: ╔ Form data | --dio-boundary-0514650006
flutter: ╟ phone: 15858109291
flutter: ╟ password: 123456
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter:
flutter: ╔╣ Response ║ POST ║ Status: 200
flutter: ║  http://47.92.232.69:8080/sign/signUp/password
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝
flutter: ╔ Body
flutter: ║
flutter: ║    {
flutter: ║         code: 0,
flutter: ║         "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjdXJyZW50IjoxNzE4ODU0NDUyMDgyLCJ1c2VyTmFt
flutter: ║         ZSI6IjE1ODU4MTA5MjkxIiwiZXhwIjoxNzUwMzkwNDUyLCJpYXQiOjE3MTg4NTQ0NTJ9.KkfTtiWFHwjrX
flutter: ║         wOuVcd7p3HQZo0me0eR_mijLbxUcok"
flutter: ║         errMsg: "处理成功"
flutter: ║    }
flutter: ║
flutter: ╚══════════════════════════════════════════════════════════════════════════════════════════╝

通常的做法,一开始会打开Request的Header,和后端对照Header参数;稳定后可以关闭Request的Header,基本上都一样,简洁一些更好。

选项C:自定义Interceptor,自己写一个

这个就不说了,有现成的代码可以抄,根据自己的意愿改。

3. 网络状态检查

  • 在传输数据之前,进行网络状态检查是有必要的。如果断网了,那么就可以直接错误退出,没有必要傻傻等超时。

  • 网络状态检查,可以写在通用的request方法中,不过有拦截器这么好用的手段,在一个独立的拦截其中做更合适,更有利于解耦。还有网络状态实时监控什么的,都可以做在这个拦截器中,更容易扩展。

  • 按照通常的理解,网络状态检查应该作为第1个拦截器,一旦判断是断网情况,直接handler.reject()就可以了,后续的拦截器什么的都不需要执行。甚至这个时候都可以考虑加个toast提醒用户。本次就打算采用这种武断的方式。

  • 另外一种思路是就算断网,后续的拦截器也应该执行,比如log拦截器,记录一下状态也好。这个根据具体情况,具体取舍就好,怎么做都可以。

  • 网络状态检查,有个比较流行的插件connectivity_plus,点赞数量比较多,直接拿来用,没必要自己再整一套。

    插件热度高
  • 按照上面的理解,只要重写onRequest一个方法就可以了。

class NetworkStatusInterceptor extends Interceptor {
  @override
  void onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {
    List<ConnectivityResult> resultList =
        await (Connectivity().checkConnectivity());
    if (resultList.contains(ConnectivityResult.none)) {
      LogUtil.log("检测到网络不通,请检查联网状态", level: LogLevel.fatal);
      handler.reject(
          DioException.connectionError(requestOptions: options, reason: "无网络"));
    } else {
      handler.next(options);
    }
  }
}

4. 错误处理

  • 一种是网络错误,Http返回值不是2XX(一般是200)的那种;这个就是通常认为的网络异常。DioException就是用来描述这些错误的,具体的错误类型用了一个枚举类型DioExceptionType来列举。每种类型都有一个对应的DioException,其中的message字段就是错误原因。只是其中的描述是英文的,不是很友好,可以考虑自己提供一套。由于这个时候,后端没有返回数据,所以可以模仿网络正常的时候,自定义一套code和message,放入DioException的response字段。
/// The exception enumeration indicates what type of exception
/// has happened during requests.
enum DioExceptionType {
  /// Caused by a connection timeout.
  connectionTimeout,

  /// It occurs when url is sent timeout.
  sendTimeout,

  ///It occurs when receiving timeout.
  receiveTimeout,

  /// Caused by an incorrect certificate as configured by [ValidateCertificate].
  badCertificate,

  /// The [DioException] was caused by an incorrect status code as configured by
  /// [ValidateStatus].
  badResponse,

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

  /// Caused for example by a `xhr.onError` or SocketExceptions.
  connectionError,

  /// Default error type, Some other [Error]. In this case, you can use the
  /// [DioException.error] if it is not null.
  unknown,
}
  • 另外一种错误叫做逻辑错误。网络是没问题的,服务器有响应,但是不符合业务规则,所以定义为错误。比如,没有Token,访问个人用户数据,就会定义为业务错误。比如这样的:
{"code":-1,"data":null,"errMsg":"账号不存在"}

有些特殊的业务,需要特殊处理,可以根据和后端的约定,判断code的值,然后在这里统一处理。比如,没有Token的时候,约定code是405,就弹出登录对话框。

  • 对于网络错误,可以根据DioException中的DioExceptionType对特定类型进行进行特殊处理。比如我们对证书问题,连接问题,取消问题三种情况比较关注,先记一下log,提醒开发者注意。
class ErrorInterceptor extends Interceptor {
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    final code = response.data["code"];
    if (code == 405) {
      ToastUtil.showText(text: "Token过期,请重新登录");
    }

    handler.next(response);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    switch (err.type) {
      case DioExceptionType.badCertificate:
        LogUtil.log("证书问题", level: LogLevel.warning);
      case DioExceptionType.cancel:
        LogUtil.log("请求被取消了", level: LogLevel.warning);
      case DioExceptionType.connectionError:
        LogUtil.log("连接问题,请检查网络状态", level: LogLevel.warning);
      default:
      // 什么也不做
    }

    handler.next(err);
  }
}

拦截器统一管理

将添加拦截器的代码集中到一个文件,简化主流程的复杂度。

class InterceptorManager {
  /// 单例
  static final InterceptorManager _instance = InterceptorManager._internal();

  factory InterceptorManager() => _instance;

  InterceptorManager._internal() {
    /// 网络状态拦截器开始侦听网络
    networkStatusInterceptor = NetworkStatusInterceptor();
    networkStatusInterceptor.startMonitorNetworkStatus();
  }

  /// 网络状态拦截器
  late NetworkStatusInterceptor networkStatusInterceptor;

  /// iOS模拟器,插件connectivity_plus有问题,所以给这个拦截器加个开关
  bool isCheckNetworkStatus = false;

  /// 添加拦截器
  void addTo(Dio dio) {
    /// 网络状态, 一般放第一个
    if (isCheckNetworkStatus) {
      dio.interceptors.add(networkStatusInterceptor);
    }

    /// token
    dio.interceptors.add(TokenInterceptor());

    /// 错误处理
    dio.interceptors.add(ErrorInterceptor());

    /// 日志; 一般放最后
    dio.interceptors.add(PrettyDioLogger(
      request: true,
      requestHeader: false,
      requestBody: true,
      responseHeader: false,
      responseBody: true,
      logPrint: _debugLogPrint,
    ));
  }

  /// Debug模式才输出log
  void _debugLogPrint(Object object) {
    if (kDebugMode) {
      print(object);
    }
  }
}

Loading和Toast

这两个没什么内容,并且需要变量控制,还是直接放在主流程比较好,不适合做成拦截器。考虑了Loading和Toast之后的request代码变成如下的样子:

  Future<BaseResponse> doRequest(
    String path,
    HttpMethod method, {
    Map<String, dynamic>? parameters,
    bool isShowLoading = false,
    bool isShowToast = true,
  }) async {
    Options options = Options(method: method.value);

    /// 根据method类型处理参数
    Object? data;
    Map<String, dynamic>? queryParameters;
    switch (method) {
      case HttpMethod.get:
        queryParameters = parameters;
      case HttpMethod.post:
        data = parameters;
      case HttpMethod.postFormData:
        if (parameters != null) {
          data = FormData.fromMap(parameters);
        }
    }

    /// 包装成自定义的响应;后端自定义的数据优先
    BaseResponse baseResponse = BaseResponse();
    try {
      if (isShowLoading) {
        LottieUtils.showToastLoading();
      }
      /// 统一调用request进行数据传输
      final dioResponse = await _dio.request(
        path,
        data: data,
        queryParameters: queryParameters,
        options: options,
      );
      baseResponse.code = dioResponse.data?["code"];
      baseResponse.data = dioResponse.data?["data"];
      baseResponse.errMsg = dioResponse.data?["errMsg"];
    } on DioException catch (e) {
      /// 异常时,将DioException转化为自定义的baseResponse
      baseResponse = BaseResponse.fromException(e);
    } finally {
      if (isShowLoading) {
        LottieUtils.hideToastLoading();
      }
      if (isShowToast && !baseResponse.isSuccess) {
        ToastUtil.showText(text: baseResponse.errMsg ?? "");
      }
    }
    return baseResponse;
  }

缓存和Cookie

这两个有现成的插件可用,暂时也不知道具体的价值,也没有想到必须导入的原因,网上的评价也一般,所以不考虑引入,等需要的时候再加就可以了。拦截器可以随时加,很方便的。另外,重试,代理等功能也是一样,不是强需求,可以必要的时候再考虑加。
dio_cache_interceptor
dio_cookie_manager

相关文章

网友评论

      本文标题:Flutter之Dio拦截器 2024-06-19 周三

      本文链接:https://www.haomeiwen.com/subject/eqefcjtx.html