許多掘金朋友在上一篇留言,說要封裝下最新版,所以這篇把封裝思路寫下,大家可以自己封裝。有好的想法也可以去github提request,也感謝WingCH的貢獻
分析需求
為什麼要封裝?
-
全域性token驗證
-
自定義攔截器
-
快取處理
-
統一封裝業務錯誤邏輯
-
代理配置
-
重試機制
-
log輸出
-
自定義解析,資料脫殼
要初始化哪些配置?
- 域名
- 代理地址
- cookie本地快取地址
- 超時時間
- 自定義攔截器
定義一個配置資訊類去初始化這些配置:
// dio 配置項
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>();
}
複製程式碼
請求差異化有哪些配置?
-
解析策略
許多公司介面規範經歷過變更,有多個返回型別,那麼就需要針對不同的資料型別,做不同的解析。
比如舊版本:
// 舊版本 { "code": 1, "data": {}, "state": true } // 新版本 { "code": 1, "data": { "data": {}, "hasmore":false }, "message": “success” } 複製程式碼
要做到脫殼,拿到解析後的
data
,就需要兩種解析策略。所以需要根據不同介面動態配置解析策略。 -
path
-
引數
-
cancelToken
-
dio 的常用引數
Dio 的請求引數已經很全面的包括了分析出的配置引數,只需要另新增一個解析策略即可。
遵守 SOLID 原則定義一個抽象解析策略:
/// Response 解析 abstract class HttpTransformer { HttpResponse parse(Response response); } 複製程式碼
根據實際需求預設實現:
class DefaultHttpTransformer extends HttpTransformer { // 假設介面返回型別 // { // "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(); /// 內部構造方法,可避免外部暴露建構函式,進行例項化 DefaultHttpTransformer._internal(); /// 工廠構造方法,這裡使用命名建構函式方式進行宣告 factory DefaultHttpTransformer.getInstance() => _instance; } 複製程式碼
單例模式是為了避免多次建立例項。方便下一步使用。
異常處理
異常大體分為以下幾種:
- 網路異常
- 客戶端請求異常
- 服務端異常
客戶端異常又可拆分兩種常見的異常:請求引數或路徑錯誤,鑑權失敗/token
失效
異常歸檔後建立異常:
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);
}
/// 服務端響應錯誤
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();
}
複製程式碼
返回資料型別
返回的資料型別,需要有成功或是失敗的標識,還需要脫殼後的資料,如果失敗了,也需要失敗的資訊,定義幾個工廠方法方便建立例項:
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 配置組裝,需要我們定義一個初始化類,用於把請求的初始化配置新增進去。一般可以定義一個單例類,init
方法裡去初始化一個 Dio ,也可以採用實現 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 標準,建立對應的請求方法:
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;
}
}
}
複製程式碼
響應解析
得到請求資料後,解析為定義的通用返回資料型別,需要首先判斷是否取得返回值,然後判斷網路請求成功,網路請求成功之後,採取判斷是否介面返回期望的資料,還是因為請求引數錯誤或者伺服器錯誤返回了錯誤資訊。如果錯誤了,把錯誤資訊格式化為定義的異常:
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: "沒有許可權", code: response.statusCode));
}
// 介面呼叫成功
if (_isRequestSuccess(response.statusCode)) {
return httpTransformer.parse(response);
} else {
// 介面呼叫失敗
return HttpResponse.failure(
errorMsg: response.statusMessage, errorCode: response.statusCode);
}
}
HttpResponse handleException(Exception exception) {
var parseException = _parseException(exception);
return HttpResponse.failureFromError(parseException);
}
/// 鑑權失敗
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: "沒有許可權", code: errCode);
case 403:
return BadRequestException(message: "伺服器拒絕執行", code: errCode);
case 404:
return BadRequestException(message: "無法連線伺服器", code: errCode);
case 405:
return BadRequestException(message: "請求方法被禁止", code: errCode);
case 500:
return BadServiceException(message: "伺服器內部錯誤", code: errCode);
case 502:
return BadServiceException(message: "無效的請求", code: errCode);
case 503:
return BadServiceException(message: "伺服器掛了", code: errCode);
case 505:
return UnauthorisedException(
message: "不支援HTTP協議請求", 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攔截
預設的通用攔截器在 AppDio
裡直接定義,如果需要額外配置的攔截器,從HttpConfig
裡傳入。
這些攔截器的建立,可以參考上一篇強大的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());
}
}
複製程式碼
附上開發環境:
[✓] Flutter (Channel stable, 2.0.5, on Mac OS X 10.15.7 19H15 darwin-x64, locale zh-Hans-CN)
複製程式碼