目录
前言
在 Flutter 的第三方网络请求库中,Dio的使用人数应该算是使用最多的,而网络请求过程中有很多额外的事情要做,比如 Request 前需要授权信息,发送网络请求前需要带上令牌(token);请求的日志记录,方便在测试的同学可以在不用抓包的情况下直接看到完整的请求信息;拿到 Response 进行数据解析、业务错误码判断,拿到正确的业务数据后进行 JSON 转 Model 等等。
添加或者授权的Token
在 Request Header 中添加授权的 Token,一般情况下登录成功后会将 Token 存入本地,退出登录时将 Token 清除掉。
class AuthInterceptor extends Interceptor {
@override
onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// 从本地缓存中取出 Token
String accessToken = SaveDataTool().getToken();
if (accessToken != null &&
accessToken.isNotEmpty) {
options.headers['Authorization'] = "Token mobile:$accessToken";
}
return super.onRequest(options, handler);
}
}
如果 Token 过期了,一般会返回401的错误码,此时需要重新获取一下Token,再缓存到本地。
var dio = Dio();
var tokenDio = Dio();
String? csrfToken;
dio.options.baseUrl = 'xxx.example.com';
tokenDio.options = dio.options;
dio.interceptors.add(QueuedInterceptorsWrapper(
onRequest: (options, handler) {
if (csrfToken == null) {
// 首次获取 csrfToken
tokenDio.get('/token').then((d) {
// 这里模拟拿到最新的 csrfToken,并加到 Request headers 中
options.headers['csrfToken'] = csrfToken = d.data['token'];
handler.next(options);
}).catchError((error, stackTrace) {
handler.reject(error, true);
});
} else {
options.headers['csrfToken'] = csrfToken;
return handler.next(options);
}
},
onError: (error, handler) {
if (error.response?.statusCode == 401) {
var options = error.response!.requestOptions;
// 如果是401且当前不是最新的csrfToken,那就更新一下 csrfToken
if (csrfToken != options.headers['csrfToken']) {
options.headers['csrfToken'] = csrfToken;
//再发送请求
dio.fetch(options).then(
(r) => handler.resolve(r),
onError: (e) {
handler.reject(e);
},
);
return;
}
tokenDio.get('/token').then((d) {
// 更新到最新的 csrfToken,并加到 Request headers 中
options.headers['csrfToken'] = csrfToken = d.data['token'];
}).then((e) {
//再发送请求
dio.fetch(options).then(
(r) => handler.resolve(r),
onError: (e) {
handler.reject(e);
},
);
});
return;
}
return handler.next(error);
},
));
记录完整的请求日志信息
有时候需要看一下完整的请求日志信息,也自己来定制一套。
class LoggingInterceptor extends Interceptor {
DebugModel debugModel;
@override
onRequest(RequestOptions options, RequestInterceptorHandler handler) {
debugModel = DebugModel();
debugModel.startTime = DateTime.now();
debugModel.headers = options.headers.toString();
debugModel.requestMethod = options.method;
debugModel.headers = options.headers.toString();
if (options.queryParameters.isEmpty) {
debugModel.url = options.baseUrl + options.path;
} else {
String re = options.baseUrl +
options.path +
"?" +
Transformer.urlEncodeMap(options.queryParameters);
debugModel.url = re;
}
debugModel.params = options.data != null ? options.data.toString() : null;
return super.onRequest(options, handler);
}
@override
onResponse(Response response, ResponseInterceptorHandler handler) {
debugModel.endTime = DateTime.now();
int duration = endTime.difference(startTime).inMilliseconds;
if (response != null) {
debugModel.statusCode = response.statusCode;
if (response.data != null) {
debugModel.responseString = response.data.toString();
}
}
// DebugUtil 是全局的请求日志管理工具类,每一次的网络请求都会添加到其logs这个数组中
// 这里只是简单做了一个去重的处理,日常开发中不建议这样判断
if (!DebugUtil.instance.logs.contains(debugModel)) {
DebugUtil.instance.addLog(debugModel);
}
return super.onResponse(response, handler);
}
@override
onError(DioError err, ErrorInterceptorHandler handler) {
debugModel.error = err.toString();
if (!DebugUtil.instance.logs.contains(debugModel)) {
DebugUtil.instance.addLog(debugModel);
}
return super.onError(err, handler);
}
}
保存数据的 DebugModel 和工具类 DebugUtil 的实现。
class DebugModel {
String url = "";
String headers = "";
String requestMethod = "";
int statusCode = 0;
String params = "";
List<Map<String, dynamic>> responseList = [];
Map<String, dynamic> responseMap = {};
String responseString = "";
DateTime startTime = DateTime.now();
DateTime endTime = DateTime.now();
String error = "";
DebugModel({this.url, this.params, this.responseList, this.responseMap});
}
class DebugUtil {
factory DebugUtil() => _getInstance();
static DebugUtil get instance => _getInstance();
static DebugUtil _instance;
DebugUtil._internal();
static DebugUtil _getInstance() {
if (_instance == null) {
_instance = DebugUtil._internal();
}
return _instance;
}
List<DebugModel> logs = [];
void addLog(DebugModel log){
if (logs.length > 10) {
logs.removeLast();
}
logs.insert(0, log);
}
void clearLogs(){
logs = [];
}
}
统一处理后端返回的数据格式
后端返回的数据格式统一处理,也可以通过Interceptor来实现。日常开发过程中,如果前期没有对Response数据做统一的适配,在业务层做解析判断,而后端API接口返回的JSON数据不够规范,又或者随着业务的迭代,API返回JSON数据的结构有了比较大的调整,客户端需要修改所有涉及到网络请求的地方,增加了不少工作量。
{"code":0,"data":{},"message":""}
上面是我们希望的数据格式,而如果后端接口不规范的话,可能是这样的:
{"id":"","tree_id":0,"level":1,"parent":""}
或者直接是这样的:
{"result":{"id":"","tree_id":0,"level":1,"parent":""}}
而在业务查询错误时,返回的又是这样的:
{"detail": "xxxxxx", "err_code": 10004}
这种情况下,最好的办法是直接和后端的开发人员沟通协调,让其做到接口数据规范化,从源头上掐掉将来会出现的隐患,而且API接口如果多端使用,比如有Web、App和小程序都有用到,API做到规范统一能节省很多工作量,减少出错的概率。还有一种情况就是随着业务的迭代,API返回JSON数据的结构有了调整,这时就需要我们可以在拦截器里面做处理。
class AdapterInterceptor extends Interceptor {
static const String msg = "msg";
static const String detail = "detail";
static const String non_field_errors = "non_field_errors";
static const String defaultText = "\"无返回信息\"";
static const String notFound = "未找到查询信息";
static const String failureFormat = "{\"code\":%d,\"message\":\"%s\"}";
static const String successFormat =
"{\"code\":0,\"data\":%s,\"message\":\"\"}";
@override
onResponse(Response response, ResponseInterceptorHandler handler) {
return super.onResponse(adapterData(response), handler);
}
@override
onError(DioError err, ErrorInterceptorHandler handler) {
if (err.response != null) {
adapterData(err.response);
}
return super.onError(err, handler);
}
Response adapterData(Response response) {
String result;
String content = response.data == null ? "" : response.data.toString();
/// 成功时,直接格式化返回
if (response.statusCode == 200) {
if (content == null || content.isEmpty) {
content = defaultText;
}
// 这里还需要对业务错误码的判断
// .......
// 这里的 sprintf 用的是第三方插件 sprintf: ^6.0.0 来格式化字符串
result = sprintf(successFormat, [content]);
response.statusCode = ExceptionHandle.success;
} else { // 这里一般的是网络错误逻辑
if (response.statusCode == 404) {
/// 错误数据格式化后,按照成功数据返回
result = sprintf(failureFormat, [response.statusCode, notFound]);
} else {
// 解析异常直接按照返回原数据处理(一般为返回500,503 HTML页面代码)
result = sprintf(failureFormat,[response.statusCode, "服务器异常(${response.statusCode})"]);
}
}
response.data = result;
return response;
}
}
需要特别提醒的是业务错误码和网络错误码的区别,业务错误码是网络请求是正常的,但数据查询出错,通常是后端自己定义的;而网络错误码就是网络请求报错,如401、404等。客户端需要根据不同的错误码给用户提示或者其它的处理。
小结
这些是我在开发中对于网络请求需要经常处理的地方,如有不严谨的地方,欢迎指正,或者发表自己的看法。