一、Uri
Uri 類 提供了 編碼和解碼 URI(URL) 字符的功能。 這些函數處理 URI 特殊的字符,例如 &
和 =
。 Uri 類還可以解析和處理 URI 的每個部分,比如 host, port, scheme 等。
1.1.Encoding and decoding fully qualified URIs(編碼解碼URI)
要編碼和解碼除了 URI 中特殊意義(例如 /
, :
, &
,#
)的字符, 則可以使用 encodeFull()
和 decodeFull()
函數。這兩個函數可以用來編碼和解碼整個 URI,并且保留 URI 特殊意義的字符不變。
var uri = 'http://example.org/api?foo=some message';
var encoded = Uri.encodeFull(uri);
assert(encoded ==
'http://example.org/api?foo=some%20message');
var decoded = Uri.decodeFull(encoded);
assert(uri == decoded);
注意:上面
some
和message
之間的空格被編碼了。
1.2.Encoding and decoding URI components(編碼解碼URI組件)
使用encodeComponent()
和 decodeComponent()
可以編碼 和解碼 URI 中的所有字符,特殊意義的字符(/
, &
, 和:
等) 也會編碼,
var uri = 'http://example.org/api?foo=some message';
var encoded = Uri.encodeComponent(uri);
assert(encoded ==
'http%3A%2F%2Fexample.org%2Fapi%3Ffoo%3Dsome%20message');
var decoded = Uri.decodeComponent(encoded);
assert(uri == decoded);
注意:上面特殊字符也被編碼了,比如
/
編碼為%2F
。
1.3.Parsing URIs
如果有個 Uri 對象或者 URI 字符串,使用 Uri 的屬性 可以獲取每個部分,比如 path
。使用 parse()
靜態 函數可以從字符串中解析一個 Uri 對象:
var uri = Uri.parse('http://example.org:8080/foo/bar#frag');
assert(uri.scheme == 'http');
assert(uri.host == 'example.org');
assert(uri.path == '/foo/bar');
assert(uri.fragment == 'frag');
assert(uri.origin == 'http://example.org:8080');
1.4.Building URIs
使用 Uri()
構造函數可以從 URI 的 各個部分來構造一個 Uri 對象:
var uri = new Uri(scheme: 'http', host: 'example.org',
path: '/foo/bar', fragment: 'frag');
assert(uri.toString() ==
'http://example.org/foo/bar#frag');
更多信息參考 Uri API 文檔 。
二、dart原生網絡請求API
原生API使用的是用dart:io
中的HttpClient
發起的請求,但HttpClient
本身功能較弱,很多常用功能都不支持。
HTTP API 在返回值中使用了Dart Futures。 建議使用async
/await
語法來調用API。
網絡調用通常遵循如下步驟:
- 1、創建 client.
- 2、構造 Uri.
- 3、發起請求, 等待請求,同時您也可以配置請求headers、 body。
- 4、關閉請求, 等待響應.
- 5、解碼響應的內容.
以下示例對HTTPS GET請求返回的JSON數據進行解碼:
import 'dart:async';
import 'dart:io';
import 'dart:convert'; // 使用dart:convert內置庫可以簡單解碼和編碼JSON
_getIPAddress() async {
var url = 'https://httpbin.org/ip';
// 1.創建 client.
var httpClient = new HttpClient();
// 2.構造 Uri.
var uri = Uri.parse(url);
String result;
try {
// 3.發起請求, 等待請求,同時您也可以配置請求headers、 body。
var request = await httpClient.getUrl(uri);
// 4.關閉請求, 等待響應.
var response = await request.close();
if (response.statusCode == HttpStatus.ok) {
// 5.解碼響應的內容.
var json = await response.transform(utf8.decoder).join();
var data = jsonDecode(json);
result = data['origin'];
} else {
result =
'Error getting IP address:\nHttp status ${response.statusCode}';
}
} catch (exception) {
result = 'Failed getting IP address';
}
print(result); // 14.147.104.242, 14.147.104.242
三、使用第三方庫http
http包含一組高級函數和類,可以方便地使用HTTP資源。它與平臺無關,可以在命令行和瀏覽器上使用。
3.1.庫的安裝
- 1、在
pubspec.yaml
文件添加這個包的依賴:
dependencies:
http: ^0.12.0+2
- 2、通過命令行
pub
命令安裝包
pub get
如果你裝了Flutter插件也可以用以下令(也可通過用戶交互界面安裝,如Visual Studio Code、Android Studio裝了Flutter插件):
flutter packages get
3.2.庫的使用
使用這個庫最簡單的方法是通過頂級函數。它們允許你以最便捷的方式發出單獨的HTTP請求:
import 'package:http/http.dart' as http;
var url = 'http://example.com/whatsit/create';
var response = await http.post(url, body: {'name': 'doodle', 'color': 'blue'});
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');
print(await http.read('http://example.com/foobar.txt'));
如果向同一服務器發出多個請求,可以使用Client而不是一次性請求來保持打開持久連接。如果您這樣做,請確保在完成時關閉client。
var client = new http.Client();
try {
var uriResponse = await client.post('http://example.com/whatsit/create',
body: {'name': 'doodle', 'color': 'blue'});
print(await client.get(uriResponse.bodyFields['uri']));
} finally {
client.close();
}
您還可以通過自己創建Request 或StreamedRequest 對象并將其傳遞給Client.send來對請求和響應施加更詳細的控制。
這個包被設計成可組合的。這使得外部庫可以很容易地相互協作,向其添加行為。希望添加行為的庫應該創建BaseClient的子類,該類包裝另一個Client并添加所需的行為:
class UserAgentClient extends http.BaseClient {
final String userAgent;
final http.Client _inner;
UserAgentClient(this.userAgent, this._inner);
Future<StreamedResponse> send(BaseRequest request) {
request.headers['user-agent'] = userAgent;
return _inner.send(request);
}
}
四、使用第三方庫dio(推薦)
dio是一個強大的Dart Http請求庫,支持Restful API、FormData、攔截器、請求取消、Cookie管理、文件上傳/下載、超時、自定義適配器等...
4.1.添加依賴
- 1、在
pubspec.yaml
文件添加這個包的依賴:
dependencies:
dio: ^2.1.5 // 請使用pub上2.1分支的最新版本
- 2、通過命令行
pub
命令安裝包
pub get
如果你裝了Flutter插件也可以用以下令(也可通過用戶交互界面安裝,如Visual Studio Code、Android Studio裝了Flutter插件):
flutter packages get
4.2.示例
- 一個極簡的示例
import 'package:dio/dio.dart';
void getHttp() async {
try {
Response response = await Dio().get("http://www.baidu.com");
print(response);
} catch (e) {
print(e);
}
}
- 發起一個
GET
請求 :
Response response;
Dio dio = new Dio();
response = await dio.get("/test?id=12&name=wendu")
print(response.data.toString());
// 請求參數也可以通過對象傳遞,上面的代碼等同于:
response = await dio.get("/test", queryParameters: {"id": 12, "name": "wendu"});
print(response.data.toString());
- 發起一個
POST
請求:
response = await dio.post("/test", data: {"id": 12, "name": "wendu"});
- 發起多個并發請求:
response = await Future.wait([dio.post("/info"), dio.get("/token")]);
- 下載文件:
response = await dio.download("https://www.google.com/", "./xx.html");
- 以流的方式接收響應數據:
Response<ResponseBody> rs = await Dio().get<ResponseBody>(url,
options: Options(responseType: ResponseType.stream), //設置接收類型為stream
);
print(rs.data.stream); //響應流
- 以二進制數組的方式接收響應數據:
Response<List<int>> rs = await Dio().get<List<int>>(url,
options: Options(responseType: ResponseType.bytes), //設置接收類型為bytes
);
print(rs.data); //二進制數組
- 發送 FormData:
FormData formData = new FormData.from({
"name": "wendux",
"age": 25,
});
response = await dio.post("/info", data: formData);
- 通過FormData上傳多個文件:
FormData formData = new FormData.from({
"name": "wendux",
"age": 25,
"file1": new UploadFileInfo(new File("./upload.txt"), "upload1.txt"),
//支持直接上傳字節數組 (List<int>) ,方便直接上傳內存中的內容
"file2": new UploadFileInfo.fromBytes(
utf8.encode("hello world"), "word.txt"),
// 支持文件數組上傳
"files": [
new UploadFileInfo(new File("./example/upload.txt"), "upload.txt"),
new UploadFileInfo(new File("./example/upload.txt"), "upload.txt")
]
});
response = await dio.post("/info", data: formData);
- 監聽發送(上傳)數據進度:
response = await dio.post(
"http://www.dtworkroom.com/doris/1/2.0.0/test",
data: {"aa": "bb" * 22},
onSendProgress: (int sent, int total) {
print("$sent $total");
},
);
- 以流的形式提交二進制數據:
// 二進制數據
List<int> postData = <int>[...];
await dio.post(
url,
data: Stream.fromIterable(postData.map((e) => [e])), //創建一個Stream<List<int>>
options: Options(
headers: {
HttpHeaders.contentLengthHeader: postData.length, // 設置content-length
},
),
);
注意:如果要監聽提交進度,則必須設置content-length,反之則是可選的。
4.3.Dio APIs
建議:在項目中使用Dio單例,這樣便可對同一個dio實例發起的所有請求進行一些統一的配置,比如設置公共header、請求基地址、超時時間等;這里有一個在Flutter工程中使用Dio單例(定義為top level變量)的示例供開發者參考。
你可以使用默認配置或傳遞一個可選 BaseOptions
參數來創建一個Dio實例 :
Dio dio = new Dio(); // 使用默認配置
// 配置dio實例
dio.options.baseUrl = "https://www.xx.com/api";
dio.options.connectTimeout = 5000; //5s
dio.options.receiveTimeout = 3000;
// 或者通過傳遞一個 `BaseOptions`來創建dio實例
BaseOptions options = new BaseOptions(
baseUrl: "https://www.xx.com/api",
connectTimeout: 5000,
receiveTimeout: 3000,
);
Dio dio = new Dio(options);
Dio實例的核心API是 :
Future request(String path, {data,Map queryParameters, Options options,CancelToken cancelToken, ProgressCallback onSendProgress, ProgressCallback onReceiveProgress)
response = await request(
"/test",
data: {"id": 12, "name": "xx"},
options: Options(method: "GET"),
);
請求方法別名
為了方便使用,Dio提供了一些其它的Restful API, 這些API都是request
的別名。
Future get(...)
Future post(...)
Future put(...)
Future delete(...)
Future head(...)
Future put(...)
Future path(...)
Future download(...)
4.4.請求配置
下面是所有的請求配置選項。 如果請求method
沒有指定,則默認為GET
:
{
/// Http method.
String method;
/// 請求基地址,可以包含子路徑,如: "https://www.google.com/api/".
String baseUrl;
/// Http請求頭.
Map<String, dynamic> headers;
/// 連接服務器超時時間,單位是毫秒.
int connectTimeout;
/// 2.x中為接收數據的最長時限.
int receiveTimeout;
/// 請求路徑,如果 `path` 以 "http(s)"開始, 則 `baseURL` 會被忽略; 否則,
/// 將會和baseUrl拼接出完整的的url.
String path = "";
/// 請求的Content-Type,默認值是[ContentType.JSON].
/// 如果您想以"application/x-www-form-urlencoded"格式編碼請求數據,
/// 可以設置此選項為 `ContentType.parse("application/x-www-form-urlencoded")`, 這樣[Dio]
/// 就會自動編碼請求體.
ContentType contentType;
/// [responseType] 表示期望以那種格式(方式)接受響應數據。
/// 目前 [ResponseType] 接受三種類型 `JSON`, `STREAM`, `PLAIN`.
///
/// 默認值是 `JSON`, 當響應頭中content-type為"application/json"時,dio 會自動將響應內容轉化為json對象。
/// 如果想以二進制方式接受響應數據,如下載一個二進制文件,那么可以使用 `STREAM`.
///
/// 如果想以文本(字符串)格式接收響應數據,請使用 `PLAIN`.
ResponseType responseType;
/// `validateStatus` 決定http響應狀態碼是否被dio視為請求成功, 返回`validateStatus`
/// 返回`true` , 請求結果就會按成功處理,否則會按失敗處理.
ValidateStatus validateStatus;
/// 用戶自定義字段,可以在 [Interceptor]、[Transformer] 和 [Response] 中取到.
Map<String, dynamic> extra;
/// 公共query參數
Map<String, dynamic /*String|Iterable<String>*/ > queryParameters;
}
這里有一個完成的示例.
4.5.響應數據
當請求成功時會返回一個Response對象,它包含如下字段:
{
/// 響應數據,可能已經被轉換了類型, 詳情請參考Options中的[ResponseType].
var data;
/// 響應頭
HttpHeaders headers;
/// 本次請求信息
Options request;
/// Http status code.
int statusCode;
/// 是否重定向
bool isRedirect;
/// 重定向信息
List<RedirectInfo> redirects ;
/// 最終真正的請求地址(因為可能會重定向)
Uri realUri;
/// 響應對象的自定義字段(可以在攔截器中設置它),調用方可以在`then`中獲取.
Map<String, dynamic> extra;
}
示例如下:
Response response = await dio.get("https://www.google.com");
print(response.data);
print(response.headers);
print(response.request);
print(response.statusCode);
4.5.1.泛型支持
2.0.18版本后可以通過泛型來指定對響應數據
假如有一個url返回的是json數據,返回數據在默認情況下(options.responseType
為json)會被自動轉為Json對象(Map或List)的:
Response response = await dio.get("/test");
print(response.data is Map); //true,自動轉為了map
上面的代碼在IDE里面輸入時,IDE是無法推斷出response.data
的真實類型,所以對于Map的方法和屬性給不出提示,這時我們只需要指定Response的泛型參數為Map即可:
Response<Map<String,dynamic>> r = await dio.get("/test");
print(r.data.containsKey("errCode")); // IDE可以給出代碼提示
有時我們如果想以字符串方式接收json文本的話,我們可以通過制定responseType
為plain
來禁止自動轉化:
Response response = await dio.get("/test", options: Options(responseType: ResponseType.plain));
現在,我們也可以通過指定泛型參數來做到這一點了:
Response response = await dio.get<String>("/test");
是不是很簡單,但是,上面的寫法有個瑕疵就是有些編輯器無法推斷出response.data的類型,所以當你對輸入response.data
時,字符串的方法和屬性不會被推薦出來,要解決這個問題很簡單,我們只需要指定Response的泛型參數為String即可:
Response<String> response = await dio.get<String>("/test");
同理,如果我們在BaseOptions
里設置了responseType
為ResponseType.plain
,那么我們需要對某一個接口返回的數據轉為Map的化,可以指定泛型參數為Map:
dio.options.responseType = ResponseType.plain;
Response<Map> r= await dio.get<Map>("/test");
注意:當responseType類型為plain或json時,泛型參數只能是String、Map和List三種類型,所有的請求內容都可以String形式接收,但只有Json數據可以轉為Map和List,所以如果泛型參數傳入Map或List時,則會強制將響應內容轉為Map或List,如果轉換失敗,則會拋出異常。
詳細示例請參見這里
4.6.攔截器
每個 Dio 實例都可以添加任意多個攔截器,通過攔截器你可以在請求之前或響應之后(但還沒有被 then
或 catchError
處理)做一些統一的預處理操作。
dio.interceptors.add(InterceptorsWrapper(
onRequest:(RequestOptions options){
// 在請求被發送之前做一些事情
return options; //continue
// 如果你想完成請求并返回一些自定義數據,可以返回一個`Response`對象或返回`dio.resolve(data)`。
// 這樣請求將會被終止,上層then會被調用,then中返回的數據將是你的自定義數據data.
//
// 如果你想終止請求并觸發一個錯誤,你可以返回一個`DioError`對象,或返回`dio.reject(errMsg)`,
// 這樣請求將被中止并觸發異常,上層catchError會被調用。
},
onResponse:(Response response) {
// 在返回響應數據之前做一些預處理
return response; // continue
},
onError: (DioError e) {
// 當請求失敗時做一些預處理
return e;//continue
}
));
4.6.1.完成和終止請求/響應
在所有攔截器中,你都可以改變請求執行流, 如果你想完成請求/響應并返回自定義數據,你可以返回一個 Response 對象或返回 dio.resolve(data)
的結果。 如果你想終止(觸發一個錯誤,上層catchError
會被調用)一個請求/響應,那么可以返回一個DioError
對象或返回 dio.reject(errMsg)
的結果.
dio.interceptors.add(InterceptorsWrapper(
onRequest:(RequestOptions options){
return dio.resolve("fake data")
},
));
Response response = await dio.get("/test");
print(response.data);//"fake data"
4.6.2.攔截器中支持異步任務
攔截器中不僅支持同步任務,而且也支持異步任務, 下面是在請求攔截器中發起異步任務的一個實例:
dio.interceptors.add(InterceptorsWrapper(
onRequest:(Options options) async{
//...If no token, request token firstly.
Response response = await dio.get("/token");
//Set the token to headers
options.headers["token"] = response.data["data"]["token"];
return options; //continue
}
));
4.6.3.Lock/unlock 攔截器
你可以通過調用攔截器的lock()
/unlock
方法來鎖定/解鎖攔截器。一旦請求/響應攔截器被鎖定,接下來的請求/響應將會在進入請求/響應攔截器之前排隊等待,直到解鎖后,這些入隊的請求才會繼續執行(進入攔截器)。這在一些需要串行化請求/響應的場景中非常實用,后面我們將給出一個示例。
tokenDio = new Dio(); //Create a new instance to request the token.
tokenDio.options = dio;
dio.interceptors.add(InterceptorsWrapper(
onRequest:(Options options) async {
// If no token, request token firstly and lock this interceptor
// to prevent other request enter this interceptor.
dio.interceptors.requestLock.lock();
// We use a new Dio(to avoid dead lock) instance to request token.
Response response = await tokenDio.get("/token");
//Set the token to headers
options.headers["token"] = response.data["data"]["token"];
dio.interceptors.requestLock.unlock();
return options; //continue
}
));
Clear()
方法
你也可以調用攔截器的clear()
方法來清空等待隊列。
4.6.4.別名
當請求攔截器被鎖定時,接下來的請求將會暫停,這等價于鎖住了dio實例,因此,Dio示例上提供了請求攔截器lock
/unlock
的別名方法:
dio.lock() == dio.interceptors.requestLock.lock()
dio.unlock() == dio.interceptors.requestLock.unlock()
dio.clear() == dio.interceptors.requestLock.clear()
4.6.5.示例
假設這么一個場景:出于安全原因,我們需要給所有的請求頭中添加一個csrfToken,如果csrfToken不存在,我們先去請求csrfToken,獲取到csrfToken后,再發起后續請求。 由于請求csrfToken的過程是異步的,我們需要在請求過程中鎖定后續請求(因為它們需要csrfToken), 直到csrfToken請求成功后,再解鎖,代碼如下:
dio.interceptors.add(InterceptorsWrapper(
onRequest: (Options options) {
print('send request:path:${options.path},baseURL:${options.baseUrl}');
if (csrfToken == null) {
print("no token,request token firstly...");
//lock the dio.
dio.lock();
return tokenDio.get("/token").then((d) {
options.headers["csrfToken"] = csrfToken = d.data['data']['token'];
print("request token succeed, value: " + d.data['data']['token']);
print(
'continue to perform request:path:${options.path},baseURL:${options.path}');
return options;
}).whenComplete(() => dio.unlock()); // unlock the dio
} else {
options.headers["csrfToken"] = csrfToken;
return options;
}
}
));
完整的示例代碼請點擊 這里.
4.6.6.日志
我們可以添加 LogInterceptor 攔截器來自動打印請求、響應日志, 如:
dio.interceptors.add(LogInterceptor(responseBody: false)); //開啟請求日志
由于攔截器隊列的執行順序是FIFO,如果把log攔截器添加到了最前面,則后面攔截器對
options
的更改就不會被打?。ǖ廊粫В?, 所以建議把log攔截添加到隊尾。
4.7.Cookie管理
我們可以通過添加CookieManager
攔截器來自動管理請求/響應 cookie。CookieManager
依賴 cookieJar package
:
dio cookie 管理 API 是基于開源庫 cookie_jar.
你可以創建一個CookieJar
或 PersistCookieJar
來幫您自動管理cookie, dio 默認使用 CookieJar
, 它會將cookie保存在內存中。 如果您想對cookie進行持久化, 請使用 PersistCookieJar
, 示例代碼如下:
var dio = new Dio();
dio.interceptors.add(CookieManager(CookieJar()))
PersistCookieJar
實現了RFC中標準的cookie策略.PersistCookieJar
會將cookie保存在文件中,所以 cookies 會一直存在除非顯式調用 delete
刪除.
注意: 在Flutter中,傳給
PersistCookieJar
的路徑必須是有效的,必須是設備中存在的路徑并且路徑擁有寫權限,你可以通過 path_provider 包來獲取正確的路徑。
更多關于 cookie_jar 請參考 : https://github.com/flutterchina/cookie_jar .
4.7.1.自定義攔截器
開發者可以通過繼承Interceptor
類來實現自定義攔截器,這是一個簡單的緩存示例攔截器。
4.8.錯誤處理
當請求過程中發生錯誤時, Dio 會包裝 Error
/Exception
為一個 DioError
:
try {
//404
await dio.get("https://wendux.github.io/xsddddd");
} on DioError catch (e) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx and is also not 304.
if (e.response) {
print(e.response.data);
print(e.response.headers);
print(e.response.request);
} else {
// Something happened in setting up or sending the request that triggered an Error
print(e.request);
print(e.message);
}
}
4.8.1.DioError 字段
{
/// 響應信息, 如果錯誤發生在在服務器返回數據之前,它為 `null`
Response response;
/// 錯誤描述.
String message;
/// 錯誤類型,見下文
DioErrorType type;
///原始的error或exception對象,通常type為DEFAULT時存在。
dynamic error;
/// 錯誤棧信息,可能為null
StackTrace stackTrace;
}
4.8.2.DioErrorType
enum DioErrorType {
/// When opening url timeout, it occurs.
CONNECT_TIMEOUT,
/// Whenever more than [receiveTimeout] (in milliseconds) passes between two events from response stream,
/// [Dio] will throw the [DioError] with [DioErrorType.RECEIVE_TIMEOUT].
///
/// Note: This is not the receiving time limitation.
RECEIVE_TIMEOUT,
/// When the server response, but with a incorrect status, such as 404, 503...
RESPONSE,
/// When the request is cancelled, dio will throw a error with this type.
CANCEL,
/// Default error type, Some other Error. In this case, you can
/// read the DioError.error if it is not null.
DEFAULT
}
4.9.使用application/x-www-form-urlencoded編碼
默認情況下, Dio 會將請求數據(除過String類型)序列化為 JSON
. 如果想要以 application/x-www-form-urlencoded
格式編碼, 你可以顯式設置contentType
:
//Instance level
dio.options.contentType=ContentType.parse("application/x-www-form-urlencoded");
//or works once
dio.post("/info",data:{"id":5}, options: new Options(contentType:ContentType.parse("application/x-www-form-urlencoded")));
這里有一個示例.
4.10.FormData
Dio支持發送 FormData, 請求數據將會以 multipart/form-data
方式編碼, FormData中可以一個或多個包含文件 .
FormData formData = new FormData.from({
"name": "wendux",
"age": 25,
"file": new UploadFileInfo(new File("./example/upload.txt"), "upload.txt")
});
response = await dio.post("/info", data: formData);
注意: 只有 post 方法支持發送 FormData.
這里有一個完整的示例.
4.11.轉換器
轉換器Transformer
用于對請求數據和響應數據進行編解碼處理。Dio實現了一個默認轉換器DefaultTransformer
作為默認的 Transformer
. 如果你想對請求/響應數據進行自定義編解碼處理,可以提供自定義轉換器,通過 dio.transformer
設置。
請求轉換器 Transformer.transformRequest(...) 只會被用于 'PUT'、 'POST'、 'PATCH'方法,因為只有這些方法才可以攜帶請求體(request body)。但是響應轉換器 Transformer.transformResponse() 會被用于所有請求方法的返回數據。
4.11.1.Flutter中設置
如果你在開發Flutter應用,強烈建議json的解碼通過compute
方法在后臺進行,這樣可以避免在解析復雜json時導致的UI卡頓。
// 必須是頂層函數
_parseAndDecode(String response) {
return jsonDecode(response);
}
parseJson(String text) {
return compute(_parseAndDecode, text);
}
void main() {
...
// 自定義 jsonDecodeCallback
(dio.transformer as DefaultTransformer).jsonDecodeCallback = parseJson;
runApp(MyApp());
}
4.11.2.其它示例
這里有一個 自定義Transformer的示例.
4.11.3.執行流
雖然在攔截器中也可以對數據進行預處理,但是轉換器主要職責是對請求/響應數據進行編解碼,之所以將轉化器單獨分離,一是為了和攔截器解耦,二是為了不修改原始請求數據(如果你在攔截器中修改請求數據(options.data
),會覆蓋原始請求數據,而在某些時候您可能需要原始請求數據). Dio的請求流是:
請求攔截器 >> 請求轉換器 >> 發起請求 >> 響應轉換器 >> 響應攔截器 >> 最終結果。
這是一個自定義轉換器的示例.
4.12.HttpClientAdapter
HttpClientAdapter
是 Dio 和 HttpClient
之間的橋梁。2.0抽象出adapter主要是方便切換、定制底層網絡庫。Dio實現了一套標準的、強大API,而HttpClient
則是真正發起Http請求的對象。我們通過HttpClientAdapter
將Dio和HttpClient
解耦,這樣一來便可以自由定制Http請求的底層實現,比如,在Flutter中我們可以通過自定義HttpClientAdapter
將Http請求轉發到Native中,然后再由Native統一發起請求。再比如,假如有一天OKHttp
提供了dart版,你想使用OKHttp
發起http請求,那么你便可以通過適配器來無縫切換到OKHttp
,而不用改之前的代碼。
Dio 使用DefaultHttpClientAdapter
作為其默認HttpClientAdapter,DefaultHttpClientAdapter
使用dart:io:HttpClient
來發起網絡請求。
這里 有一個簡單的自定義Adapter的示例,讀者可以參考。另外本項目的自動化測試用例全都是通過一個自定義的MockAdapter來模擬服務器返回數據的。
4.12.1.設置Http代理
DefaultHttpClientAdapter
提供了一個onHttpClientCreate
回調來設置底層 HttpClient
的代理,我們想使用代理,可以參考下面代碼:
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
// config the http client
client.findProxy = (uri) {
//proxy all request to localhost:8888
return "PROXY localhost:8888";
};
// you can also create a new HttpClient to dio
// return new HttpClient();
};
完整的示例請查看這里.
4.12.2.Https證書校驗
有兩種方法可以校驗https證書,假設我們的后臺服務使用的是自簽名證書,證書格式是PEM格式,我們將證書的內容保存在本地字符串中,那么我們的校驗邏輯如下:
String PEM = "XXXXX"; // certificate content
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.badCertificateCallback=(X509Certificate cert, String host, int port){
if(cert.pem==PEM){ // Verify the certificate
return true;
}
return false;
};
};
X509Certificate
是證書的標準格式,包含了證書除私鑰外所有信息,讀者可以自行查閱文檔。另外,上面的示例沒有校驗host,是因為只要服務器返回的證書內容和本地的保存一致就已經能證明是我們的服務器了(而不是中間人),host驗證通常是為了防止證書和域名不匹配。
對于自簽名的證書,我們也可以將其添加到本地證書信任鏈中,這樣證書驗證時就會自動通過,而不會再走到badCertificateCallback
回調中:
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
SecurityContext sc = new SecurityContext();
//file is the path of certificate
sc.setTrustedCertificates(file);
HttpClient httpClient = new HttpClient(context: sc);
return httpClient;
};
注意,通過setTrustedCertificates()
設置的證書格式必須為PEM或PKCS12,如果證書格式為PKCS12,則需將證書密碼傳入,這樣則會在代碼中暴露證書密碼,所以客戶端證書校驗不建議使用PKCS12格式的證書。
4.13.請求取消
你可以通過 cancel token 來取消發起的請求:
CancelToken token = new CancelToken();
dio.get(url, cancelToken: token)
.catchError((DioError err){
if (CancelToken.isCancel(err)) {
print('Request canceled! '+ err.message)
}else{
// handle error.
}
});
// cancel the requests with "cancelled" message.
token.cancel("cancelled");
注意: 同一個cancel token 可以用于多個請求,當一個cancel token取消時,所有使用該cancel token的請求都會被取消。
完整的示例請參考取消示例.
參考資料:
https://pub.dev/packages/http
https://pub.dev/packages/dio