实现功能
- 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
(拦截器)
Interceptor
是 dio
库中的一个抽象类,它允许你在请求发送前、响应返回后以及请求发生错误时插入自定义逻辑。通过实现 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
QueuedInterceptorsWrapper
是 dio
库中的一个拦截器包装器。
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);
}
}
关键点解释
- 串行队列处理:
- 继承
QueuedInterceptor
确保所有请求按顺序进入拦截器 - 即使并发发起多个请求,拦截器也会逐个处理
- 继承
- Token 获取锁:
_isFetchingToken
标志位防止重复请求- 第一个请求触发 token 获取后,后续请求进入等待状态
- 自动重试机制:
- 当检测到正在获取 token 时(第7步),使用延迟递归调用实现自动重试
- 100ms 重试间隔避免立即重试造成的性能问题
- 错误处理:
- 在
onError
中处理 401 未授权情况,自动清除失效 token - 获取 token 失败时会终止当前请求链
- 在
- 条件判断:
- 根据请求路径决定是否需要添加 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);
}
}