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的介紹以及對HttpClientHttpDio這三個網路請求的分析,這章節主要是對Dio 原始碼的分析。

從post請求來進行分析

var response = await Dio().post('http://localhost:8080/login', queryParameters: {
  "username": "123456",
  "password": "123456"
});
複製程式碼

post方法

post 方法有七個引數,在該函式中呼叫了request方法,並沒有做任何處理,接下來我們看下request 方法。

  1. path: 請求的url連結
  2. data: 請求資料,例如上傳用到的FromData
  3. queryParameters: 查詢引數
  4. options: 請求選項
  5. cancelToken: 用來取消傳送請求的token
  6. onSendProgress: 網路請求傳送的進度
  7. onReceiveProgress: 網路請求接收的進度
@override
Future<Response<T>> post<T>(
  String path, {
  data,
  Map<String, dynamic>? queryParameters,
  Options? options,
  CancelToken? cancelToken,
  ProgressCallback? onSendProgress,
  ProgressCallback? onReceiveProgress,
}) {
  return request<T>(
    path,
    data: data,
    options: checkOptions('POST', options),
    queryParameters: queryParameters,
    cancelToken: cancelToken,
    onSendProgress: onSendProgress,
    onReceiveProgress: onReceiveProgress,
  );
}
複製程式碼

request方法

request 接收了post 方法中傳進來的引數。

第一步:合併選項

通過呼叫compose 方法來進行選項合併。

compose函式執行流程
  1. 首先判斷queryParameters 是否為空,不為空則新增到一個query 臨時變數中
  2. options 中的headers 全部拿出來存到臨時變數_headers中進行不區分大小寫的對映,並刪除headers 中的 contentTypeHeader
  3. 如果headers不為空,則把headers 中的全部屬性新增到臨時變數_headers 中並把contentTypeHeader賦值到一個臨時變數_contentType中。
  4. options中的自定義欄位extra 賦值給一個臨時變數
  5. method統一轉換成大寫字母
  6. 建立一個RequestOptions並傳入上面處理過的引數並返回
compose原始碼
RequestOptions compose(
  BaseOptions baseOpt,
  String path, {
    data,
    Map<String, dynamic>? queryParameters,
    CancelToken? cancelToken,
    Options? options,
    ProgressCallback? onSendProgress,
    ProgressCallback? onReceiveProgress,
  }) {
  var query = <String, dynamic>{};
  if (queryParameters != null) query.addAll(queryParameters);
  query.addAll(baseOpt.queryParameters);

  var _headers = caseInsensitiveKeyMap(baseOpt.headers);
  _headers.remove(Headers.contentTypeHeader);

  var _contentType;

  if (headers != null) {
    _headers.addAll(headers!);
    _contentType = _headers[Headers.contentTypeHeader];
  }

  var _extra = Map<String, dynamic>.from(baseOpt.extra);
  if (extra != null) {
    _extra.addAll(extra!);
  }
  var _method = (method ?? baseOpt.method).toUpperCase();
  var requestOptions = RequestOptions(
    method: _method,
    headers: _headers,
    extra: _extra,
    baseUrl: baseOpt.baseUrl,
    path: path,
    data: data,
    connectTimeout: baseOpt.connectTimeout,
    sendTimeout: sendTimeout ?? baseOpt.sendTimeout,
    receiveTimeout: receiveTimeout ?? baseOpt.receiveTimeout,
    responseType: responseType ?? baseOpt.responseType,
    validateStatus: validateStatus ?? baseOpt.validateStatus,
    receiveDataWhenStatusError:
    receiveDataWhenStatusError ?? baseOpt.receiveDataWhenStatusError,
    followRedirects: followRedirects ?? baseOpt.followRedirects,
    maxRedirects: maxRedirects ?? baseOpt.maxRedirects,
    queryParameters: query,
    requestEncoder: requestEncoder ?? baseOpt.requestEncoder,
    responseDecoder: responseDecoder ?? baseOpt.responseDecoder,
    listFormat: listFormat ?? baseOpt.listFormat,
  );

  requestOptions.onReceiveProgress = onReceiveProgress;
  requestOptions.onSendProgress = onSendProgress;
  requestOptions.cancelToken = cancelToken;

  requestOptions.contentType = _contentType ??
    contentType ??
    baseOpt.contentTypeWithRequestBody(_method);
  return requestOptions;
}
複製程式碼

第二步:呼叫fetch

判斷使用者是否關閉請求,關閉則退出,未關閉呼叫Fetch方法

request原始碼
 @override
  Future<Response<T>> request<T>(
    String path, {
    data,
    Map<String, dynamic>? queryParameters,
    CancelToken? cancelToken,
    Options? options,
    ProgressCallback? onSendProgress,
    ProgressCallback? onReceiveProgress,
  }) async {
    options ??= Options();
    var requestOptions = options.compose(
      this.options,
      path,
      data: data,
      queryParameters: queryParameters,
      onReceiveProgress: onReceiveProgress,
      onSendProgress: onSendProgress,
      cancelToken: cancelToken,
    );
    requestOptions.onReceiveProgress = onReceiveProgress;
    requestOptions.onSendProgress = onSendProgress;
    requestOptions.cancelToken = cancelToken;

    if (_closed) {
      throw DioError(
        requestOptions: requestOptions,
        error: "Dio can't establish new connection after closed.",
      );
    }

    return fetch<T>(requestOptions);
  }
複製程式碼

Fetch方法

第一步:請求引數賦值

判斷如果傳遞進來的requestOptions.cancelToken 不為空的情況下,則把傳遞進來的requestOptions 進行賦值。

if (requestOptions.cancelToken != null) {
  requestOptions.cancelToken!.requestOptions = requestOptions;
}
複製程式碼
第二步:響應資料設定

如果請求回來的引數不是動態型別並且不是bytesstream的方式,則進行判斷該返回值型別是否是字串,為真返回UTF-8的編碼型別,否則返回字串型別

if (T != dynamic &&
    !(requestOptions.responseType == ResponseType.bytes ||
      requestOptions.responseType == ResponseType.stream)) {
  if (T == String) {
    requestOptions.responseType = ResponseType.plain;
  } else {
    requestOptions.responseType = ResponseType.json;
  }
}
複製程式碼
第三步:構建請求流並新增攔截器

1、構建一個請求流,InterceptorState是一個內部類,裡面總共與兩個屬性T data 以及 InterceptorResultType type ,用於當前攔截器和下一個攔截器之間傳遞狀態所定義。

2、按 FIFO 順序執行,迴圈遍歷向請求流中新增請求攔截器,攔截器中最主要的有RequestInterceptor 請求前攔截和 ResponseInterceptor 請求後攔截的兩個例項。

var future = Future<dynamic>(() => InterceptorState(requestOptions));

interceptors.forEach((Interceptor interceptor) {
  future = future.then(_requestInterceptorWrapper(interceptor.onRequest));
});
複製程式碼
第四步:攔截器轉換為函式回撥

這裡主要做的一步操作是把函式的回撥作為方法的引數,這樣就實現了把攔截器轉換為函式回撥,這裡做了一層判斷,如果state.type 等於 next 的話,那麼會增加一個監聽取消的非同步任務,並把cancelToken傳遞給了這個任務,接下來他會檢查當前的這個攔截器請求是否入隊,最後定義了一個請求攔截器的變數,該攔截器裡面有三個主要的方法分別是next() 、resole()reject() ,最後把這個攔截器返回出去。

FutureOr Function(dynamic) _requestInterceptorWrapper(
  void Function(
    RequestOptions options,
    RequestInterceptorHandler handler,
  )
  interceptor,
) {
  return (dynamic _state) async {
    var state = _state as InterceptorState;
    if (state.type == InterceptorResultType.next) {
      return listenCancelForAsyncTask(
        requestOptions.cancelToken,
        Future(() {
          return checkIfNeedEnqueue(interceptors.requestLock, () {
            var requestHandler = RequestInterceptorHandler();
            interceptor(state.data, requestHandler);
            return requestHandler.future;
          });
        }),
      );
    } else {
      return state;
    }
  };
}
複製程式碼
第五步:構建請求流排程回撥

排程回撥和新增攔截器轉換為函式回撥,不同的是排程回撥裡面進行了請求分發。

future = future.then(_requestInterceptorWrapper((
  RequestOptions reqOpt,
  RequestInterceptorHandler handler,
) {
  requestOptions = reqOpt;
  _dispatchRequest(reqOpt).then(
    (value) => handler.resolve(value, true),
    onError: (e) {
      handler.reject(e, true);
    },
  );
}));
複製程式碼
第六步:請求分發

1、請求分發函式裡面會呼叫_transfromData 進行資料轉換,最終轉換出來的資料是一個 Stream 流。

2、呼叫網路請求介面卡進行網路請求 fetch 方法,這裡說明下該介面卡定義有兩個,分別如下:

2.1、BrowserHttpClientAdapter 是呼叫了html_dart2js 的庫進行了網路請求,該庫是將dart程式碼編譯成可部署的JavaScript

2.2、DefaultHttpClientAdapter 是採用系統請求庫HttpClient進行網路請求。

3、把響應頭賦值給臨時變數responseBody 並通過fromMap 轉換成 Map<String, List<String>> 型別

4、初始化響應類,並對返回的資料進行賦值處理。

5、判斷如果是正常返回就對ret.data 變數進行資料格式轉換,失敗則取消監聽響應流

6、檢查請求是否通過cancelToken 變數取消了,如果取消了則直接丟擲異常

7、最後在進行請求是否正常,如果正常則檢查是否入隊並返回,否則直接丟擲請求異常DioError

Future<Response<T>> _dispatchRequest<T>(RequestOptions reqOpt) async {
  var cancelToken = reqOpt.cancelToken;
  ResponseBody responseBody;
  try {
    var stream = await _transformData(reqOpt);
    responseBody = await httpClientAdapter.fetch(
      reqOpt,
      stream,
      cancelToken?.whenCancel,
    );
    responseBody.headers = responseBody.headers;
    var headers = Headers.fromMap(responseBody.headers);
    var ret = Response(
      headers: headers,
      requestOptions: reqOpt,
      redirects: responseBody.redirects ?? [],
      isRedirect: responseBody.isRedirect,
      statusCode: responseBody.statusCode,
      statusMessage: responseBody.statusMessage,
      extra: responseBody.extra,
    );
    var statusOk = reqOpt.validateStatus(responseBody.statusCode);
    if (statusOk || reqOpt.receiveDataWhenStatusError == true) {
      var forceConvert = !(T == dynamic || T == String) &&
        !(reqOpt.responseType == ResponseType.bytes ||
          reqOpt.responseType == ResponseType.stream);
      String? contentType;
      if (forceConvert) {
        contentType = headers.value(Headers.contentTypeHeader);
        headers.set(Headers.contentTypeHeader, Headers.jsonContentType);
      }
      ret.data = await transformer.transformResponse(reqOpt, responseBody);
      if (forceConvert) {
        headers.set(Headers.contentTypeHeader, contentType);
      }
    } else {
      await responseBody.stream.listen(null).cancel();
    }
    checkCancelled(cancelToken);
    if (statusOk) {
      return checkIfNeedEnqueue(interceptors.responseLock, () => ret)
        as Response<T>;
    } else {
      throw DioError(
        requestOptions: reqOpt,
        response: ret,
        error: 'Http status error [${responseBody.statusCode}]',
        type: DioErrorType.response,
      );
    }
  } catch (e) {
    throw assureDioError(e, reqOpt);
  }
}
複製程式碼

download方法

download 方法的執行流程和post一樣,只是接收的資料型別以及邏輯處理上不一樣,會把下載的檔案儲存到本地,具體實現流程在 src>entry>dio_fornative.dart 檔案中,這裡不在做過多的贅述。

總結

在我們進行 get() post() 等呼叫時,都會進入到request方法,request 方法主要負責對請求引數以及自定義請求頭的統一處理,並呼叫了fetch 方法,而 fetch 中是對響應資料設定、構建請求流、新增攔截器、請求分發的操作。

相關文章