一步一步教你封裝最新版的Dio

艾維碼發表於2021-05-16

許多掘金朋友在上一篇留言,說要封裝下最新版,所以這篇把封裝思路寫下,大家可以自己封裝。有好的想法也可以去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)

複製程式碼

相關文章