Flutter 中 Dio 拦截器

9,692 阅读6分钟

原文地址:medium.com/flutter-com…

有时我们不需要复杂的应用程序,只需要显示一个数据列表的简单功能,我们可以通过一个简单的方法来实现:

Future<Response> fetchItems() {
 return Dio().("https://some-website.com/listing");
}

没有任何错误,也不用用log打印任何响应信息。

但是事实是,几乎没有这么简单的应用程序,有时我们需要做更多用于从服务器获取响应或调试我们的应用程序,比如:

  1. 将动态的Header发送给服务器,比如存储在SharedPreferences中的值。
  2. 检查每个响应头并保存他的值。
  3. 验证服务器返回的错误并将他们映射到我们应用程序中能够处理的错误类中。
  4. 另外,我们希望为我们的应用添加简单的缓存功能,以便在连接超时或用户无法访问网络时,可以显示该请求的缓存响应;
  5. 我们也可以添加log日志来打印我们的请求和响应信息。

Interceptors拦截器将通过为我们提供错误,请求和响应的特定回调来帮助我们处理这些问题。

在深入之前,我们先来看一下如何配置Dio。

Dio 配置

可以通过BaseOption来配置Dio,该对象允许我们设置一些参数来初始化Dio实例: connectTimeout,receiveTimeout和baseUrl,他们将用于每个Api的调用。

Dio createDio() {
  return Dio(
    BaseOptions(
      connectTimeout: 5000,
      receiveTimeout: 5000,
      baseUrl: "https://some-website.com"
    )
  );
}

但是在基本配置中无法添加拦截器,我们需要在创建dio实例的时候在拦截器集合中添加我们需要的拦截器:

Dio addInterceptors(Dio dio) {
  return dio..interceptors.add(InterceptorsWrapper(
      onRequest: (RequestOptions options) => requestInterceptor(options),
      onResponse: (Response response) => responseInterceptor(response),
      onError: (DioError dioError) => errorInterceptor(dioError)));
}

我们已经设置了一个dio实例,可以用于任何一个Api调用。

添加动态Headers

假如我们需要将SharedPreferences中存储的一个值作为header传给服务器,我们不能使用BaseOption中的extra字段,该字段用于访问在创建请求时设置静态数据。

InterceptorsWrapper给我们提供了一个RequestOptions对象,有如下一些属性:

  1. Request dynamic data
  2. Url String path
  3. Query Parameters Map<String, dynamic> queryParameters

有了这些信息,我们就可以实现我们自己的requestInterceptor方法了。

此方法返回一个动态类型,可以是:

  1. RequestOptions对象,如果我们想继续请求的话
  2. Response对象,如果应用程序想自己处理请求
  3. DioErrordio.reject对象,将会抛出一个异常

这将使我们可以灵活的验证每个请求,添加数据以及在必要时抛出错误,我们使用起来只需要添加一些数据,然后处理返回的数据就可以了。

dynamic requestInterceptor(RequestOptions options) async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  var token = prefs.get("token");

  options.headers.addAll({"Token": "$token${DateTime.now()}"});

	return options;
}

我们还可以在创建endpoints时,为不同的请求添加是否需要token的header:

Future<Response> getListOfTodos() {
  return dio.get("/todos/1", options: Options(headers: {"requiresToken" : true}));
}

然后在拦截器中进行统一处理:

dynamic requestInterceptor(RequestOptions options) async {
  if (options.headers.containsKey("requiresToken")) {
    //remove the auxiliary header
    options.headers.remove("requiresToken");

    SharedPreferences prefs = await SharedPreferences.getInstance();
    var header = prefs.get("Header");

    options.headers.addAll({"Header": "$header${DateTime.now()}"});
    
    return options;
  }
}

验证 Response

Response中有Headers,还有Status Code以及响应数据。我们将响应数据类型设置为动态类型。他可以是:

  1. 如果我们想继续请求就是Response
  2. 如果我们在验证数据响应之后抛出错误,就是DioError

意味着,如果Header中的isUserActive值为false,那么就返回一个DioError.

dynamic responseInterceptor(Response options) async {
  if (options.headers.value("verifyToken") != null) {
    //if the header is present, then compare it with the Shared Prefs key
    SharedPreferences prefs = await SharedPreferences.getInstance();
    var verifyToken = prefs.get("VerifyToken");
    
    // if the value is the same as the header, continue with the request
    if (options.headers.value("verifyToken") == verifyToken) {
      return options;
    }
  }

  return DioError(request: options.request, message: "User is no longer active");
}

验证服务器错误

假设服务器中有删除用户账户的机制,那么一旦账户删除了之后,就会收到一个错误信息,从而界面用户界面将会跳转到创建用户的界面,服务器将返回该{"error":"ERROR_001"}错误信息,我们需要创建一个拦截器用来统一处理这些错误信息。

错误拦截器的类型也是动态的:

  1. 如果我们想继续发送错误请求,我们将返回DioError对象
  2. 如果我们要解析请求并返回Response对象,这种情况下,应用程序并不知道是服务器错误,将会继续正常请求。
dynamic errorInterceptor(DioError dioError) {
  if (dioError.message.contains("ERROR_001")) {
    // this will push a new route and remove all the routes that were present
    navigatorKey.currentState.pushNamedAndRemoveUntil(
        "/login", (Route<dynamic> route) => false);
  }
  
  return dioError;
}

扩展拦截器类

我们可以创建一个拦截器类来覆盖onRequestonErroronResponse方法。

class AppInterceptors extends Interceptor {
  @override
  FutureOr<dynamic> onRequest(RequestOptions options) async {
    if (options.headers.containsKey("requiresToken")) {
      //remove the auxiliary header
      options.headers.remove("requiresToken");

      SharedPreferences prefs = await SharedPreferences.getInstance();
      var header = prefs.get("Header");

      options.headers.addAll({"Header": "$header${DateTime.now()}"});

      return options;
    }
  }

  @override
  FutureOr<dynamic> onError(DioError dioError) {
    if (dioError.message.contains("ERROR_001")) {
      // this will push a new route and remove all the routes that were present
      navigatorKey.currentState.pushNamedAndRemoveUntil(
          "/login", (Route<dynamic> route) => false);
    }

    return dioError;
  }

  @override
  FutureOr<dynamic> onResponse(Response options) async {
    if (options.headers.value("verifyToken") != null) {
      //if the header is present, then compare it with the Shared Prefs key
      SharedPreferences prefs = await SharedPreferences.getInstance();
      var verifyToken = prefs.get("VerifyToken");

      // if the value is the same as the header, continue with the request
      if (options.headers.value("verifyToken") == verifyToken) {
        return options;
      }
    }

    return DioError(request: options.request, message: "User is no longer active");
  }
}

该类可以被轻松的添加到dio对象的拦截器集合中:

Dio addInterceptors(Dio dio) {
  dio.interceptors.add(AppInterceptors());
}

创建一个简单的缓存

import 'package:dio/dio.dart';

class CacheInterceptor extends Interceptor {
  CacheInterceptor();

  var _cache = new Map<Uri, Response>();

  @override
  onRequest(RequestOptions options) {
    return options;
  }

  @override
  onResponse(Response response) {
    _cache[response.request.uri] = response;
  }

  @override
  onError(DioError e) {
    print('onError: $e');
    if (e.type == DioErrorType.CONNECT_TIMEOUT || e.type == DioErrorType.DEFAULT) {
      var cachedResponse = _cache[e.request.uri];
      if (cachedResponse != null) {
        return cachedResponse;
      }
    }
    return e;
  }
}

之后可以通过添加数据库缓存当方式来改进该缓存拦截器。

记录所有Dio相关的内容

在请求中,我们想通过控制台打印所有的请求信息以及相应信息,以方便我们调试请求中的问题。

和以前一样,我们创建一个新的Interceptor类,实现所有必要的方法,并从请求中记录我们想要的所有信息,此外,我们希望我们的人日志打印的是格式化好的信息:

# REQUEST:
--> GET https://jsonplaceholder.typicode.com/todos/1
...
--> END GET
# RESPONSE:
<-- 200 https://jsonplaceholder.typicode.com/todos/1
...
<-- END HTTP
  1. 对于Request,我们希望打印请求参数、请求体、headers、URL
  2. 对于Response,我们要打印出URL、Headers、body和状态码
  3. 对于错误,我们需要打印状态码和错误本身
class LoggingInterceptors extends Interceptor {
  @override
  FutureOr<dynamic> onRequest(RequestOptions options) {
    print(
        "--> ${options.method != null ? options.method.toUpperCase() : 'METHOD'} ${"" + (options.baseUrl ?? "") + (options.path ?? "")}");
    print("Headers:");
    options.headers.forEach((k, v) => print('$k: $v'));
    if (options.queryParameters != null) {
      print("queryParameters:");
      options.queryParameters.forEach((k, v) => print('$k: $v'));
    }
    if (options.data != null) {
      print("Body: ${options.data}");
    }
    print(
        "--> END ${options.method != null ? options.method.toUpperCase() : 'METHOD'}");

    return options;
  }

  @override
  FutureOr<dynamic> onError(DioError dioError) {
    print(
        "<-- ${dioError.message} ${(dioError.response?.request != null ? (dioError.response.request.baseUrl + dioError.response.request.path) : 'URL')}");
    print(
        "${dioError.response != null ? dioError.response.data : 'Unknown Error'}");
    print("<-- End error");
  }

  @override
  FutureOr<dynamic> onResponse(Response response) {
    print(
        "<-- ${response.statusCode} ${(response.request != null ? (response.request.baseUrl + response.request.path) : 'URL')}");
    print("Headers:");
    response.headers?.forEach((k, v) => print('$k: $v'));
    print("Response: ${response.data}");
    print("<-- END HTTP");
  }
}

使用JsonPlaceHolder测试得到如下输出:

—> GET https:///jsonplaceholder.typicode.com/todos/1/
Headers:
requiresToken: true
queryParameters:
—> END GET
<— 200 https:///jsonplaceholder.typicode.com/todos/1/
Headers:
connection: [keep-alive]
set-cookie: [__cfduid=dd3fb888c5f062dd954e06e2e4c1166241567679659; expires=Fri, 04-Sep-20 10:34:19 GMT; path=/; domain=.typicode.com; HttpOnly]
cache-control: [public, max-age=14400]
transfer-encoding: [chunked]
date: [Thu, 05 Sep 2019 10:34:19 GMT]
content-encoding: [gzip]
vary: [Origin, Accept-Encoding]
age: [4045]
cf-cache-status: [HIT]
expect-ct: [max-age=604800, report-uri=“https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct”]
content-type: [application/json; charset=utf-8]
pragma: [no-cache]
server: [cloudflare]
x-powered-by: [Express]
access-control-allow-credentials: [true]
cf-ray: [51178cd29f9b724b-AMS]
etag: [W/“53-hfEnumeNh6YirfjyjaujcOPPT+s”]
via: [1.1 vegur]
x-content-type-options: [nosniff]
expires: [Thu, 05 Sep 2019 14:34:19 GMT]
Response: {userId: 1, id: 1, title: delectus aut autem, completed: false}
<— END HTTP

我们目前使用的是print方法打印的日志,这样如果我们在生产环境中使用就不太合适,任何使用该App的用户只要用手机连接电脑,然后通过flutter log命令就可以看到这些所有的输出,我们可以结合debugPrintProduct Flavors一起使用,请参考debugPrint and the power of hiding and customizing your logs in Dart