文章系列
Flutter Dio原始碼分析(二)--HttpClient、Http、Dio對比
視訊系列
Flutter Dio原始碼分析(一)--Dio介紹視訊教程
Flutter Dio原始碼分析(二)--HttpClient、Http、Dio對比視訊教程
原始碼倉庫地址
介紹
在前面兩篇文章中我們說了Dio
的介紹以及對HttpClient
、Http
、Dio
這三個網路請求的分析,這章節主要是對Dio
原始碼的分析。
從post請求來進行分析
var response = await Dio().post('http://localhost:8080/login', queryParameters: {
"username": "123456",
"password": "123456"
});
複製程式碼
post方法
post
方法有七個引數,在該函式中呼叫了request
方法,並沒有做任何處理,接下來我們看下request
方法。
- path: 請求的url連結
- data: 請求資料,例如上傳用到的FromData
- queryParameters: 查詢引數
- options: 請求選項
- cancelToken: 用來取消傳送請求的token
- onSendProgress: 網路請求傳送的進度
- 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函式執行流程
- 首先判斷
queryParameters
是否為空,不為空則新增到一個query
臨時變數中 - 把
options
中的headers
全部拿出來存到臨時變數_headers
中進行不區分大小寫的對映,並刪除headers
中的contentTypeHeader
- 如果
headers
不為空,則把headers
中的全部屬性新增到臨時變數_headers
中並把contentTypeHeader
賦值到一個臨時變數_contentType
中。 - 把
options
中的自定義欄位extra
賦值給一個臨時變數 - 把
method
統一轉換成大寫字母 - 建立一個
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;
}
複製程式碼
第二步:響應資料設定
如果請求回來的引數不是動態型別並且不是bytes
和stream
的方式,則進行判斷該返回值型別是否是字串,為真返回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
中是對響應資料設定、構建請求流、新增攔截器、請求分發的操作。