Flutter Dio原始碼分析(四)--封裝

Jimi發表於2021-08-30

文章系列

Flutter Dio原始碼分析(一)--Dio介紹

Flutter Dio原始碼分析(二)--HttpClient、Http、Dio對比

Flutter Dio原始碼分析(三)--深度剖析

Flutter Dio原始碼分析(四)--封裝

視訊系列

Flutter Dio原始碼分析(一)--Dio介紹視訊教程

Flutter Dio原始碼分析(二)--HttpClient、Http、Dio對比視訊教程

Flutter Dio原始碼分析(三)--深度剖析視訊教程

Flutter Dio原始碼分析(四)--封裝視訊教程

原始碼倉庫地址

github倉庫地址

前言

本文會手把手教你該怎麼去封裝一個類庫,平時在我們的工作中都是拿著別人的造好的輪子在使用,這篇文章將帶你怎麼去自己造輪子,以後再碰到別的類庫需要對其進行封裝的時候提供一個的思路和方法。

為什麼需要封裝Dio?

在前面的文章中,我們對Dio的基本使用、請求庫對比、原始碼分析,我們知道Dio 的使用非常的簡單,那為什麼還需要進行封裝呢?有兩點如下:

1、程式碼遷移

當元件庫方法發生重要改變需要遷移的時候如果有多處地方用到,那麼需要對使用到的每個檔案都進行修改,非常的繁瑣而且很容易出問題。

2、請求庫切換

當不需要Dio 庫的時候,我們可以隨時方便切換到別的網路請求庫,當然Dio 目前內建支援使用第三方庫的介面卡。

3、統一配置

因為一個應用程式基本都是統一的配置方式,所以我們可以針對攔截器轉換器快取統一處理錯誤代理配置證書校驗 等多個配置進行統一管理。

使用單例模式進行Dio封裝

為什麼使用單例模式?

因為我們的應用程式在每個頁面中都會用到網路請求,那麼如果我們每次請求的時候都去例項化一個Dio,無非是增加了系統不必要的開銷,而使用單例模式物件一旦建立每次訪問都是同一個物件,不需要再次例項化該類的物件。

建立單例類

這是通過靜態變數的私有構造器來建立的單例模式

class DioUtil {

  factory DioUtil() => _getInstance();
  static DioUtil get instance => _getInstance();
  static DioUtil _instance;

  DioUtil._init() {
    // 初始化
  }
  static DioUtil _getInstance() {
    if (_instance == null) {
      _instance = DioUtil._init();
    }
    return _instance;
  }
}
複製程式碼

對Dio請求進行初始化

我們對 超時時間響應時間BaseUrl 進行統一設定

/// 連線超時時間
static const int CONNECT_TIMEOUT = 60*1000;
/// 響應超時時間
static const int RECEIVE_TIMEOUT = 60*1000;

/// 宣告Dio變數
Dio _dio;

DioUtil._init() {
  if (_dio == null) {
    /// 初始化基本選項
    BaseOptions options = BaseOptions(
      baseUrl: "http://localhost:8080",
      connectTimeout: CONNECT_TIMEOUT,
      receiveTimeout: RECEIVE_TIMEOUT
    );

    /// 初始化dio
    _dio = Dio(options);
  }
}
複製程式碼

對Restful APi風格進行統一封裝

因為不管是get()還是post()請求,Dio 內部最終都會呼叫request 方法,只是傳入的method 不一樣,所以我們這裡定義一個列舉型別在一個方法中進行處理

enum DioMethod {
  get,
  post,
  put,
  delete,
  patch,
  head,
}

/// 請求類
Future<T> request<T>(String path, {
  DioMethod method = DioMethod.get,
  Map<String, dynamic> params,
  data,
  CancelToken cancelToken,
  Options options,
  ProgressCallback onSendProgress,
  ProgressCallback onReceiveProgress,
}) async {
  const _methodValues = {
    DioMethod.get: 'get',
    DioMethod.post: 'post',
    DioMethod.put: 'put',
    DioMethod.delete: 'delete',
    DioMethod.patch: 'patch',
    DioMethod.head: 'head'
  };


  options ??= Options(method: _methodValues[method]);
  try {
    Response response;
    response = await _dio.request(path,
                                  data: data,
                                  queryParameters: params,
                                  cancelToken: cancelToken,
                                  options: options,
                                  onSendProgress: onSendProgress,
                                  onReceiveProgress: onReceiveProgress
                                 );
    return response.data;
  } on DioError catch (e) {
    throw e;
  }
}
複製程式碼

攔截器

介紹

我們已經把Restful API 風格簡化成了一個方法,通過DioMethod 來標明不同的請求方式。在我們平時開發的過程中,需要在請求前、響應前、錯誤時對某一些介面做特殊的處理,那我們就需要用到攔截器。Dio 為我們提供了自定義攔截器功能,很容易輕鬆的實現對請求、響應、錯誤時進行攔截

錯誤統一處理

我們發現雖然Dio框架已經封裝了一個DioError類庫,但如果需要對返回的錯誤進行統一彈窗處理或者路由跳轉等就只能自定義了

請求前統一處理

在我們傳送請求的時候會碰到幾種情況,比如需要對非open開頭的介面自動加上一些特定的引數,獲取需要在請求頭增加統一的token

響應前統一處理

在我們請求介面前可以對響應資料進行一些基礎的處理,比如對響應的結果進行自定義封裝,還可以針對單獨的url 做特殊處理等。

自定義攔截器實現

import 'package:dio/dio.dart';
import 'package:flutter_dio/dio_util/dio_response.dart';

class DioInterceptors extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {

    // 對非open的介面的請求引數全部增加userId
    if (!options.path.contains("open")) {
      options.queryParameters["userId"] = "xxx";
    }

    // 頭部新增token
    options.headers["token"] = "xxx";

    // 更多業務需求

    handler.next(options);

    // super.onRequest(options, handler);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {

    // 請求成功是對資料做基本處理
    if (response.statusCode == 200) {
      response.data = DioResponse(code: 0, message: "請求成功啦", data: response);
    } else {
      response.data = DioResponse(code: 1, message: "請求失敗啦", data: response);
    }

    // 對某些單獨的url返回資料做特殊處理
    if (response.requestOptions.baseUrl.contains("???????")) {
      //....
    }

    // 根據公司的業務需求進行定製化處理

    // 重點
    handler.next(response);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    switch(err.type) {
        // 連線伺服器超時
      case DioErrorType.connectTimeout:
        {
          // 根據自己的業務需求來設定該如何操作,可以是彈出框提示/或者做一些路由跳轉處理
        }
        break;
        // 響應超時
      case DioErrorType.receiveTimeout:
        {
          // 根據自己的業務需求來設定該如何操作,可以是彈出框提示/或者做一些路由跳轉處理
        }
        break;
        // 傳送超時
      case DioErrorType.sendTimeout:
        {
          // 根據自己的業務需求來設定該如何操作,可以是彈出框提示/或者做一些路由跳轉處理
        }
        break;
        // 請求取消
      case DioErrorType.cancel:
        {
          // 根據自己的業務需求來設定該如何操作,可以是彈出框提示/或者做一些路由跳轉處理
        }
        break;
        // 404/503錯誤
      case DioErrorType.response:
        {
          // 根據自己的業務需求來設定該如何操作,可以是彈出框提示/或者做一些路由跳轉處理
        }
        break;
        // other 其他錯誤型別
      case DioErrorType.other:
        {

        }
        break;

    }
    super.onError(err, handler);
  }
}


class DioResponse<T> {

  /// 訊息(例如成功訊息文字/錯誤訊息文字)
  final String message;
  /// 自定義code(可根據內部定義方式)
  final int code;
  /// 介面返回的資料
  final T data;
  /// 需要新增更多
  /// .........

  DioResponse({
    this.message,
    this.data,
    this.code,
  });

  @override
  String toString() {
    StringBuffer sb = StringBuffer('{');
    sb.write("\"message\":\"$message\"");
    sb.write(",\"errorMsg\":\"$code\"");
    sb.write(",\"data\":\"$data\"");
    sb.write('}');
    return sb.toString();
  }
}

class DioResponseCode {
  /// 成功
  static const int SUCCESS = 0;
  /// 錯誤
  static const int ERROR = 1;
  /// 更多
}
複製程式碼

轉換器

介紹

轉換器Transformer 用於對請求資料和響應資料進行編解碼處理。Dio實現了一個預設轉換器DefaultTransformer作為預設的 Transformer. 如果想對請求/響應資料進行自定義編解碼處理,可以提供自定義轉換器

為什麼需要轉換器?

我們看了轉換器的介紹,發現和攔截器的功能差不多,那為什麼還要存在轉換器,有兩點:

  1. 和攔截器解耦
  2. 不修改原始請求資料

執行流程:請求攔截器 >> 請求轉換器 >> 發起請求 >> 響應轉換器 >> 響應攔截器 >> 最終結果

請求轉換器

只會被用於 'PUT'、 'POST'、 'PATCH'方法,因為只有這些方法才可以攜帶請求體(request body)

響應轉換器

會被用於所有請求方法的返回資料。

自定義轉換器實現

import 'dart:async';
import 'package:dio/dio.dart';

class DioTransformer extends DefaultTransformer {
  @override
  Future<String> transformRequest(RequestOptions options) async {
    // 如果請求的資料介面是List<String>那我們直接丟擲異常
    if (options.data is List<String>) {
      throw DioError(
        error: "你不能直接傳送List資料到伺服器",
        requestOptions: options,
      );
    } else {
      return super.transformRequest(options);
    }
  }

  @override
  Future transformResponse(RequestOptions options, ResponseBody response) async {
    // 例如我們響應選項裡面沒有自定義某些頭部資料,那我們就可以自行新增
    options.extra['myHeader'] = 'abc';
    return super.transformResponse(options, response);
  }
}
複製程式碼

重新整理Token

在開發過程中,客戶端和伺服器打交道的時候,往往會用一個token來做校驗,因為每個公司處理重新整理token的邏輯都不一樣,我這裡舉一個簡單的例子

我們需要給所有的請求頭中新增一個refreshToken,如果refreshToken不存在,我們先去請求refreshToken,獲取到refreshToken後,再發起後續請求。 由於請求refreshToken的過程是非同步的,我們需要在請求過程中鎖定後續請求(因為它們需要refreshToken), 直到refreshToken請求成功後,再解鎖

import 'package:dio/dio.dart';
import 'package:flutter_dio/dio_util/dio_util.dart';

class DioTokenInterceptors extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    if (options.headers['refreshToken'] == null) {
      DioUtil.instance.dio.lock();
      Dio _tokenDio = Dio();
      _tokenDio..get("http://localhost:8080/getRefreshToken").then((d) {
        options.headers['refreshToken'] = d.data['data']['token'];
        handler.next(options);
      }).catchError((error, stackTrace) {
        handler.reject(error, true);
      }) .whenComplete(() {
        DioUtil.instance.dio.unlock();
      }); // unlock the dio
    } else {
      options.headers['refreshToken'] = options.headers['refreshToken'];
      handler.next(options);
    }
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) async {

    // 響應前需要做重新整理token的操作

    super.onResponse(response, handler);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    super.onError(err, handler);
  }
}
複製程式碼

取消請求

為什麼我們需要有取消請求的功能,如果當我們的頁面在傳送請求時,使用者主動退出當前介面或者app應用程式退出的時候資料還沒有響應,那我們就需要取消該網路請求,防止不必要的錯誤。

/// 取消請求token
CancelToken _cancelToken = CancelToken();

/// 取消網路請求
void cancelRequests({CancelToken token}) {
  token ?? _cancelToken?.cancel("cancelled");
}
複製程式碼

cookie管理

cookie介紹

伺服器生成一小段文字資訊,傳送給瀏覽器,瀏覽器把 cookie 以kv形式儲存到本地某個目錄下的文字檔案內,下一次請求同一網站時會把該 cookie 傳送給伺服器。

原理

  1. 客戶端傳送一個請求(http請求+使用者認證資訊)到伺服器
  2. 認證成功,伺服器傳送一個HttpResponse響應到客戶端,其中包含Set-Cookie的頭部
  3. 客戶端提取並儲存 cookie 於記憶體或磁碟
  4. 再次請求時,HttpRequest請求中會包含一個已認證的 Cookie 的頭部
  5. 伺服器解析cookie,獲取 cookie 中客戶端的相關資訊
  6. 伺服器返回響應資料

使用

cookie 的使用需要用到兩個第三方元件 dio_cookie_managercookie_jar

  • cookie_jar:Darthttp 請求的 cookie 管理器,通過它您可以輕鬆處理複雜的 cookie 策略和持久化 cookie
  • dio_cookie_manager: CookieManager 攔截器可以幫助我們自動管理請求/響應 cookie。 CookieManager 依賴於 cookieJar 包

匯入檔案

dio_cookie_manager: ^2.0.0
cookie_jar: ^3.0.1
複製程式碼
/// cookie
CookieJar cookieJar = CookieJar();

/// 新增cookie管理器
_dio.interceptors.add(CookieManager(cookieJar));

List<Cookie> cookies = [
  Cookie("xxx", xxx),
  // ....
];

//Save cookies            
DioUtil.instance.cookieJar.saveFromResponse(Uri.parse(BaseUrl.url), cookies);

//Get cookies   
List<Cookie> cookies = DioUtil.instance.cookieJar.loadForRequest(Uri.parse(BaseUrl.url));
複製程式碼

網路介面快取

為什麼使用快取?

因為在我們平時的開發過程中,會碰到一種情況,在進行網路請求時,我們希望能正常訪問到上次的資料,對於使用者的體驗比較好,而不是展示一個空白的頁面,該快取主要是 《Flutter實戰》網路介面快取 提供參考。

使用shared_preferences持久化

我們在程式退出後記憶體快取將會消失,所以我們用shared_preferences 進行磁碟快取資料。

import 'dart:collection';
import 'package:dio/dio.dart';
import 'package:flutter_dio/dio_util/dio_util.dart';

class CacheObject {
  CacheObject(this.response)
    : timeStamp = DateTime.now().millisecondsSinceEpoch;
  Response response;
  int timeStamp;

  @override
  bool operator ==(other) {
    return response.hashCode == other.hashCode;
  }

  @override
  int get hashCode => response.realUri.hashCode;
}

class DioCacheInterceptors extends Interceptor {
  // 為確保迭代器順序和物件插入時間一致順序一致,我們使用LinkedHashMap
  var cache = LinkedHashMap<String, CacheObject>();

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    if (!DioUtil.CACHE_ENABLE) return super.onRequest(options, handler);
    // 通過refresh欄位來判斷是否重新整理快取
    bool refresh = options.extra["refresh"] == true;
    if (refresh) {
      // 刪除本地快取
      delete(options.uri.toString());
    }
    // 只有get請求才開啟快取
    if (options.extra["noCache"] != true &&
        options.method.toLowerCase() == 'get') {
      String key = options.extra["cacheKey"] ?? options.uri.toString();
      var ob = cache[key];
      if (ob != null) {
        //若快取未過期,則返回快取內容
        if ((DateTime.now().millisecondsSinceEpoch - ob.timeStamp) / 1000 <
            DioUtil.MAX_CACHE_AGE) {
          return handler.resolve(cache[key].response);
        } else {
          //若已過期則刪除快取,繼續向伺服器請求
          cache.remove(key);
        }
      }
    }
    super.onRequest(options, handler);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    // 把響應的資料儲存到快取
    if (DioUtil.CACHE_ENABLE) {
      _saveCache(response);
    }

    super.onResponse(response, handler);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    // TODO: implement onError
    super.onError(err, handler);
  }


  _saveCache(Response object) {
    RequestOptions options = object.requestOptions;
    if (options.extra["noCache"] != true &&
        options.method.toLowerCase() == "get") {
      // 如果快取數量超過最大數量限制,則先移除最早的一條記錄
      if (cache.length == DioUtil.MAX_CACHE_COUNT) {
        cache.remove(cache[cache.keys.first]);
      }
      String key = options.extra["cacheKey"] ?? options.uri.toString();
      cache[key] = CacheObject(object);
    }
  }

  void delete(String key) {
    cache.remove(key);
  }
}
複製程式碼

代理配置

在我們用flutter進行抓包的時候需要配置Dio代理。由DefaultHttpClientAdapter 提供了一個onHttpClientCreate 回撥來設定底層 HttpClient的代理。

/// 設定Http代理(設定即開啟)
void setProxy({
  String proxyAddress,
  bool enable = false
}) {
  if (enable) {
    (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
      (HttpClient client) {
      client.findProxy = (uri) {
        return proxyAddress;
      };
      client.badCertificateCallback =
        (X509Certificate cert, String host, int port) => true;
    };
  }
}
複製程式碼

證書校驗

用於驗證正在訪問的網站是否真實。提供安全性,因為證書和域名繫結,並且由根證書機構簽名確認。

/// 設定https證書校驗
void setHttpsCertificateVerification({
  String pem,
  bool enable = false
}) {
  if (enable) {
    (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate  = (client) {
      client.badCertificateCallback=(X509Certificate cert, String host, int port){
        if(cert.pem==pem){ // 驗證證書
          return true;
        }
        return false;
      };
    };
  }
}

複製程式碼

統一日誌列印

日誌列印主要是幫助我們開發時進行輔助排錯

/// 開啟日誌列印
void openLog() {
	_dio.interceptors.add(LogInterceptor(responseBody: true));
}

DioUtil().openLog();
複製程式碼

相關文章