------更工程化的"中间件"语义:挂起、统一重放、统一失败、统一登出
共享 Future 方案已经能解决"并发只刷新一次";
队列版适用于:
想在 refresh 期间挂起请求,不立刻把 401 抛给业务层
想 refresh 失败时一锅端所有等待请求(并且只登出一次)
想控制重放节奏:顺序 / 限流 / 去重 / 白名单(中大型项目常见需求)
1)为什么要搞队列版?
当首页并发几十个请求时,accessToken 过期会出现很多 401。
共享 Future 版是:每个 401 各自 await 刷新结果,然后各自 retry。
队列版是:401 先入队挂起,刷新完成后统一重放/统一失败。
队列版的核心价值:
-
刷新期间请求不"立即失败",体验更平滑(尤其下拉刷新、弱网场景)
-
刷新失败时统一失败 + 统一登出,UI 不会多次弹提示/多次跳登录
-
更容易扩展:重放顺序、限流、去重、接口白名单
2)队列版核心思想(中间件语义)
当请求 401:
-
不立刻
handler.next(err) -
把这个请求的
(RequestOptions + handler)存入队列(挂起) -
触发一次 refresh(并发锁保证只刷新一次)
-
refresh 成功:统一重放 队列里所有请求(逐个
resolve回原 caller) -
refresh 失败:统一 reject 所有等待请求,并触发 全局 onAuthExpired(只触发一次)
关键点:队列版必须有"全局登出/跳登录"的收敛点,否则并发失败会导致 UI 混乱。
3)代码实现(更新版:带全局 onAuthExpired)
3.1 TokenStore(示例)
Dart
class TokenPair {
final String accessToken;
final String refreshToken;
TokenPair(this.accessToken, this.refreshToken);
}
class TokenStore {
static TokenPair? _pair;
static TokenPair? get() => _pair;
static Future<void> set(TokenPair pair) async {
_pair = pair;
// TODO: persist to secure storage
}
static Future<void> clear() async {
_pair = null;
}
}
3.2 RefreshManager(共享 Future:保证 refresh 只执行一次)
队列版仍然需要"只刷新一次"的并发锁(不然还是刷新风暴)。
Dart
class RefreshManager {
Future<String?>? _refreshing;
Future<String?> getOrCreate(Future<String?> Function() job) {
_refreshing ??= job().whenComplete(() {
_refreshing = null;
});
return _refreshing!;
}
}
3.3 AuthInterceptor(请求注入 accessToken)
Dart
import 'package:dio/dio.dart';
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final token = TokenStore.get()?.accessToken;
if (token != null && token.isNotEmpty) {
options.headers["Authorization"] = "Bearer $token";
}
handler.next(options);
}
}
3.4 队列元素:保存"挂起请求"
Dart
import 'package:dio/dio.dart';
class _QueuedReq {
final RequestOptions options;
final ErrorInterceptorHandler handler;
_QueuedReq(this.options, this.handler);
}
3.5 QueueRefreshInterceptor(更新版:用 onAuthExpired 回调解耦 UI)
更新点:
拦截器不再直接依赖 EventBus/UI
refresh 失败时只调用
onAuthExpired()(只触发一次)App 层统一登出/跳登录(最干净)
Dart
import 'package:dio/dio.dart';
class QueueRefreshInterceptor extends Interceptor {
final Dio dio; // 主 Dio:用于重放
final Dio cleanDio; // 干净 Dio:用于 refresh(避免递归)
final RefreshManager mgr;
final void Function() onAuthExpired;
bool _expiredFired = false;
final List<_QueuedReq> _queue = [];
QueueRefreshInterceptor({
required this.dio,
required this.cleanDio,
required this.mgr,
required this.onAuthExpired,
});
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode != 401) {
return handler.next(err);
}
final req = err.requestOptions;
// 防死循环:同一请求最多重放一次
if (req.extra["retried"] == true) {
return handler.next(err);
}
final pair = TokenStore.get();
if (pair == null || pair.refreshToken.isEmpty) {
_fireExpiredOnce();
return handler.next(err);
}
// ① 入队 + 挂起(此刻不 next,不 resolve)
_queue.add(_QueuedReq(req, handler));
// ② 触发"只一次"的刷新:成功返回 newAccessToken,失败返回 null
final newAccessToken = await mgr.getOrCreate(() async {
try {
// TODO: 替换为你们真实 refresh 接口
final res = await cleanDio.post(
"/auth/refresh",
data: {"refreshToken": pair.refreshToken},
);
final data = res.data as Map<String, dynamic>;
final access = data["accessToken"] as String;
final refresh = data["refreshToken"] as String;
await TokenStore.set(TokenPair(access, refresh));
return access;
} catch (_) {
await TokenStore.clear();
return null;
}
});
// ③ 多个 onError 会到这里:避免重复 drain
if (_queue.isEmpty) return;
final pending = List<_QueuedReq>.from(_queue);
_queue.clear();
// ④ 刷新失败:统一失败 + 全局登出(只触发一次)
if (newAccessToken == null) {
_fireExpiredOnce();
for (final q in pending) {
q.handler.next(err);
}
return;
}
// ⑤ 刷新成功:统一重放队列(顺序重放最稳)
for (final q in pending) {
try {
final resp = await _replay(q.options, newAccessToken);
q.handler.resolve(resp);
} catch (e) {
q.handler.next(e is DioException ? e : err);
}
}
}
Future<Response<dynamic>> _replay(RequestOptions req, String token) {
return dio.request(
req.path,
data: req.data,
queryParameters: req.queryParameters,
options: Options(
method: req.method,
headers: Map<String, dynamic>.from(req.headers)
..["Authorization"] = "Bearer $token",
extra: Map<String, dynamic>.from(req.extra)..["retried"] = true,
),
cancelToken: req.cancelToken,
onSendProgress: req.onSendProgress,
onReceiveProgress: req.onReceiveProgress,
);
}
void _fireExpiredOnce() {
if (_expiredFired) return;
_expiredFired = true;
onAuthExpired();
}
}
4)接入 DioClient(更新版:注入 onAuthExpired)
Dart
import 'package:dio/dio.dart';
class DioClient {
DioClient._();
static final DioClient instance = DioClient._();
late final Dio dio;
late final Dio cleanDio;
final RefreshManager mgr = RefreshManager();
void init({required void Function() onAuthExpired}) {
dio = Dio(BaseOptions(baseUrl: "https://api.example.com"));
cleanDio = Dio(BaseOptions(baseUrl: "https://api.example.com"));
dio.interceptors.addAll([
AuthInterceptor(),
QueueRefreshInterceptor(
dio: dio,
cleanDio: cleanDio,
mgr: mgr,
onAuthExpired: onAuthExpired,
),
]);
}
}
5)全局 onAuthExpired:统一登出 / 跳登录(App 层只做一次)
队列版最怕:refresh 失败时释放大量等待请求 → UI 多处重复登出。
所以 App 层要"只做一次"。
5.1 NavigatorKey(示例)
final navigatorKey = GlobalKey<NavigatorState>();
MaterialApp:
MaterialApp(
navigatorKey: navigatorKey,
// routes...
);
5.2 Gate:防重复跳转(推荐)
Dart
class AuthExpiredGate {
bool _fired = false;
void fireOnce(void Function() action) {
if (_fired) return;
_fired = true;
action();
}
void reset() => _fired = false; // 登录成功后 reset
}
final authGate = AuthExpiredGate();
5.3 注入 init(闭环)
Dart
DioClient.instance.init(onAuthExpired: () {
authGate.fireOnce(() async {
await TokenStore.clear();
// TODO: 清理用户态 userStore.logout()
navigatorKey.currentState
?.pushNamedAndRemoveUntil('/login', (route) => false);
// TODO: toast("登录过期,请重新登录");
});
});
6)共享 Future 版 vs 队列版怎么选?
-
小中型项目:共享 Future 版足够(实现简单,稳定好维护)
-
中大型项目:队列版更工程化(挂起、统一重放、统一失败、统一登出、便于扩展限流/去重/白名单)
结语:队列版的本质不是 Dio,而是"异步并发控制 + 中间件语义"
-
共享 Future:异步锁(临界区只执行一次)
-
队列:挂起/重放/统一失败(中间件语义更强)
-
onAuthExpired:全局状态一致(只登出一次,UI 不乱)
下一篇: