Flutter實(shí)戰(zhàn) 網(wǎng)絡(luò)請(qǐng)求封裝

2021-03-09 15:13 更新

本節(jié)我們會(huì)基于前面介紹過的 dio 網(wǎng)絡(luò)庫(kù)封裝 APP 中用到的網(wǎng)絡(luò)請(qǐng)求接口,并同時(shí)應(yīng)用一個(gè)簡(jiǎn)單的緩存策略。下面我們先介紹一下網(wǎng)絡(luò)接口緩存原理,然后再封裝 APP 的業(yè)務(wù)請(qǐng)求接口。

#15.5.1 網(wǎng)絡(luò)接口緩存

由于在國(guó)內(nèi)訪問 Github 服務(wù)器速度較慢,所以我們應(yīng)用一些簡(jiǎn)單的緩存策略:將請(qǐng)求的 url 作為 key,對(duì)請(qǐng)求的返回值在一個(gè)指定時(shí)間段類進(jìn)行緩存,另外設(shè)置一個(gè)最大緩存數(shù),當(dāng)超過最大緩存數(shù)后移除最早的一條緩存。但是也得提供一種針對(duì)特定接口或請(qǐng)求決定是否啟用緩存的機(jī)制,這種機(jī)制可以指定哪些接口或那次請(qǐng)求不應(yīng)用緩存,這種機(jī)制是很有必要的,比如登錄接口就不應(yīng)該緩存,又比如用戶在下拉刷新時(shí)就不應(yīng)該再應(yīng)用緩存。在實(shí)現(xiàn)緩存之前我們先定義保存緩存信息的CacheObject類:

class CacheObject {
  CacheObject(this.response)
      : timeStamp = DateTime.now().millisecondsSinceEpoch;
  Response response;
  int timeStamp; // 緩存創(chuàng)建時(shí)間


  @override
  bool operator ==(other) {
    return response.hashCode == other.hashCode;
  }


  //將請(qǐng)求uri作為緩存的key
  @override
  int get hashCode => response.realUri.hashCode;
}

接下來我們需要實(shí)現(xiàn)具體的緩存策略,由于我們使用的是 dio package,所以我們可以直接通過攔截器來實(shí)現(xiàn)緩存策略:

import 'dart:collection';
import 'package:dio/dio.dart';
import '../index.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 NetCache extends Interceptor {
  // 為確保迭代器順序和對(duì)象插入時(shí)間一致順序一致,我們使用LinkedHashMap
  var cache = LinkedHashMap<String, CacheObject>();


  @override
  onRequest(RequestOptions options) async {
    if (!Global.profile.cache.enable) return options;
    // refresh標(biāo)記是否是"下拉刷新"
    bool refresh = options.extra["refresh"] == true;
    //如果是下拉刷新,先刪除相關(guān)緩存
    if (refresh) {
      if (options.extra["list"] == true) {
        //若是列表,則只要url中包含當(dāng)前path的緩存全部刪除(簡(jiǎn)單實(shí)現(xiàn),并不精準(zhǔn))
        cache.removeWhere((key, v) => key.contains(options.path));
      } else {
        // 如果不是列表,則只刪除uri相同的緩存
        delete(options.uri.toString());
      }
      return options;
    }
    if (options.extra["noCache"] != true &&
        options.method.toLowerCase() == 'get') {
      String key = options.extra["cacheKey"] ?? options.uri.toString();
      var ob = cache[key];
      if (ob != null) {
        //若緩存未過期,則返回緩存內(nèi)容
        if ((DateTime.now().millisecondsSinceEpoch - ob.timeStamp) / 1000 <
            Global.profile.cache.maxAge) {
          return cache[key].response;
        } else {
          //若已過期則刪除緩存,繼續(xù)向服務(wù)器請(qǐng)求
          cache.remove(key);
        }
      }
    }
  }


  @override
  onError(DioError err) async {
    // 錯(cuò)誤狀態(tài)不緩存
  }


  @override
  onResponse(Response response) async {
    // 如果啟用緩存,將返回結(jié)果保存到緩存
    if (Global.profile.cache.enable) {
      _saveCache(response);
    }
  }


  _saveCache(Response object) {
    RequestOptions options = object.request;
    if (options.extra["noCache"] != true &&
        options.method.toLowerCase() == "get") {
      // 如果緩存數(shù)量超過最大數(shù)量限制,則先移除最早的一條記錄
      if (cache.length == Global.profile.cache.maxCount) {
        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);
  }
}

關(guān)于代碼的解釋都在注釋中了,在此需要說明的是 dio 包的option.extra是專門用于擴(kuò)展請(qǐng)求參數(shù)的,我們通過定義了“refresh”和“noCache”兩個(gè)參數(shù)實(shí)現(xiàn)了“針對(duì)特定接口或請(qǐng)求決定是否啟用緩存的機(jī)制”,這兩個(gè)參數(shù)含義如下:

參數(shù)名 類型 解釋
refresh bool 如果為 true,則本次請(qǐng)求不使用緩存,但新的請(qǐng)求結(jié)果依然會(huì)被緩存
noCache bool 本次請(qǐng)求禁用緩存,請(qǐng)求結(jié)果也不會(huì)被緩存。

#15.5.2 封裝網(wǎng)絡(luò)請(qǐng)求

一個(gè)完整的 APP,可能會(huì)涉及很多網(wǎng)絡(luò)請(qǐng)求,為了便于管理、收斂請(qǐng)求入口,工程上最好的作法就是將所有網(wǎng)絡(luò)請(qǐng)求放到同一個(gè)源碼文件中。由于我們的接口都是請(qǐng)求的 Github 開發(fā)平臺(tái)提供的 API,所以我們定義一個(gè) Git 類,專門用于 Github API 接口調(diào)用。另外,在調(diào)試過程中,我們通常需要一些工具來查看網(wǎng)絡(luò)請(qǐng)求、響應(yīng)報(bào)文,使用網(wǎng)絡(luò)代理工具來調(diào)試網(wǎng)絡(luò)數(shù)據(jù)問題是主流方式。配置代理需要在應(yīng)用中指定代理服務(wù)器的地址和端口,另外 Github API 是 HTTPS 協(xié)議,所以在配置完代理后還應(yīng)該禁用證書校驗(yàn),這些配置我們?cè)?Git 類初始化時(shí)執(zhí)行(init()方法)。下面是 Git 類的源碼:

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/adapter.dart';
import 'package:flutter/material.dart';
import '../index.dart';


class Git {
  // 在網(wǎng)絡(luò)請(qǐng)求過程中可能會(huì)需要使用當(dāng)前的context信息,比如在請(qǐng)求失敗時(shí)
  // 打開一個(gè)新路由,而打開新路由需要context信息。
  Git([this.context]) {
    _options = Options(extra: {"context": context});
  }


  BuildContext context;
  Options _options;
  static Dio dio = new Dio(BaseOptions(
    baseUrl: 'https://api.github.com/',
    headers: {
      HttpHeaders.acceptHeader: "application/vnd.github.squirrel-girl-preview,"
          "application/vnd.github.symmetra-preview+json",
    },
  ));


  static void init() {
    // 添加緩存插件
    dio.interceptors.add(Global.netCache);
    // 設(shè)置用戶token(可能為null,代表未登錄)
    dio.options.headers[HttpHeaders.authorizationHeader] = Global.profile.token;


    // 在調(diào)試模式下需要抓包調(diào)試,所以我們使用代理,并禁用HTTPS證書校驗(yàn)
    if (!Global.isRelease) {
      (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
          (client) {
        client.findProxy = (uri) {
          return "PROXY 10.1.10.250:8888";
        };
        //代理工具會(huì)提供一個(gè)抓包的自簽名證書,會(huì)通不過證書校驗(yàn),所以我們禁用證書校驗(yàn)
        client.badCertificateCallback =
            (X509Certificate cert, String host, int port) => true;
      };
    }
  }


  // 登錄接口,登錄成功后返回用戶信息
  Future<User> login(String login, String pwd) async {
    String basic = 'Basic ' + base64.encode(utf8.encode('$login:$pwd'));
    var r = await dio.get(
      "/users/$login",
      options: _options.merge(headers: {
        HttpHeaders.authorizationHeader: basic
      }, extra: {
        "noCache": true, //本接口禁用緩存
      }),
    );
    //登錄成功后更新公共頭(authorization),此后的所有請(qǐng)求都會(huì)帶上用戶身份信息
    dio.options.headers[HttpHeaders.authorizationHeader] = basic;
    //清空所有緩存
    Global.netCache.cache.clear();
    //更新profile中的token信息
    Global.profile.token = basic;
    return User.fromJson(r.data);
  }


  //獲取用戶項(xiàng)目列表
  Future<List<Repo>> getRepos(
      {Map<String, dynamic> queryParameters, //query參數(shù),用于接收分頁信息
      refresh = false}) async {
    if (refresh) {
      // 列表下拉刷新,需要?jiǎng)h除緩存(攔截器中會(huì)讀取這些信息)
      _options.extra.addAll({"refresh": true, "list": true});
    }
    var r = await dio.get<List>(
      "user/repos",
      queryParameters: queryParameters,
      options: _options,
    );
    return r.data.map((e) => Repo.fromJson(e)).toList();
  }
}

可以看到我們?cè)?code>init()方法中,我們判斷了是否是調(diào)試環(huán)境,然后做了一些針對(duì)調(diào)試環(huán)境的網(wǎng)絡(luò)配置(設(shè)置代理和禁用證書校驗(yàn))。而Git.init()方法是應(yīng)用啟動(dòng)時(shí)被調(diào)用的(Global.init()方法中會(huì)調(diào)用Git.init())。

另外需要注意,我們所有的網(wǎng)絡(luò)請(qǐng)求是通過同一個(gè)dio實(shí)例(靜態(tài)變量)發(fā)出的,在創(chuàng)建該dio實(shí)例時(shí)我們將 Github API 的基地址和 API 支持的 Header 進(jìn)行了全局配置,這樣所有通過該dio實(shí)例發(fā)出的請(qǐng)求都會(huì)默認(rèn)使用者些配置。

在本實(shí)例中,我們只用到了登錄接口和獲取用戶項(xiàng)目的接口,所以在Git類中只定義了login(…)getRepos(…)方法,如果讀者要在本實(shí)例的基礎(chǔ)上擴(kuò)充功能,讀者可以將其它的接口請(qǐng)求方法添加到Git類中,這樣便實(shí)現(xiàn)了網(wǎng)絡(luò)請(qǐng)求接口在代碼層面的集中管理和維護(hù)。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)