Swift 开发 wanandroid 客户端——接口调试翻车现场和抽象Model

1,164 阅读6分钟

这是我参与更文挑战的第18天,活动详情查看: 更文挑战

调试一个接口结果翻车了

我本来是打算写完玩安卓的个人积分接口就继续努力更文的,结果一不小心翻车了。

事情也很简单,就是一个接口请求,明明已经成功了,结果在转Model的时候出错了。

获取个人积分,需要登录后访问

www.wanandroid.com/lg/coin/use…

官方给的JSON样例:

{
    "data": {
        "coinCount": 451, //总积分
        "rank": 7, //当前排名
        "userId": 2,
        "username": "x**oyang"
    },
    "errorCode": 0,
    "errorMsg": ""
}

其实这个接口很简单的,在昨天说的BaseModel的基础上,只需要将泛型T即JSON中的data里包裹的内容换为定义好的模型就好了:

  • BaseModel
struct BaseModel<T: Codable>: Codable {
    let data : T?
    let errorCode : Int?
    let errorMsg : String?
}
  • MyCoin
struct MyCoin: Codable {
    let coinCount: Int?
    let rank: Int?
    let userId: Int?
    let username: String?

使用的时候BaseModel<MyCoin>这样表示就好,于是通过RxMoya也很好调用了,我在请求完成后,直接通过map函数将数据转为BaseModel<MyCoin>,并最终转为Single类型。

关于Single类型,RxSwift中是这么解释的:

Single 是 Observable 的另外一个版本。不像 Observable 可以发出多个元素,它要么只能发出一个元素,要么产生一个 error 事件。

  • 发出一个元素,或一个 error 事件
  • 不会共享附加作用 一个比较常见的例子就是执行 HTTP 请求,然后返回一个应答或错误。不过你也可以用 Single 来描述任何只有一个元素的序列。
func getMyCoin() -> Single<BaseModel<MyCoin>> {
    return myProvider.rx.request(MyService.userCoinInfo)
        .map(BaseModel<MyCoin>.self)
}

有关于RxSwift的序列,后面会专门进行讲解,这里大家就关注为啥没有返回值。于是我把这个Single进行了订阅,想看看怎么回事:

func getMyCoin() {
    myProvider.rx.request(MyService.userCoinInfo)
        .map(BaseModel<MyCoin>.self)
        .subscribe { baseModel in
            print(baseModel)
        } onError: { error in
            print(error)
        }.disposed(by: disposeBag)
}

于是打印出了error信息:

objectMapping(Swift.DecodingError.typeMismatch(Swift.Int, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil), CodingKeys(stringValue: "rank", intValue: nil)], debugDescription: "Expected to decode Int but found a string/data instead.", underlyingError: nil)), Status Code: 200, Data Length: 133)

大概说的意思就是进行Decoding的时候异常了:

CodingKeys(stringValue: "rank", intValue: nil)], debugDescription: "Expected to decode Int but found a string/data instead.这里是关键信息:rank这个值预测是转为Int,但是发现是一个String。

同时注意结尾的Status Code: 200, Data Length: 133,说明接口是通的,数据的长度是133。

那么将问题定位在JSON转Model这一块了,既然官方给的JSON转不成功,那么我就自己用Postman试试吧,结果果然是在这里翻车了,rank在Postman里返回的确实是String类型。

image.png

官网给的样例和自己请求的JSON类型确实不太一样,于是我把MyCoin的模型定义进行了变更:

struct MyCoin: Codable {
    let coinCount : Int?
    let level : Int?
    let nickname : String?
    let rank : String?
    let userId : Int?
    let username : String?
}

接着上面的订阅进行了onSuccess回调中,走了print(baseModel):

BaseModel<MyCoin>(data: Optional(RxStudy.MyCoin(coinCount: Optional(2175), level: Optional(22), nickname: Optional(""), rank: Optional("766"), userId: Optional(59171), username: Optional("1**37156081"))), errorCode: Optional(0), errorMsg: Optional(""))

翻车反思:

  • 如果官方给的文档不可靠的话,自己一定要尝试看看返回的结果是什么样子的。就算是自己在工作中遇到了这类问题,也可以多和后端同事沟通,通过Swagger等其他工具进行调试。

  • 昨天说了有条件的话自己用Postman调试一下,这话没白说,自己实践自己证明。

  • 定义模型最好是能进行严格的一一对应:

    ①官方给的JSON中rank是Int类型,而我自己请求后发现rank是String

    ②至于少了定义的模型多了几个可选类型属性或者少了几个可选类型属性,并不影响转换

    ③类型完全一致性在Swift中JSON转模型非常重要,如果是其他语言,JSON返回的是一个Int拿String去接,理论上应该是可以接住的。

抽象Model

昨天我们通过观察几个JSON的样例,封装抽出了最外层的BaseModel,BaseModel可以本文上面写的细化,而JSON样例可以在玩安卓开放API在线例子中查看。

由于玩安卓的接口中有很多涉及分页,让我们来看看其样例JSON:

  • 积分排行榜JSON:
{
    "data": {
        "curPage": 1,
        "datas": [
            {
                "coinCount": 12159,
                "level": 122,
                "rank": 1,
                "userId": 20382,
                "username": "g**eii"
            }
        ],
        "offset": 0,
        "over": false,
        "pageCount": 1070,
        "size": 30,
        "total": 32082
    },
    "errorCode": 0,
    "errorMsg": ""
}
  • 项目列表数据JSON:
{
    "data":{
        "curPage":1,
        "datas":[
            {
                "apkLink":"",
                "audit":1,
                "author":"wo5813288",
                "canEdit":false,
                "chapterId":294,
                "chapterName":"完整项目",
                "collect":false,
                "courseId":13,
                "desc":"更新学习flutter,所以系统的做一款应用来实践一下。这款应用也开发了很多内容了,后续还要继续更新功能。开发这个项目主要也是熟悉flutter的树形结构的写法和UI组件,项目中也用到了flutter比较流行的第三方框架。",
                "descMd":"",
                "envelopePic":"https://www.wanandroid.com/blogimgs/e092cd25-3e43-42c4-a7eb-b1ebc60ce02a.png",
                "fresh":true,
                "host":"",
                "id":18624,
                "link":"https://www.wanandroid.com/blog/show/3020",
                "niceDate":"10小时前",
                "niceShareDate":"10小时前",
                "origin":"",
                "prefix":"",
                "projectLink":"https://github.com/wo5813288/wan_giao",
                "publishTime":1623768707000,
                "realSuperChapterId":293,
                "selfVisible":0,
                "shareDate":1623768707000,
                "shareUser":"",
                "superChapterId":294,
                "superChapterName":"开源项目主Tab",
                "tags":[
                    {
                        "name":"项目",
                        "url":"/project/list/1?cid=294"
                    }
                ],
                "title":"Flutter开发的WanAndroid",
                "type":0,
                "userId":-1,
                "visible":1,
                "zan":0
            }
        ],
        "offset":0,
        "over":false,
        "pageCount":18,
        "size":15,
        "total":261
    },
    "errorCode":0,
    "errorMsg":""
}
  • 个人积分列表JSON:
{
    "data": {
      "curPage": 0,
      "datas": [
        {
          "coinCount": 11,
          "date": 1587302836000,
          "desc": "2020-04-19 21:27:16 签到 , 积分:10 + 1",
          "id": 191949,
          "reason": "签到",
          "type": 1,
          "userId": 59171,
          "userName": "xxxxxx"
        }
      ],
      "offset": -20,
      "over": false,
      "pageCount": 1,
      "size": 20,
      "total": 2
    },
    "errorCode": 0,
    "errorMsg": ""
  }

仔细观察除了最外层的{ "data":业务具体数据 "errorCode":0, "errorMsg":"" }之外,我们来看看data里面包裹着什么样的数据,总结一下就是这样:

{
      "curPage": 0,
      "datas": [业务数据], /// 注意这里是一个数组
      "offset": -20,
      "over": false,
      "pageCount": 1,
      "size": 20,
      "total": 2
    }

于是又可以抽象一个Page类型的Model:

/// 有分页的基础模型
struct Page<Content: Codable> : Codable {
    let curPage : Int?
    let datas : [Content]?
    let offset : Int?
    let over : Bool?
    let pageCount : Int?
    let size : Int?
    let total : Int?
}

于是对于一个有分页的Model我们可以这样表示:BaseModel<Page<CoinRank>>,注意CoinRank为具体业务数据,复用的越多,我们就可以写更少的代码,同时要注意理解起来也越来越困难。

肯定有人会问,像你这样去抽象与总结类,提取到泛型,我一眼根本就看不出来。我想说的是:

这是一个积累的过程,就是BaseModel抽不出来也没什么太大关系,我自己也不是一步登天就能看出来可以抽取出来的,都是平时写代码观察成长的,加上从OC转过来写Swift,我在OC时代明明知道BaseModel应该抽出来,但是通过OC语言也不知道该如何去抽取,慢慢来,不要心浮气躁即可。

明日继续

今天已经是第18天了,不过我对自己一开始定的目标就很严格,在动力与压力的驱动下,去完成RxSwift版本的玩安卓App,所以明日继续。

计划是同一个页面,分别使用Swift与RxSwift编写进行对比。

大家加油!!!