# 封装请求
在继续后续的功能开发之前,短信是需要发送请求的,所以,需要考虑一个请求工具类的封装。
需求分析:
要考虑第三方包有没有类似于axios,对RESTful类型的请求进行支持的
- 拦截器
- 请求取消
- FormData,文件上传/下载功能
- ...
# Dio库介绍
特点:
- 拦截器 (opens new window)
- Cookie管理 (opens new window)
- 错误处理 (opens new window)
- 使用application/x-www-form-urlencoded编码 (opens new window)
- FormData (opens new window)
- 转换器 (opens new window)
- HttpClientAdapter (opens new window)
- 设置Http代理 (opens new window)
- Https证书校验 (opens new window)
- Http2支持 (opens new window)
- 请求取消 (opens new window)
- 继承 Dio class (opens new window)
官方提供了大量的示例,而且使用的人数非常多:
# 基础封装
安装依赖:
dependencies:
dio: ^4.0.4
使用命令flutter pub get
安装最新的包,说明,课程中使用的是4.0.0
的版本,但是由于该版本有BUG,后面会有介绍到,大家可以尝试一下最新的版本。
# 需求分析
与前端项目一样,需要创建单例(Get_it,工厂函数来创建 (opens new window))以便后续发送请求可以使用同一个实例。
而且,方便后续进行断线重连或者处理401的RefreshToken的逻辑。
# 工厂函数
class Logger {
final String name;
bool mute = false;
// _cache is library-private, thanks to
// the _ in front of its name.
static final Map<String, Logger> _cache =
<String, Logger>{};
factory Logger(String name) {
return _cache.putIfAbsent(
name, () => Logger._internal(name));
}
factory Logger.fromJson(Map<String, Object> json) {
return Logger(json['name'].toString());
}
Logger._internal(this.name);
void log(String msg) {
if (!mute) print(msg);
}
}
对比两个实例:
var logger = Logger('UI');
logger.log('Button clicked');
var logMap = {'name': 'UI'};
var loggerJson = Logger.fromJson(logMap);
// 使用print查验 是否为同一个实例
print(identical(loggerJson, logMap))
# 创建工具类及配置
创建utils/request/dio_http.dart
,代码:
// Dio -> http客户端
// 如何在Dart中使用单例 -> 1.Get_it 2.factory
import 'package:dio_http/dio_http.dart';
import 'package:my_app/config.dart';
class DioHttp {
Dio _client = Dio();
Dio get client => _client;
// 1.static -> 作用:让多个相同类型的类共享同一个成员变量
// 2.构造函数:匿名,命名式 -> ._(名称) -> factory
static DioHttp _instance = DioHttp._internal();
factory DioHttp() => _instance;
DioHttp._internal() {
// 对dio进行实例化
_client.options = _client.options.copyWith(
baseUrl: Config.baseUrl,
connectTimeout: 1000 * 10,
receiveTimeout: 1000 * 5,
);
// 初始化_client上的拦截器 -> 添加多个
_client.interceptors
..addAll([
// todo
]);
}
// DioHttp().公有的方法 or 属性
}
创建config.dart
:
class Config {
static const String baseUrl = 'http://localhost:3000';
}
使用的方式:
DioHttp().client // 然后在clinet上有RESTful的方法
// 例如:
DioHttp().client.get
# 常见RESTful方法
# GET/POST/PUT/DELETE
问题:如何知道需要取用的参数?如何确定哪些可用的参数?
可以借助着dart优秀的类型的推导功能来进行选择需要的参数及设置类型。
修改utils/request/dio_http.dart
,代码:
// DioHttp().公有的方法 or 属性
// init -> baseUrl headers
void init(
{String? baseUrl,
int? connectTimeout,
int? receiveTimeout,
List<Interceptor>? interceptors}) {
_client.options = _client.options.copyWith(
baseUrl: baseUrl,
connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout,
);
_client.interceptors..addAll(interceptors!);
}
// RESTful -> get post delete put
// axios -> axios.get(path, params, {headers})
Future<Response<Map<String, dynamic>>> get(String path,
[Map<String, dynamic>? params, Options? options]) async {
return await _client.get(
path,
queryParameters: params,
options: options,
);
}
Future<Response<Map<String, dynamic>>> post(String path,
[Map<String, dynamic>? data, Options? options]) async {
return await _client.post(
path,
data: data,
options: options,
);
}
Future<Response<Map<String, dynamic>>> delete(String path,
[Map<String, dynamic>? data, Options? options]) async {
return await _client.delete(
path,
data: data,
options: options,
);
}
Future<Response<Map<String, dynamic>>> put(String path,
[Map<String, dynamic>? data, Options? options]) async {
return await _client.put(
path,
data: data,
options: options,
);
}
添加了如上的代码之后,可以不用使用client属性才去使用RESTful方法了,可以直接使用DioHttp().get
,等。
# 上传文件
上传文件可以参考官方的代码:
// 定义DioFile
class DioFile {
final String path;
final String filename;
DioFile({required this.path, required this.filename});
}
// ...
// formData -> 上传文件
Future<FormData> _prepareFormData(String path, String filename) async {
return FormData.fromMap({
'file': await MultipartFile.fromFile(
path,
filename: filename,
),
});
}
// 进度条
void _showProgress(received, total) {
if (total != -1) {
print((received / total * 100).toStringAsFixed(0) + '%');
}
}
Future<Response<Map<String, dynamic>>> postFormData(String path,
DioFile dioFile, void Function(int, int)? onSendProgress) async {
return await _client.post(
path,
data: await _prepareFormData(dioFile.path, dioFile.filename),
onSendProgress: onSendProgress ?? _showProgress,
);
}
# 下载文件
可以参考官方的代码:
// 下载文件
// e.g. 示例代码:https://github.com/flutterchina/dio/blob/master/example/download_with_trunks.dart
// await downloadWithChunks(url, savePath, onReceiveProgress: (received, total) {
// if (total != -1) {
// print('${(received / total * 100).floor()}%');
// }
// });
/// Downloading by spiting as file in chunks
Future downloadWithChunks(
url,
savePath, {
ProgressCallback? onReceiveProgress,
}) async {
const firstChunkSize = 102;
const maxChunk = 3;
var total = 0;
var dio = Dio();
var progress = <int>[];
void Function(int, int) createCallback(no) {
return (int received, int _) {
progress[no] = received;
if (onReceiveProgress != null && total != 0) {
onReceiveProgress(progress.reduce((a, b) => a + b), total);
}
};
}
Future<Response> downloadChunk(url, start, end, no) async {
progress.add(0);
--end;
return dio.download(
url,
savePath + 'temp$no',
onReceiveProgress: createCallback(no),
options: Options(
headers: {'range': 'bytes=$start-$end'},
),
);
}
Future mergeTempFiles(chunk) async {
var f = File(savePath + 'temp0');
var ioSink = f.openWrite(mode: FileMode.writeOnlyAppend);
for (var i = 1; i < chunk; ++i) {
var _f = File(savePath + 'temp$i');
await ioSink.addStream(_f.openRead());
await _f.delete();
}
await ioSink.close();
await f.rename(savePath);
}
var response = await downloadChunk(url, 0, firstChunkSize, 0);
if (response.statusCode == 206) {
total = int.parse(response.headers
.value(HttpHeaders.contentRangeHeader)!
.split('/')
.last);
var reserved = total -
int.parse(response.headers.value(Headers.contentLengthHeader)!);
var chunk = (reserved / firstChunkSize).ceil() + 1;
if (chunk > 1) {
var chunkSize = firstChunkSize;
if (chunk > maxChunk + 1) {
chunk = maxChunk + 1;
chunkSize = (reserved / maxChunk).ceil();
}
var futures = <Future>[];
for (var i = 0; i < maxChunk; ++i) {
var start = firstChunkSize + i * chunkSize;
futures.add(downloadChunk(url, start, start + chunkSize, i + 1));
}
await Future.wait(futures);
}
await mergeTempFiles(chunk);
}
}
# 拦截器
# 自定义拦截器
首先要了解如何创建dio的拦截器,官方的文档:
dio.interceptors.add(InterceptorsWrapper(
onRequest:(options, handler){
// Do something before request is sent
return handler.next(options); //continue
// 如果你想完成请求并返回一些自定义数据,你可以resolve一个Response对象 `handler.resolve(response)`。
// 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义response.
//
// 如果你想终止请求并触发一个错误,你可以返回一个`DioError`对象,如`handler.reject(error)`,
// 这样请求将被中止并触发异常,上层catchError会被调用。
},
onResponse:(response,handler) {
// Do something with response data
return handler.next(response); // continue
// 如果你想终止请求并触发一个错误,你可以 reject 一个`DioError`对象,如`handler.reject(error)`,
// 这样请求将被中止并触发异常,上层catchError会被调用。
},
onError: (DioError e, handler) {
// Do something with response error
return handler.next(e);//continue
// 如果你想完成请求并返回一些自定义数据,可以resolve 一个`Response`,如`handler.resolve(response)`。
// 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义response.
}
));
官方的示例:
import 'package:dio/dio.dart';
class CustomInterceptors extends Interceptor {
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
print('REQUEST[${options.method}] => PATH: ${options.path}');
return super.onRequest(options, handler);
}
Future onResponse(Response response, ResponseInterceptorHandler handler) {
print('RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions?.path}');
return super.onResponse(response, handler);
}
Future onError(DioError err, ErrorInterceptorHandler handler) {
print('ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions?.path}');
return super.onError(err, handler);
}
}
接下来了解到如何创建拦截器之后,就是自定义统一错误处理的拦截器了。
# 统一错误处理
与前端axios是一样的,可以在一个统一的位置对所有的请求异常进行处理,包括4xx/5xx的错误以及RefreshToken的逻辑。
定义错误的类型utils/request/http_exception.dart
,用于扩展基础的HttpException:
// 定义一个基类 -> HttpException
class HttpException implements Exception {
final String? _message;
final int? _code;
// 接收的可选参数 有位置的限制
// 传一个参数 -> message
// 传两个参数 -> message code
// 传参不需要设置属性名
HttpException([this._message, this._code]);
String get message => _message ?? this.runtimeType.toString();
int get code => _code ?? -1;
}
class BadRequestException extends HttpException {
// message与code需要在设置的时候,传递属性名
// message与code不是必传参数 -> null
BadRequestException({String? message, int? code}) : super(message, code);
}
class BadServiceException extends HttpException {
BadServiceException({String? message, int? code}) : super(message, code);
}
class UnknownException extends HttpException {
UnknownException({String? message}) : super(message);
}
class CancelException extends HttpException {
CancelException({String? message}) : super(message);
}
class NetWorkException extends HttpException {
NetWorkException({String? message, int? code}) : super(message, code);
}
class UnauthorizedException extends HttpException {
UnauthorizedException({String? message}) : super(message, 401);
}
class BadResponseException extends HttpException {
BadResponseException({String? message, int? code}) : super(message, code);
}
主要的作用:更加语义化的编码,方便的设置参数,比如错误message消息、错误code代码。
创建一个统一的错误处理的parse方法utils/request/parse_exception.dart
:
import 'dart:io';
import 'package:dio_http/dio_http.dart';
import 'http_exception.dart';
HttpException parseException(DioError error) {
switch (error.type) {
case DioErrorType.connectTimeout:
case DioErrorType.receiveTimeout:
case DioErrorType.sendTimeout:
return NetWorkException(message: error.message);
case DioErrorType.cancel:
return CancelException(message: error.message);
case DioErrorType.response:
try {
int? errCode = error.response?.statusCode;
switch (errCode) {
case 400:
return BadRequestException(
message: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', code: errCode);
case 401:
return UnauthorizedException(message: '用户没有权限(令牌、用户名、密码错误)。');
case 403:
return BadRequestException(
message: '用户得到授权,但是访问是被禁止的。', code: errCode);
case 404:
return BadRequestException(
message: '发出的请求针对的是不存在的记录,服务器没有进行操作。', code: errCode);
case 405:
return BadRequestException(
message: '请求方法不存在,请检查路由!', code: errCode);
case 406:
return BadRequestException(message: '请求的格式不可得。', code: errCode);
case 410:
return BadRequestException(
message: '请求的资源被永久删除,且不会再得到的。', code: errCode);
case 422:
return BadRequestException(
message: '当创建一个对象时,发生一个验证错误。', code: errCode);
case 500:
return BadServiceException(
message: '服务器发生错误,请检查服务器。', code: errCode);
case 502:
return BadServiceException(message: '网关错误。', code: errCode);
case 503:
return BadServiceException(
message: '服务不可用,服务器暂时过载或维护。', code: errCode);
case 504:
return BadServiceException(message: '网关超时。', code: errCode);
default:
return UnknownException(message: '不支持HTTP协议请求');
}
} on Exception catch (_) {
return UnknownException(message: error.message);
}
case DioErrorType.other:
if (error.error is SocketException) {
return NetWorkException(message: error.message);
} else {
return UnknownException(message: error.message);
}
default:
return UnknownException(message: error.message);
}
}
创建utils/request/dio_error_interceptor.dart
文件:
import 'package:dio_http/dio_http.dart';
import 'package:my_app/utils/request/http_exception.dart';
import 'package:my_app/utils/request/parse_exception.dart';
// 异常的类型
// 网络异常
// 客户端异常
// 服务端异常
class ErrorInterceptor extends Interceptor {
ErrorInterceptor();
void onError(DioError err, ErrorInterceptorHandler handler) {
// print('onError: $err');
// 401 -> refreshToken
HttpException httpException = parseException(err);
// 4xx 5xx -> parseException -> 日志 -> 本地数据库&缓存
// todo 发送错误日志的请求
handler.resolve(Response(
requestOptions: err.requestOptions,
statusCode: httpException.code,
data: {
'code': httpException.code,
'message': httpException.message,
}));
// super.onError(err, handler);
}
}
在dio_http.dart中引用该拦截器:
DioHttp._internal() {
// 对dio进行实例化
_client.options = _client.options.copyWith(
baseUrl: Config.baseUrl,
connectTimeout: 1000 * 10,
receiveTimeout: 1000 * 5,
);
// 初始化_client上的拦截器 -> 添加多个
_client.interceptors
..addAll([
// 统一的错误处理
ErrorInterceptor(),
]);
}
# 取消网络请求
关键逻辑:
// 取消请求
void cancelRequests({CancelToken? cancelToken}) {
cancelToken ?? _cancelToken.cancel('Request Canceled');
cancelToken?.cancel('Request Canceled');
}
这里有一个语法糖:
cancelToken ??
是判断前面的方法是否存在,如果存在,则执行之后的代码。
修改过后的dio_http.dart
文件:
// Dio -> http客户端
// 如何在Dart中使用单例 -> 1.Get_it 2.factory
import 'dart:io';
import 'package:dio_http/dio_http.dart';
import 'package:my_app/config.dart';
import 'dio_error_interceptor.dart';
class DioFile {
final String path;
final String filename;
DioFile({required this.path, required this.filename});
}
class DioHttp {
Dio _client = Dio();
Dio get client => _client;
CancelToken _cancelToken = CancelToken();
// 1.static -> 作用:让多个相同类型的类共享同一个成员变量
// 2.构造函数:匿名,命名式 -> ._(名称) -> factory
static DioHttp _instance = DioHttp._internal();
factory DioHttp() => _instance;
DioHttp._internal() {
// 对dio进行实例化
_client.options = _client.options.copyWith(
baseUrl: Config.baseUrl,
connectTimeout: 1000 * 10,
receiveTimeout: 1000 * 5,
);
// 初始化_client上的拦截器 -> 添加多个
_client.interceptors
..addAll([
// todo
ErrorInterceptor(),
]);
}
// DioHttp().公有的方法 or 属性
// init -> baseUrl headers
void init(
{String? baseUrl,
int? connectTimeout,
int? receiveTimeout,
List<Interceptor>? interceptors}) {
_client.options = _client.options.copyWith(
baseUrl: baseUrl,
connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout,
);
_client.interceptors..addAll(interceptors!);
}
// 取消请求
void cancelRequests({CancelToken? cancelToken}) {
cancelToken ?? _cancelToken.cancel('Request Canceled');
cancelToken?.cancel('Request Canceled');
}
// RESTful -> get post delete put
// axios -> axios.get(path, params, {headers})
Future<Response<Map<String, dynamic>>> get(String path,
[Map<String, dynamic>? params, Options? options, CancelToken? cancelToken,]) async {
return await _client.get(
path,
queryParameters: params,
options: options,
cancelToken: cancelToken ?? _cancelToken,
);
}
Future<Response<Map<String, dynamic>>> post(String path,
[Map<String, dynamic>? data, Options? options, CancelToken? cancelToken,]) async {
return await _client.post(
path,
data: data,
options: options,
cancelToken: cancelToken ?? _cancelToken,
);
}
Future<Response<Map<String, dynamic>>> delete(String path,
[Map<String, dynamic>? data, Options? options, CancelToken? cancelToken,
]) async {
return await _client.delete(
path,
data: data,
options: options,
cancelToken: cancelToken ?? _cancelToken,
);
}
Future<Response<Map<String, dynamic>>> put(String path,
[Map<String, dynamic>? data, Options? options,
CancelToken? cancelToken,
]) async {
return await _client.put(
path,
data: data,
options: options,
cancelToken: cancelToken ?? _cancelToken,
);
}
// formData -> 上传文件
Future<FormData> _prepareFormData(String path, String filename) async {
return FormData.fromMap({
'file': await MultipartFile.fromFile(
path,
filename: filename,
),
});
}
void _showProgress(received, total) {
if (total != -1) {
print((received / total * 100).toStringAsFixed(0) + '%');
}
}
Future<Response<Map<String, dynamic>>> postFormData(String path,
DioFile dioFile, void Function(int, int)? onSendProgress, CancelToken? cancelToken,
) async {
return await _client.post(
path,
data: await _prepareFormData(dioFile.path, dioFile.filename),
onSendProgress: onSendProgress ?? _showProgress,
cancelToken: cancelToken ?? _cancelToken,
);
}
// 下载文件
// e.g. 示例代码:https://github.com/flutterchina/dio/blob/master/example/download_with_trunks.dart
// await downloadWithChunks(url, savePath, onReceiveProgress: (received, total) {
// if (total != -1) {
// print('${(received / total * 100).floor()}%');
// }
// });
/// Downloading by spiting as file in chunks
Future downloadWithChunks(
url,
savePath, {
ProgressCallback? onReceiveProgress,
CancelToken? cancelToken,
}) async {
const firstChunkSize = 102;
const maxChunk = 3;
var total = 0;
var dio = Dio();
var progress = <int>[];
void Function(int, int) createCallback(no) {
return (int received, int _) {
progress[no] = received;
if (onReceiveProgress != null && total != 0) {
onReceiveProgress(progress.reduce((a, b) => a + b), total);
}
};
}
Future<Response> downloadChunk(url, start, end, no) async {
progress.add(0);
--end;
return dio.download(
url,
savePath + 'temp$no',
onReceiveProgress: createCallback(no),
options: Options(
headers: {'range': 'bytes=$start-$end'},
),
cancelToken: cancelToken ?? _cancelToken,
);
}
Future mergeTempFiles(chunk) async {
var f = File(savePath + 'temp0');
var ioSink = f.openWrite(mode: FileMode.writeOnlyAppend);
for (var i = 1; i < chunk; ++i) {
var _f = File(savePath + 'temp$i');
await ioSink.addStream(_f.openRead());
await _f.delete();
}
await ioSink.close();
await f.rename(savePath);
}
var response = await downloadChunk(url, 0, firstChunkSize, 0);
if (response.statusCode == 206) {
total = int.parse(response.headers
.value(HttpHeaders.contentRangeHeader)!
.split('/')
.last);
var reserved = total -
int.parse(response.headers.value(Headers.contentLengthHeader)!);
var chunk = (reserved / firstChunkSize).ceil() + 1;
if (chunk > 1) {
var chunkSize = firstChunkSize;
if (chunk > maxChunk + 1) {
chunk = maxChunk + 1;
chunkSize = (reserved / maxChunk).ceil();
}
var futures = <Future>[];
for (var i = 0; i < maxChunk; ++i) {
var start = firstChunkSize + i * chunkSize;
futures.add(downloadChunk(url, start, start + chunkSize, i + 1));
}
await Future.wait(futures);
}
await mergeTempFiles(chunk);
}
}
}
调用cancel方法:
DioHttp().cancelRequest() // 即可取消当前请求
取消之后会收到一个Request被取消的Error,会交由统一的错误处理:
特别说明:
Dio取消请求与其他比如axios取消请求是一样的,只是取消了前端接收的数据,但是只要是该请求被服务端接收到,服务端该处理的还是会处理,只是返回的数据被丢弃掉了。
一般在发送请求之前,会在Button上加上延迟处理,这样可以防止用户频繁重复的请求。
# 请求日志
在Dio中有一个官方的拦截器:
修改dio_http.dart
文件:
DioHttp._internal() {
// 对dio进行实例化
_client.options = _client.options.copyWith(
baseUrl: Config.baseUrl,
connectTimeout: 1000 * 10,
receiveTimeout: 1000 * 5,
);
// 初始化_client上的拦截器 -> 添加多个
_client.interceptors
..addAll([
// todo
ErrorInterceptor(),
// 关键是这里需要判断一下是否是开发&生产环境
if(Config.env == Env.Dev)LogInterceptor(responseBody: true),
]);
}
配置一个环境变量:
enum Env { Dev, Prod }
class Config {
static const String baseUrl = 'http://localhost:3000';
static const Env env = Env.Dev;
}
方便区别测试&生产环境。效果: