Flutter dio 手把手教你封装一个实用网络工具

1,093 阅读11分钟

未命名文件

代码github地址

实现功能

  • 1、get、post请求
  • 2、自定义RequestOptions
  • 3、dio请求管理队列,用于统一管理请求
  • 4、HttpClient链接管理,用于获取解析DNS时间、TCP连接开始时间、SSL握手开始时间(如果是HTTPS)、首包时间
  • 5、json转model
  • 6、缓存管理
  • 7、日志管理拦截器
  • 8、数据转换管理拦截器
  • 9、loading拦截器
  • 10、token续租拦截器
  • 11、错误处理拦截器

1、基础使用

在 Flutter 中,dio 是一个强大的 HTTP 客户端,用于发送各种网络请求,如 GET、POST、PUT、DELETE 等。

pubspec.yaml 文件中添加 dio 依赖:

dependencies:
  dio: ^5.3.2
GET 请求
import 'package:dio/dio.dart';

void fetchData() async {
  try {
    // 创建 Dio 实例
    Dio dio = Dio();
    // 发送 GET 请求
    Response response = await dio.get('https://jsonplaceholder.typicode.com/posts/1');
    // 打印响应数据
    print(response.data);
  } catch (e) {
    // 打印错误信息
    print('请求出错: $e');
  }
}
POST 请求
import 'package:dio/dio.dart';

void postData() async {
  try {
    Dio dio = Dio();
    // 定义请求数据
    Map<String, dynamic> data = {
      'title': 'foo',
      'body': 'bar',
      'userId': 1
    };
    // 发送 POST 请求
    Response response = await dio.post('https://jsonplaceholder.typicode.com/posts', data: data);
    print(response.data);
  } catch (e) {
    print('请求出错: $e');
  }
}

2、自定义RequestOptions

因为我们项目中可能需要自定义header设置mockUrl等需求,我们这个时候可以自定义一个RequestOptions,帮组我们来统一管理Request

/// 请求方式
enum MyRequestMethod { get, post }

class MyRequestOptions {
  /// 请求方式
  MyRequestMethod method = MyRequestMethod.get;

  /// 基础url
  String baseUrl =
      "https://mockapi.eolink.com/uvemJdBf6d6fe15694c6ce211778969e0cfaacf4f97f262";

  /// 请求路径
  String urlPath = "";

  /// 参数
  Map<String, dynamic> params = Map<String, dynamic>();

  /// HTTP 请求头。
  Map<String, dynamic> headers = Map<String, dynamic>();

  /// 连接服务器超时时间.
  Duration connectTimeout = Duration(seconds: 10);

  /// 接收数据的超时设置。
  ///
  /// 这里的超时对应的时间是:
  ///  - 在建立连接和第一次收到响应数据事件之前的超时。
  ///  - 每个数据事件传输的间隔时间,而不是接收的总持续时间。
  ///
  /// 超时时会抛出类型为 [DioExceptionType.receiveTimeout] 的
  /// [DioException]。
  ///
  /// `null``Duration.zero` 即不设置超时。
  Duration receiveTimeout = Duration(seconds: 10);

  MyRequestOptions({required String url, Map<String, dynamic>? paramsMap}) {
    urlPath = url;
    if (paramsMap != null) {
      params.addAll(paramsMap);
    }

    // 设置默认header
    _addDefaultHeader();
  }

  /// 设置Mockurl
  void setMockUrl({required String mockUrl}) {
    if (kDebugMode) {
      baseUrl = mockUrl;
    }
  }

  String getMethod() {
    if (method == MyRequestMethod.get) {
      return "get";
    }
    return "post";
  }

  /// 设置默认header
  void _addDefaultHeader() {
    Map<String, dynamic> defaultHeader = {
      "Content-Type": "application/json; charset=utf-8",
      "Accept": "application/json"
    };
    headers.addAll(defaultHeader);
  }

  /// 设置header
  void setHeader({required Map<String, dynamic> headerMap}) {
    headers.addAll(headerMap);
  }
}

3、dio请求管理队列,用于统一管理请求

在发起请求以后,我们可能会因为各种情况需要管理这个请求,如取消某一个请求,或者取消全部请求等等,这个时候我们最好有一个管理工具类

取消请求

void cancelMultipleRequests() async {
  Dio dio = Dio();
  CancelToken cancelToken = CancelToken();

  try {
    // 第一个请求
    dio.get(
      'https://jsonplaceholder.typicode.com/posts/1',
      cancelToken: cancelToken,
    );
  } on DioException catch (e) {
    if (e.type == DioExceptionType.cancel) {
      print('请求已取消: ${e.message}');
    } else {
      print('请求出错: ${e.message}');
    }
  }

  // 取消所有使用该 CancelToken 的请求
  cancelToken.cancel('批量取消请求');
}

管理类

我们为每一个请求都创建一个cancelToken来管理,cancelToken的生成是根据MyRequestOptions来生成的

获取CancelTokenKey

String getCancelTokenKey({required MyRequestOptions options}) {
    String url = options.baseUrl + options.urlPath;
    String paramString = options.params.toString();
    String cancelTokenKey = (url + paramString).md5Hash();
    return cancelTokenKey;
  }

获取CancelToken

CancelToken getCancelToken({required MyRequestOptions options}) {
    String cancelTokenKey = getCancelTokenKey(options: options);
    CancelToken? cancelToken;
    cancelToken = cancelTokens[cancelTokenKey];
    if (cancelToken == null) {
      cancelToken = CancelToken();
      cancelTokens[cancelTokenKey] = cancelToken;
    }
    return cancelToken;
  }

我们使用一个单例来管理我们的CancelToken

class MyDioManager {
  final Map<String, CancelToken> cancelTokens = {};
  // 静态私有实例,初始值为 null
  static MyDioManager? _instance;
  // 私有构造函数
  MyDioManager._privateConstructor();

  // 静态工厂方法,用于获取单例实例
  static MyDioManager get instance {
    // 创建一个锁对象
    final Lock lock = Lock();
    lock.synchronized(() {
      _instance ??= MyDioManager._privateConstructor();
    });
    return _instance!;
  }

  // 取消某个请求
  void cancelRequest(String cancelTokenKey) {
    final cancelToken = cancelTokens[cancelTokenKey];
    if (cancelToken != null) {
      cancelToken.cancel('Request cancelled by user');
      cancelTokens.remove(cancelTokenKey);
    }
  }

  // 取消全部请求
  void cancelAllRequests() {
    cancelTokens.forEach((key, cancelToken) {
      cancelToken.cancel('All requests cancelled by user');
    });
    cancelTokens.clear();
  }
}

4、HttpClient链接管理,用于获取解析DNS时间、TCP连接开始时间、SSL握手开始时间(如果是HTTPS)、首包时间

Dio是基于Dart的http包开发的,但Dart本身在标准库中不提供这些底层的网络指标。我们可以通过一些自定义的方式来实现这些统计

DNS解析耗时

DNS解析通常发生在建立TCP连接之前。Dart的Socket类在连接时会解析DNS,但是dio并没有暴露相关信息,我们需要自己实现一个自定义的连接器,比如继承自Dio的CustomHttpClientAdapter,然后重写一些方法,在发起请求时记录时间。

对于DNS时间,可以在打开连接时记录开始时间,当Socket连接建立时,DNS解析已经完成,这时候可以计算DNS耗时

    // DNS解析开始时间
    final dnsStartTime = DateTime.now();
    // 创建HttpClient
    final httpClient = HttpClient();
    httpClient.badCertificateCallback =
        (X509Certificate cert, String host, int port) => true;
    // 解析DNS
    final uri = options.uri;
    final addresses = await InternetAddress.lookup(uri.host);
    // DNS解析结束时间
    final dnsEndTime = DateTime.now();
    final dnsTime = dnsEndTime.difference(dnsStartTime).inMilliseconds;

TCP三次握手耗时

TCP连接的时间是从开始连接到连接成功的时间差。同样需要在发起连接的时候记录开始时间,连接成功后记录结束时间,计算差值

// TCP 连接耗时
    final tcpStart = DateTime.now();
    var socket = await Socket.connect(
      addresses.first,
      options.uri.port ?? (options.uri.scheme == 'https' ? 443 : 80),
    );
    timings['tcpTime'] = DateTime.now().difference(tcpStart).inMilliseconds;

SSL 握手耗时(HTTPS)

SSL握手时间的话,如果是HTTPS请求,在建立TCP连接之后会进行SSL握手。这时候可以在SecureSocket.connect的时候记录时间,计算SSL握手的时间差。这需要覆盖处理HTTPS的部

    // SSL握手开始时间(如果是HTTPS)
    final sslStartTime = DateTime.now();
    SecureSocket? secureSocket;
    if (uri.scheme == 'https') {
      secureSocket = await SecureSocket.secure(
        socket,
        host: uri.host,
        onBadCertificate: (cert) => true,
      );
    }
    // SSL握手结束时间
    final sslEndTime = DateTime.now();

首包时间

即从请求发送到接收到第一个响应包的时间。这个可以通过拦截器来记录。在发送请求前记录时间,然后在接收到响应时记录第一个字节到达的时间,计算差值

// 首包时间开始记录
    final firstPacketStartTime = DateTime.now();
    // 使用默认适配器发送请求
    final response = await _defaultAdapter.fetch(
      options,
      requestStream,
      cancelFuture,
    );
    // 首包时间结束记录
    final firstPacketEndTime = DateTime.now();
    final firstPacketTime =
        firstPacketEndTime.difference(firstPacketStartTime).inMilliseconds;

完整代码

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';

class CustomHttpClientAdapter implements HttpClientAdapter {
  final HttpClientAdapter _defaultAdapter = DefaultHttpClientAdapter();

  @override
  Future<ResponseBody> fetch(
    RequestOptions options,
    Stream<Uint8List>? requestStream,
    Future? cancelFuture,
  ) async {
    final timings = <String, num>{};

    // 记录开始时间
    final startTime = DateTime.now();

    // DNS解析开始时间
    final dnsStartTime = DateTime.now();

    // 创建HttpClient
    final httpClient = HttpClient();
    httpClient.badCertificateCallback =
        (X509Certificate cert, String host, int port) => true;

    // 解析DNS
    final uri = options.uri;
    final addresses = await InternetAddress.lookup(uri.host);
    // DNS解析结束时间
    final dnsEndTime = DateTime.now();
    final dnsTime = dnsEndTime.difference(dnsStartTime).inMilliseconds;

    // TCP连接开始时间
    final tcpStartTime = DateTime.now();
    final socket = await Socket.connect(
      addresses.first,
      uri.port,
      timeout: const Duration(seconds: 10),
    );
    // TCP连接结束时间
    final tcpEndTime = DateTime.now();
    final tcpTime = tcpEndTime.difference(tcpStartTime).inMilliseconds;

    // SSL握手开始时间(如果是HTTPS)
    final sslStartTime = DateTime.now();
    SecureSocket? secureSocket;
    if (uri.scheme == 'https') {
      secureSocket = await SecureSocket.secure(
        socket,
        host: uri.host,
        onBadCertificate: (cert) => true,
      );
    }
    // SSL握手结束时间
    final sslEndTime = DateTime.now();
    final sslTime = sslEndTime.difference(sslStartTime).inMilliseconds;
    // 首包时间开始记录
    final firstPacketStartTime = DateTime.now();
    // 使用默认适配器发送请求
    final response = await _defaultAdapter.fetch(
      options,
      requestStream,
      cancelFuture,
    );
    // 首包时间结束记录
    final firstPacketEndTime = DateTime.now();
    final firstPacketTime =
        firstPacketEndTime.difference(firstPacketStartTime).inMilliseconds;
    // 总耗时
    final totalTime = DateTime.now().difference(startTime).inMilliseconds;

    // 打印统计信息
    timings['dns'] = dnsTime;
    timings['tcp'] = tcpTime;
    timings['ssl'] = sslTime;
    timings['first_packet'] = firstPacketTime;
    timings['totalTime'] = totalTime;

    // print('DNS解析耗时: $dnsTime ms');
    // print('TCP三次握手耗时: $tcpTime ms');
    // print('SSL握手耗时: $sslTime ms');
    // print('首包时间: $firstPacketTime ms');
    // print('总耗时: $totalTime ms');

    // 将耗时数据存入请求配置的 extra 字段
    options.extra.addAll(timings);
    return response;
  }

  @override
  void close({bool force = false}) {
    _defaultAdapter.close(force: force);
  }
}

使用时

 _dio.httpClientAdapter = CustomHttpClientAdapter();

我们可以把将耗时数据存入请求配置的 extra 字段,方便我们使用日志拦截器时,打印整个请求详细的信息

5、json转model

json转model我是借助于json_annotation实现的,我定义了两个基类model,用于解析普通类型MyBaseModel和数组类型MyBaseListModel

@JsonSerializable(genericArgumentFactories: true, converters: [SafeNumConverter()])
class MyBaseModel<T> extends SafeConvertModel {
  @JsonKey(name: 'code')
  num? code;
  @JsonKey(name: 'message')
  String? message;
  T? data;
 
  /// 是否成功
  bool isSucess() {
    bool result = this.code?.toInt() == 0;
    return result;
  }
}
class MyBaseListModel<T> {
  @JsonKey(name: 'code')
  num? code;
  @JsonKey(name: 'message')
  String? message;
  List<T>? data;
  /// 是否成功
  bool isSucess() {
    bool result = this.code == 0;
    return result;
  }
}

NetworkService中封装json转model

Future<MyBaseModel<T>> get<T>(
      {required MyRequestOptions options,
      required T Function(Object? json) fromJsonT}) async {
    // 发起请求
    MyResopnseModel response = await _request(options: options);
    if (response.isHttpSucess() == true) {
      try {
        return MyBaseModel.fromJson(
          response.data,
          fromJsonT,
        );
      } catch (e, stackTrace) {
        print('json转model失败: $e');
        throw e;
      }
    } else {
      throw _handleError(resopnse: response);
    }
  }

使用示例

try {
    // 发起 GET 请求获取用户信息
    MyBaseModel<User> result = await networkService.get<User>(
      '/user',
      fromJsonT: (json) => User.fromJson(json as Map<String, dynamic>),
    );

    if (result.isSucess()) {
      print('User name: ${result.data?.name}');
      print('User age: ${result.data?.age}');
    } else {
      print('Request failed: ${result.message}');
    }
  } catch (e) {
    print('Error: $e');
  }

如果T是基础类型

MyBaseModel<int> model = MyBaseModel.fromJson(
    jsonMap,
    (json) => json as int, // 直接将 JSON 值转换为 int
  );

6、Interceptor(拦截器)

Interceptordio 库中的一个抽象类,它允许你在请求发送前、响应返回后以及请求发生错误时插入自定义逻辑。通过实现 Interceptor 类的方法,你可以对请求和响应进行拦截和修改。

Interceptor 类有三个主要的方法,分别用于处理请求、响应和错误:

onRequest

作用:在请求发送之前被调用,可用于修改请求选项,如添加请求头、修改请求参数等。

class AuthInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // 添加授权头
    options.headers['Authorization'] = 'Bearer your_token';
    handler.next(options);
  }
}
onResponse

作用:在响应返回之后被调用,可用于处理响应数据,如解析数据、缓存数据等。

class DataParserInterceptor extends Interceptor {
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    // 解析响应数据
    if (response.data is Map) {
      // 处理 Map 类型的数据
    }
    handler.next(response);
  }
}
onError

作用:在请求发生错误时被调用,可用于统一处理错误,如重试请求、显示错误信息等。

class RetryInterceptor extends Interceptor {
  int maxRetries = 3;

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    int retryCount = 0;
    while (retryCount < maxRetries) {
      try {
        // 重试请求
        Response response = await err.requestOptions.createDio().fetch(err.requestOptions);
        handler.resolve(response);
        return;
      } catch (e) {
        retryCount++;
      }
    }
    handler.next(err);
  }
}
拦截器的执行顺序

dio 中的拦截器是按照添加的顺序依次执行的。在请求阶段,拦截器按照添加顺序依次处理请求;在响应阶段,拦截器按照相反的顺序依次处理响应。例如:

dio.interceptors.add(Interceptor1());
dio.interceptors.add(Interceptor2());

请求处理顺序:Interceptor1 -> Interceptor2 响应处理顺序:Interceptor2 -> Interceptor1

1、缓存拦截器

缓存策略

根据以往的业务需求,我定义了下面缓存策略

/// 缓存策略
enum MyNetworkCachePolicy {
  /// 不用缓存
  none,
  /// 先用缓存,在请求网络,得到网络数据后覆盖缓存
  firstCache,
  /// 先请求网络,失败后再返回缓存
  firstRequest,
}

缓存管理类

缓存管理类主要任务

  • 1、定义缓存策略
  • 2、定义缓存时间
  • 3、实现保存和删除操作
class MyNetworkCacheManager {
  /// 缓存策略
  final MyNetworkCachePolicy cachePolicy = MyNetworkCachePolicy.none;
  /// 缓存过期时间(单位:秒)
  final int cacheExpirationTime = 24 * 60 * 60;

  /// 获取缓存
  Future<String?> getCacheData(RequestOptions options) async {
    final filePath = _getFilePath(options);
    final fileUtils = FileUtils();
    String? jsonString = await fileUtils.getFile(filePath);

    if (jsonString != null) {
      Map<String, dynamic> jsonMap = jsonString.toMap();
      int timestamp = jsonMap['timestamp'];
      // 检查缓存是否过期
      if (DateTime.now().millisecondsSinceEpoch - timestamp < cacheExpirationTime * 1000) {
        return jsonMap['data'];
      }
      // 若缓存过期,删除缓存
      await _remove(options);
      return null;
    }
    return null;
  }

  /// 保存缓存
  Future<void> saveCache(RequestOptions options, String data) async {
    final filePath = _getFilePath(options);
    final fileUtils = FileUtils();
    Map<String, dynamic> cachedData = {
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'data': data
    };
    final jsonString = json.encode(cachedData);
    fileUtils.writeFile(filePath, jsonString);
  }


  Future<void> _remove(RequestOptions options) async{
    final filePath = _getFilePath(options);
    final fileUtils = FileUtils();
    fileUtils.removeFilePath(filePath);
  }

  /// 获取文件路径
  String _getFilePath(RequestOptions options) {
    String url = options.uri.toString();
    String paramJsonString = options.queryParameters.toString();
    String method = options.method;
    return (method + url + paramJsonString).md5Hash();
  }
}

缓存拦截器

onRequest
  • 1、firstCache缓存策略下,如果我们能够拿到缓存可以直接执行handler.resolve(response);
  • 2、对于其他策略,继续原始请请求网络handler.next(options);
@override
  void onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {
    final MyNetworkCachePolicy cachePolicy = cacheManager.cachePolicy;
    if (cachePolicy == MyNetworkCachePolicy.firstCache) {
      final cacheJsonString = await cacheManager.getCacheData(options);
      if (cacheJsonString != null) {
        // 有缓存数据,先返回缓存响应
        final response = Response(
          requestOptions: options,
          data: json.decode(cacheJsonString),
          statusCode: 200,
        );
        handler.resolve(response);
        return;
      }
    }

    // 继续请求网络
    handler.next(options);
  }
onResponse

只缓存GET请求成功响应以及缓存策略是none

@override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    // 只缓存GET请求成功响应
    if (response.requestOptions.method == 'GET' &&
        response.statusCode == 200 &&
        cacheManager.cachePolicy != MyNetworkCachePolicy.none) {
      try {
        String data = json.encode(response.data);
        cacheManager.saveCache(response.requestOptions, data);
      } catch (e) {}
    }

    handler.next(response);
  }

onError

@override
  Future<void> onError(
      DioException err, ErrorInterceptorHandler handler) async {
    if (cacheManager.cachePolicy == MyNetworkCachePolicy.firstRequest) {
      final cacheJsonString =
          await cacheManager.getCacheData(err.requestOptions);
      if (cacheJsonString != null) {
        // 有缓存数据,先返回缓存响应
        final response = Response(
          requestOptions: err.requestOptions,
          data: json.decode(cacheJsonString),
          statusCode: 200,
        );
        // 返回正确的响应
        return handler.resolve(response);
      }
    }

    // 继续传递错误
    handler.next(err);
  }

2、token续租拦截器

根据以往业务需求,我希望token续租拦截器有能够实现一下功能

  • 1、如果同时发起多个网络请求,当某个请求判读token不存在或者过期时,不要继续发起其他请求了,而是等待token续租成功以后,在发起刚才没有发起的请求
  • 2、实现无感刷新token

基于以上要求,我可以借助QueuedInterceptorsWrapper拦截器帮我们实现相关功能

QueuedInterceptorsWrapper

QueuedInterceptorsWrapperdio 库中的一个拦截器包装器。

QueuedInterceptorsWrapper 的主要作用是确保多个拦截器按照队列的顺序依次执行。它会将多个拦截器的处理逻辑包装起来,使得每个拦截器的处理逻辑依次执行,并且可以处理异步操作。

实现原理

1、初始化

QueuedInterceptorsWrapper 会接收多个拦截器作为参数,并将它们存储在一个列表中。

import 'package:dio/dio.dart';

class QueuedInterceptorsWrapper extends InterceptorsWrapper {
  final List<Interceptor> _interceptors;

  QueuedInterceptorsWrapper({required List<Interceptor> interceptors})
      : _interceptors = interceptors;

  // 其他方法...
}
2、请求拦截

onRequest 方法中,QueuedInterceptorsWrapper 会依次调用每个拦截器的 onRequest 方法。如果某个拦截器返回 RequestOptions 或者 Response,则会终止后续拦截器的执行。

@override
Future<void> onRequest(
    RequestOptions options, RequestInterceptorHandler handler) async {
  for (var interceptor in _interceptors) {
    var shouldContinue = await _handleInterceptor(
      interceptor.onRequest,
      options,
      (newOptions) {
        if (newOptions is RequestOptions) {
          options = newOptions;
        } else if (newOptions is Response) {
          handler.resolve(newOptions);
          return false;
        }
        return true;
      },
    );
    if (!shouldContinue) {
      return;
    }
  }
  handler.next(options);
}

Future<bool> _handleInterceptor(
    FutureOr<dynamic> Function(RequestOptions, RequestInterceptorHandler)
        interceptorFunction,
    RequestOptions options,
    bool Function(dynamic) resultHandler) async {
  try {
    var result = await interceptorFunction(options, RequestInterceptorHandler());
    return resultHandler(result);
  } catch (e) {
    return false;
  }
}
3、 响应拦截

onResponse 方法中,QueuedInterceptorsWrapper 会依次调用每个拦截器的 onResponse 方法。

@override
Future<void> onResponse(
    Response response, ResponseInterceptorHandler handler) async {
  for (var interceptor in _interceptors) {
    var shouldContinue = await _handleInterceptor(
      (options, _) => interceptor.onResponse(response, ResponseInterceptorHandler()),
      response.requestOptions,
      (newResponse) {
        if (newResponse is Response) {
          response = newResponse;
        }
        return true;
      },
    );
    if (!shouldContinue) {
      return;
    }
  }
  handler.next(response);
}
4、错误拦截

onError 方法中,QueuedInterceptorsWrapper 会依次调用每个拦截器的 onError 方法。

@override
Future<void> onError(DioError err, ErrorInterceptorHandler handler) async {
  for (var interceptor in _interceptors) {
    var shouldContinue = await _handleInterceptor(
      (options, _) => interceptor.onError(err, ErrorInterceptorHandler()),
      err.requestOptions,
      (newError) {
        if (newError is DioError) {
          err = newError;
        }
        return true;
      },
    );
    if (!shouldContinue) {
      return;
    }
  }
  handler.next(err);
}

token续租拦截器

class CsrfTokenInterceptor extends QueuedInterceptor {
  String? _csrfToken;
  bool _isFetchingToken = false;

  @override
  Future<void> onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) async {
    // 1. 检查是否需要添加 CSRF Token(根据实际需求调整条件)
    if (options.path.startsWith('/secure/')) {
      // 2. 如果没有 token 且不在获取中
      if (_csrfToken == null && !_isFetchingToken) {
        _isFetchingToken = true;
        
        try {
          // 3. 获取新 token
          final newToken = await _fetchCsrfToken();
          _csrfToken = newToken;
          _isFetchingToken = false;
          
          // 4. 更新当前请求的 header
          options.headers['X-CSRF-TOKEN'] = newToken;
          
          // 5. 放行当前请求
          handler.next(options);
        } catch (e) {
          // 6. 获取 token 失败,终止请求链
          _isFetchingToken = false;
          handler.reject(DioException(
            requestOptions: options,
            error: 'Failed to get CSRF token: $e',
          ));
        }
      } 
      // 7. 如果 token 正在获取中,等待直到获取完成
      else if (_isFetchingToken) {
        // 延迟重试逻辑
        Future.delayed(Duration(milliseconds: 100), () {
          onRequest(options, handler);
        });
      }
      // 8. 已有 token 直接添加
      else {
        options.headers['X-CSRF-TOKEN'] = _csrfToken;
        handler.next(options);
      }
    } else {
      // 不需要 CSRF token 的请求直接放行
      handler.next(options);
    }
  }

  Future<String> _fetchCsrfToken() async {
    print('开始获取 CSRF Token...');
    // 模拟网络请求延迟
    await Future.delayed(Duration(seconds: 1));
    
    // 模拟获取 token(实际应该发送真实请求)
    final mockToken = 'csrf_token_${DateTime.now().millisecondsSinceEpoch}';
    print('获取到 CSRF Token: $mockToken');
    
    return mockToken;
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    // 401 状态码时清除 token(示例逻辑)
    if (err.response?.statusCode == 401) {
      _csrfToken = null;
      print('CSRF Token 已失效,已清除');
    }
    handler.next(err);
  }
}
关键点解释
  1. 串行队列处理
    • 继承 QueuedInterceptor 确保所有请求按顺序进入拦截器
    • 即使并发发起多个请求,拦截器也会逐个处理
  2. Token 获取锁
    • _isFetchingToken 标志位防止重复请求
    • 第一个请求触发 token 获取后,后续请求进入等待状态
  3. 自动重试机制
    • 当检测到正在获取 token 时(第7步),使用延迟递归调用实现自动重试
    • 100ms 重试间隔避免立即重试造成的性能问题
  4. 错误处理
    • onError 中处理 401 未授权情况,自动清除失效 token
    • 获取 token 失败时会终止当前请求链
  5. 条件判断
    • 根据请求路径决定是否需要添加 CSRF Token
    • 可根据实际需求扩展判断逻辑(如根据请求方法等)

3、loading拦截器

loading拦截器实现比较简单:就一个参数是否显示loading

class LoadingInterceptor extends Interceptor {
  /// 是否显示loading
  final bool isShowLoading;
  LoadingInterceptor({required this.isShowLoading});

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // 在请求发起时显示加载提示
    if (isShowLoading) {
      _showLoading();
    }
    super.onRequest(options, handler);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    // 在请求出错时隐藏加载提示
    if (isShowLoading) {
      _hideLoading();
    }
    super.onError(err, handler);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    // 在请求成功响应后隐藏加载提示
    if (isShowLoading) {
      _hideLoading();
    }

    super.onResponse(response, handler);
  }

  /// 弹窗
  void _showLoading() {
    ToastUtil.showLoading();
  }

  /// 隐藏弹窗
  void _hideLoading() {
    ToastUtil.dismiss();
  }
}

4、异常处理拦截器

Map<String, String> _errorCodeMessage = {
  "400": "状态码:400 请求参数错误",
  "401": "状态码:401 身份验证错误",
  "403": "状态码:403 服务器拒绝请求",
  "404": "状态码:404 找不到服务器地址",
  "407": "状态码:407 需要代理授权",
  "408": "状态码:408 请求超时",
  "500": "状态码:500 服务器内部错误",
  "501": "状态码:501 尚未实施",
  "502": "状态码:502 错误网关",
  "503": "状态码:503 服务不可用",
  "504": "状态码:504 网关超时",
  "505": "HTTP 版本不受支持",
  "-1000": "解析不到数据"
};

/*
 * 特殊状态code处理的拦截器,
 * 401 弹出弹窗提示用户重新登录
 */
class ErrorHandleInterceptor extends Interceptor {
  /// 是否显示http网络请求错误
  final bool isShowHttpErrorMsg;
  /// 响应code不为0异常
  final bool isShowDataErrorMsg;

  ErrorHandleInterceptor(
      {required this.isShowHttpErrorMsg, required this.isShowDataErrorMsg});

  @override
  void onError(DioError error, ErrorInterceptorHandler handler) {
    // 自定义错误处理逻辑
    if (isShowHttpErrorMsg) {
      _handleHttpError(error);
    }
    handler.next(error);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    if (isShowDataErrorMsg) {
      _handleDataError(response);
    }
    super.onResponse(response, handler);
  }

  /// 网络异常
  void _handleHttpError(DioError error) {
    String errorMsg = "网络异常";
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
        errorMsg = '连接超时';
      case DioExceptionType.sendTimeout:
        errorMsg = '发送超时';
      case DioExceptionType.receiveTimeout:
        errorMsg = '接受超时';
      case DioExceptionType.badCertificate:
        errorMsg = '无效证书';
      case DioExceptionType.badResponse:
        errorMsg = '无效响应';
      case DioExceptionType.cancel:
        errorMsg = '请求取消';
      case DioExceptionType.connectionError:
        errorMsg = '链接错误';
      case DioExceptionType.unknown:
        errorMsg = '未知错误';
    }

    int? code = error.response?.statusCode;
    if (code != null) {
      String codeString = code.toString();
      errorMsg = _errorCodeMessage[codeString] ?? "网络异常";
    }

    if (isShowHttpErrorMsg) {
      if (kDebugMode) {
        errorMsg = "网络异常";
      }
      ToastUtil.showToast(msg: errorMsg);
    }
  }

  /// 网络异常
  void _handleDataError(Response response) {
    Map<String, dynamic> _data = {};
    if (response.data is Map) {
      _data = response.data as Map<String, dynamic>;
    } else if (response.data is String) {
      _data = response.data.toMap();
    }
    final num? code = JsonTypeAdapter.safeParseNumber(_data['code']);
    // 检查 code 是否为 0
    if (code != null && code.toInt() != 0) {
      final String message = _data['message'] as String? ?? '未知错误';
      ToastUtil.showToast(msg: message);
    }
  }
}

5、日志拦截器

主要打印整个请求过程中的各个参数&状态&耗时

  • 1、请求方式: ${options.method}
  • 2、请求URL: ${options.uri}
  • 3、请求Headers: ${options.headers}
  • 4、网络请求耗时:${entTime - startTime} ms
  • 5、DNS: ${timings['dns']}ms
  • 6、TCP: ${timings['tcp']}ms
  • 7、SSL: ${timings['ssl']}ms
  • 8、首包: ${timings['first_packet']}ms
  • 9、响应状态码: ${response.statusCode}
  • 10、响应头:${response.headers}
  • 11、响应: ${response.data}
class CustomLogInterceptor extends Interceptor {
  // 用于存储每个请求的开始时间
  Map<RequestOptions, int> requestStartTimeMap = {};

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // 记录请求开始时间
    requestStartTimeMap[options] = MyDateTimeUtil.getTimeStamp();
    handler.next(options);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    // 记录响应日志
    _logResponse(response);
    handler.next(response);
  }

  @override
  void onError(DioError error, ErrorInterceptorHandler handler) {
    // 记录错误日志
    _logError(error);
    handler.next(error);
  }

  // 记录响应日志
  void _logResponse(Response response) {
    // 获取请求开始时间
    int startTime = requestStartTimeMap[response.requestOptions] ?? 0;
    // 当前时间戳
    int entTime = MyDateTimeUtil.getTimeStamp();
    // 从 extra 中读取耗时指标
    final timings = response.requestOptions.extra as Map<String, dynamic>;
    final options = response.requestOptions;
    Log.error('''
 
      请求方式: ${options.method}
      请求URL: ${options.uri}
      请求Headers: ${options.headers}
      网络请求耗时:${entTime - startTime} ms
      DNS: ${timings['dns']}ms
      TCP: ${timings['tcp']}ms
      SSL: ${timings['ssl']}ms 
      首包: ${timings['first_packet']}ms
      响应状态码: ${response.statusCode}
      响应头:${response.headers}
      响应: ${response.data}
    ''');
  }

  // 记录错误日志
  void _logError(DioError error) {
    // 获取请求开始时间
    int startTime = requestStartTimeMap[error.requestOptions] ?? 0;
    // 当前时间戳
    int entTime = MyDateTimeUtil.getTimeStamp();
    // 从 extra 中读取耗时指标
    final timings =
        error.response?.requestOptions.extra as Map<String, dynamic>;
    final options = error.requestOptions;
    Log.error('''
    网络请求错误:
      请求方式: ${options.method}
      请求URL: ${options.uri}
      请求Headers: ${options.headers}
      网络请求耗时:${entTime - startTime} 毫秒
      DNS: ${timings['dns']}ms
      TCP: ${timings['tcp']}ms
      SSL: ${timings['ssl']}ms 
      首包: ${timings['first_packet']}ms
      ${error.toString()}
    ''');
  }
}

6、数据转换拦截器

数据转换拦截器将请求或响应的数据在发送或接收时进行转换,例如将 JSON 数据转换为自定义的数据模型,或者对数据进行加密 / 解密。可以确保数据的格式和安全性符合应用的要求

class DataTransformInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    if (options.data != null && options.data is Map<String, dynamic>) {
      options.data = jsonEncode(options.data);
    }
    handler.next(options);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    if (response.data is String) {
      response.data = jsonDecode(response.data);
    }
    handler.next(response);
  }
}