文章系列
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
. 如果想对申请 / 响应数据进行自定义编解码解决,能够提供自定义转换器
为什么须要转换器?
咱们看了转换器的介绍,发现和拦截器的性能差不多,那为什么还要存在转换器,有两点:
- 和拦截器解耦
- 不批改原始申请数据
执行流程:申请拦截器 >> 申请转换器 >> 发动申请 >> 响应转换器 >> 响应拦截器 >> 最终后果。
申请转换器
只会被用于 ‘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 发送给服务器。
原理
- 客户端发送一个申请 (http 申请 + 用户认证信息) 到服务器
- 认证胜利,服务器发送一个 HttpResponse 响应到客户端,其中蕴含 Set-Cookie 的头部
- 客户端提取并保留 cookie 于内存或磁盘
- 再次申请时,HttpRequest 申请中会蕴含一个 已认证的 Cookie 的头部
- 服务器解析 cookie,获取 cookie 中客户端的相干信息
- 服务器返回响应数据
应用
cookie
的应用须要用到两个第三方组件 dio_cookie_manager
和 cookie_jar
- cookie_jar:
Dart
中http
申请的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();