許多掘金朋友在上一篇留言,說要封裝下最新版倔监,所以這篇把封裝思路寫下,大家可以自己封裝菌仁。有好的想法也可以去github提request浩习,也感謝WingCH的貢獻(xiàn)
分析需求
為什么要封裝?
全局token驗(yàn)證
自定義攔截器
緩存處理
統(tǒng)一封裝業(yè)務(wù)錯誤邏輯
代理配置
重試機(jī)制
log輸出
自定義解析济丘,數(shù)據(jù)脫殼
要初始化哪些配置谱秽?
- 域名
- 代理地址
- cookie本地緩存地址
- 超時時間
- 自定義攔截器
定義一個配置信息類去初始化這些配置:
// dio 配置項(xiàng)
class HttpConfig {
final String? baseUrl;
final String? proxy;
final String? cookiesPath;
final List<Interceptor>? interceptors;
final int connectTimeout;
final int sendTimeout;
final int receiveTimeout;
HttpConfig({
this.baseUrl,
this.proxy,
this.cookiesPath,
this.interceptors,
this.connectTimeout = Duration.millisecondsPerMinute,
this.sendTimeout = Duration.millisecondsPerMinute,
this.receiveTimeout = Duration.millisecondsPerMinute,
});
// static DioConfig of() => Get.find<DioConfig>();
}
請求差異化有哪些配置?
-
解析策略
許多公司接口規(guī)范經(jīng)歷過變更摹迷,有多個返回類型疟赊,那么就需要針對不同的數(shù)據(jù)類型,做不同的解析峡碉。
比如舊版本:
// 舊版本 { "code": 1, "data": {}, "state": true } // 新版本 { "code": 1, "data": { "data": {}, "hasmore":false }, "message": “success” }
要做到脫殼近哟,拿到解析后的
data
,就需要兩種解析策略鲫寄。所以需要根據(jù)不同接口動態(tài)配置解析策略吉执。 path
參數(shù)
cancelToken
-
dio 的常用參數(shù)
Dio 的請求參數(shù)已經(jīng)很全面的包括了分析出的配置參數(shù),只需要另添加一個解析策略即可地来。
遵守 SOLID 原則定義一個抽象解析策略:
/// Response 解析 abstract class HttpTransformer { HttpResponse parse(Response response); }
根據(jù)實(shí)際需求默認(rèn)實(shí)現(xiàn):
class DefaultHttpTransformer extends HttpTransformer { // 假設(shè)接口返回類型 // { // "code": 100, // "data": {}, // "message": "success" // } @override HttpResponse parse(Response response) { // if (response.data["code"] == 100) { // return HttpResponse.success(response.data["data"]); // } else { // return HttpResponse.failure(errorMsg:response.data["message"],errorCode: response.data["code"]); // } return HttpResponse.success(response.data["data"]); } /// 單例對象 static DefaultHttpTransformer _instance = DefaultHttpTransformer._internal(); /// 內(nèi)部構(gòu)造方法戳玫,可避免外部暴露構(gòu)造函數(shù),進(jìn)行實(shí)例化 DefaultHttpTransformer._internal(); /// 工廠構(gòu)造方法未斑,這里使用命名構(gòu)造函數(shù)方式進(jìn)行聲明 factory DefaultHttpTransformer.getInstance() => _instance; }
單例模式是為了避免多次創(chuàng)建實(shí)例咕宿。方便下一步使用。
異常處理
異常大體分為以下幾種:
- 網(wǎng)絡(luò)異常
- 客戶端請求異常
- 服務(wù)端異常
客戶端異常又可拆分兩種常見的異常:請求參數(shù)或路徑錯誤,鑒權(quán)失敗/token
失效
異常歸檔后創(chuàng)建異常:
class HttpException implements Exception {
final String? _message;
String get message => _message ?? this.runtimeType.toString();
final int? _code;
int get code => _code ?? -1;
HttpException([this._message, this._code]);
String toString() {
return "code:$code--message=$message";
}
}
/// 客戶端請求錯誤
class BadRequestException extends HttpException {
BadRequestException({String? message, int? code}) : super(message, code);
}
/// 服務(wù)端響應(yīng)錯誤
class BadServiceException extends HttpException {
BadServiceException({String? message, int? code}) : super(message, code);
}
class UnknownException extends HttpException {
UnknownException([String? message]) : super(message);
}
class CancelException extends HttpException {
CancelException([String? message]) : super(message);
}
class NetworkException extends HttpException {
NetworkException({String? message, int? code}) : super(message, code);
}
/// 401
class UnauthorisedException extends HttpException {
UnauthorisedException({String? message, int? code = 401}) : super(message);
}
class BadResponseException extends HttpException {
dynamic? data;
BadResponseException([this.data]) : super();
}
返回數(shù)據(jù)類型
返回的數(shù)據(jù)類型府阀,需要有成功或是失敗的標(biāo)識缆镣,還需要脫殼后的數(shù)據(jù),如果失敗了试浙,也需要失敗的信息费就,定義幾個工廠方法方便創(chuàng)建實(shí)例:
class HttpResponse {
late bool ok;
dynamic? data;
HttpException? error;
HttpResponse._internal({this.ok = false});
HttpResponse.success(this.data) {
this.ok = true;
}
HttpResponse.failure({String? errorMsg, int? errorCode}) {
this.error = BadRequestException(message: errorMsg, code: errorCode);
this.ok = false;
}
HttpResponse.failureFormResponse({dynamic? data}) {
this.error = BadResponseException(data);
this.ok = false;
}
HttpResponse.failureFromError([HttpException? error]) {
this.error = error ?? UnknownException();
this.ok = false;
}
}
開始封裝
配置 Dio
Dio 配置組裝,需要我們定義一個初始化類川队,用于把請求的初始化配置添加進(jìn)去力细。一般可以定義一個單例類,init
方法里去初始化一個 Dio 固额,也可以采用實(shí)現(xiàn) Dio 的方式:
class AppDio with DioMixin implements Dio {
AppDio({BaseOptions? options, HttpConfig? dioConfig}) {
options ??= BaseOptions(
baseUrl: dioConfig?.baseUrl ?? "",
contentType: 'application/json',
connectTimeout: dioConfig?.connectTimeout,
sendTimeout: dioConfig?.sendTimeout,
receiveTimeout: dioConfig?.receiveTimeout,
);
this.options = options;
// DioCacheManager
final cacheOptions = CacheOptions(
// A default store is required for interceptor.
store: MemCacheStore(),
// Optional. Returns a cached response on error but for statuses 401 & 403.
hitCacheOnErrorExcept: [401, 403],
// Optional. Overrides any HTTP directive to delete entry past this duration.
maxStale: const Duration(days: 7),
);
interceptors.add(DioCacheInterceptor(options: cacheOptions));
// Cookie管理
if (dioConfig?.cookiesPath?.isNotEmpty ?? false) {
interceptors.add(CookieManager(
PersistCookieJar(storage: FileStorage(dioConfig!.cookiesPath))));
}
if (kDebugMode) {
interceptors.add(LogInterceptor(
responseBody: true,
error: true,
requestHeader: false,
responseHeader: false,
request: false,
requestBody: true));
}
if (dioConfig?.interceptors?.isNotEmpty ?? false) {
interceptors.addAll(interceptors);
}
httpClientAdapter = DefaultHttpClientAdapter();
if (dioConfig?.proxy?.isNotEmpty ?? false) {
setProxy(dioConfig!.proxy!);
}
}
setProxy(String proxy) {
(httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) {
// config the http client
client.findProxy = (uri) {
// proxy all request to localhost:8888
return "PROXY $proxy";
};
// you can also create a HttpClient to dio
// return HttpClient();
};
}
}
Restful請求
采用 Restful 標(biāo)準(zhǔn)眠蚂,創(chuàng)建對應(yīng)的請求方法:
class HttpClient {
late AppDio _dio;
HttpClient({BaseOptions? options, HttpConfig? dioConfig})
: _dio = AppDio(options: options, dioConfig: dioConfig);
Future<HttpResponse> get(String uri,
{Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
HttpTransformer? httpTransformer}) async {
try {
var response = await _dio.get(
uri,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
);
return handleResponse(response, httpTransformer: httpTransformer);
} on Exception catch (e) {
return handleException(e);
}
}
Future<HttpResponse> post(String uri,
{data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
HttpTransformer? httpTransformer}) async {
try {
var response = await _dio.post(
uri,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
return handleResponse(response, httpTransformer: httpTransformer);
} on Exception catch (e) {
return handleException(e);
}
}
Future<HttpResponse> patch(String uri,
{data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
HttpTransformer? httpTransformer}) async {
try {
var response = await _dio.patch(
uri,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
return handleResponse(response, httpTransformer: httpTransformer);
} on Exception catch (e) {
return handleException(e);
}
}
Future<HttpResponse> delete(String uri,
{data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
HttpTransformer? httpTransformer}) async {
try {
var response = await _dio.delete(
uri,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
return handleResponse(response, httpTransformer: httpTransformer);
} on Exception catch (e) {
return handleException(e);
}
}
Future<HttpResponse> put(String uri,
{data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
HttpTransformer? httpTransformer}) async {
try {
var response = await _dio.put(
uri,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
return handleResponse(response, httpTransformer: httpTransformer);
} on Exception catch (e) {
return handleException(e);
}
}
Future<Response> download(String urlPath, savePath,
{ProgressCallback? onReceiveProgress,
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
bool deleteOnError = true,
String lengthHeader = Headers.contentLengthHeader,
data,
Options? options,
HttpTransformer? httpTransformer}) async {
try {
var response = await _dio.download(
urlPath,
savePath,
onReceiveProgress: onReceiveProgress,
queryParameters: queryParameters,
cancelToken: cancelToken,
deleteOnError: deleteOnError,
lengthHeader: lengthHeader,
data: data,
options: data,
);
return response;
} catch (e) {
throw e;
}
}
}
響應(yīng)解析
得到請求數(shù)據(jù)后,解析為定義的通用返回數(shù)據(jù)類型斗躏,需要首先判斷是否取得返回值逝慧,然后判斷網(wǎng)絡(luò)請求成功,網(wǎng)絡(luò)請求成功之后啄糙,采取判斷是否接口返回期望的數(shù)據(jù)笛臣,還是因?yàn)檎埱髤?shù)錯誤或者服務(wù)器錯誤返回了錯誤信息。如果錯誤了隧饼,把錯誤信息格式化為定義的異常:
HttpResponse handleResponse(Response? response,
{HttpTransformer? httpTransformer}) {
httpTransformer ??= DefaultHttpTransformer.getInstance();
// 返回值異常
if (response == null) {
return HttpResponse.failureFromError();
}
// token失效
if (_isTokenTimeout(response.statusCode)) {
return HttpResponse.failureFromError(
UnauthorisedException(message: "沒有權(quán)限", code: response.statusCode));
}
// 接口調(diào)用成功
if (_isRequestSuccess(response.statusCode)) {
return httpTransformer.parse(response);
} else {
// 接口調(diào)用失敗
return HttpResponse.failure(
errorMsg: response.statusMessage, errorCode: response.statusCode);
}
}
HttpResponse handleException(Exception exception) {
var parseException = _parseException(exception);
return HttpResponse.failureFromError(parseException);
}
/// 鑒權(quán)失敗
bool _isTokenTimeout(int? code) {
return code == 401;
}
/// 請求成功
bool _isRequestSuccess(int? statusCode) {
return (statusCode != null && statusCode >= 200 && statusCode < 300);
}
HttpException _parseException(Exception error) {
if (error is DioError) {
switch (error.type) {
case DioErrorType.connectTimeout:
case DioErrorType.receiveTimeout:
case DioErrorType.sendTimeout:
return NetworkException(message: error.error.message);
case DioErrorType.cancel:
return CancelException(error.error.message);
case DioErrorType.response:
try {
int? errCode = error.response?.statusCode;
switch (errCode) {
case 400:
return BadRequestException(message: "請求語法錯誤", code: errCode);
case 401:
return UnauthorisedException(message: "沒有權(quán)限", code: errCode);
case 403:
return BadRequestException(message: "服務(wù)器拒絕執(zhí)行", code: errCode);
case 404:
return BadRequestException(message: "無法連接服務(wù)器", code: errCode);
case 405:
return BadRequestException(message: "請求方法被禁止", code: errCode);
case 500:
return BadServiceException(message: "服務(wù)器內(nèi)部錯誤", code: errCode);
case 502:
return BadServiceException(message: "無效的請求", code: errCode);
case 503:
return BadServiceException(message: "服務(wù)器掛了", code: errCode);
case 505:
return UnauthorisedException(
message: "不支持HTTP協(xié)議請求", code: errCode);
default:
return UnknownException(error.error.message);
}
} on Exception catch (_) {
return UnknownException(error.error.message);
}
case DioErrorType.other:
if (error.error is SocketException) {
return NetworkException(message: error.message);
} else {
return UnknownException(error.message);
}
default:
return UnknownException(error.message);
}
} else {
return UnknownException(error.toString());
}
}
緩存沈堡、重試、401攔截
默認(rèn)的通用攔截器在 AppDio
里直接定義燕雁,如果需要額外配置的攔截器诞丽,從HttpConfig
里傳入。
這些攔截器的創(chuàng)建拐格,可以參考上一篇強(qiáng)大的dio封裝僧免,可能滿足你的一切需要,這里就不再贅述。
使用
第一步捏浊,全局配置并初始化:
HttpConfig dioConfig =
HttpConfig(baseUrl: "https://gank.io/", proxy: "192.168.2.249:8888");
HttpClient client = HttpClient(dioConfig: dioConfig);
Get.put<HttpClient>(client);
請求:
void get() async {
HttpResponse appResponse = await dio.get("api/v2/banners");
if (appResponse.ok) {
debugPrint("====" + appResponse.data.toString());
} else {
debugPrint("====" + appResponse.error.toString());
}
}
附上開發(fā)環(huán)境:
[?] Flutter (Channel stable, 2.0.5, on Mac OS X 10.15.7 19H15 darwin-x64, locale zh-Hans-CN)