阅读 3286

记一次Promise在api接口合并中的实践

写在前面的话

  1. 本篇水文发出来之后,有朋友反馈文笔太差,的确是作者的锅,码字水平目前就这么高,只能委屈大家看这篇辣眼睛的文字了,文笔只能慢慢改善。
  2. 还有朋友反馈看懵的,仔细想想也是作者的锅,没有表达清楚,修改重发。
  3. 澄清一下本文和Promise.all没有半毛钱关系,如果让大家误会,见谅。
  4. 有朋友希望快速浏览能有一句总结,不想看代码。这里解释一下,其实总结就包含在下文合并接口的解释里,本文要解决的业务相对比较小众,脱离场景谈总结也没啥意义,不看业务场景和代码,本文真的没有任何的价值,主要是记录开发业务的一个思路。

关于接口合并(不知道有没有专门的术语,暂且如此称呼)在这里解释一下,本文所指的是页面初始化加载数据是一个api接口,而加载更多数据的是另一个api接口,前一个接口肯定会调用,第二个接口不一定会被调用(用户触发),但是我们把两个调用接口封装起来,公用一个业务逻辑,作者比较懒不想给两个接口分别写业务逻辑。

一. 前言

上次作者在个人项目中遇到的post预检请求bug,水了一篇小文《记一次跨域post请求数据之preflight request》,本文也只是记录在特定项目中如何抽取业务逻辑,封装两个api接口公用一段业务逻辑的思路,对读者朋友们有所启发,那就最好不过了,有什么问题或者错漏之处欢迎大家提出来分享,作者此文权当抛砖引玉。

关于接口合并,作者在项目开发文档中也有描述,有兴趣的可以去瞅瞅。

二. 猫眼API接口分析

脱离业务谈编码就是耍流氓。下面简单介绍一下猫眼的接口,其中我们发现在猫眼的两个页面中可以使用接口合并,现在以猫眼正在热映页面的两个api为例。

1 初始化获取当前热映电影列表

以下都将以 api_1 指称 初始化获取当前热映电影列表api接口

1.1说明
信息 说明
功能 初始化获取电影信息
URL //m.maoyan.com/ajax/movieOnInfoList
格式 JSON
HTTP METHOD GET

1.2 请求参数

参数 类型 必选 说明
token String false 登录之后的凭证

1.3返回字段

字段 类型 说明
movieList Array 电影列表(默认一次返回10条)
total Number 电影总数目, total >= movieList.length
movieIds Array 所有电影ID,总数同total,后续请求更多电影时必须依赖它们
coming Array 更多电影列表,第一次请求必定是空

1.4 接口示例

//m.maoyan.com/ajax/movieOnInfoList?token

{
  "coming": [],
  "stid": "576591972453269000",
  "movieIds": [247295, 410629, 1206605, 248906, 341139, 1250341, 1218091, 344869, 1243239, 580298, 907653],
  "movieList": [
    "同下方获取当前热映更多电影列表接口返回的coming字段"
  ],
  "stids": [
    {"movieId": 247295, "stid": "576591972453269000_a247295_c0"}
  ],
  "total": 11
}
复制代码

2 获取当前热映更多电影列表

以下都将以 api_2指称 获取当前热映更多电影列表api接口

2.1 说明

信息 说明
功能 获取hot更多电影列表
URL //m.maoyan.com/ajax/moreComingList
格式 JSON
HTTP METHOD GET

2.2 请求参数

参数 类型 必选 说明 列子
token String false 登录之后的凭证
movieIds String true 请求的电影ID,依赖初始化接口的接口返回字段movieIds "1214652,1229799,1251606"

2.3 返回字段

字段 类型 说明
coming Array 更多电影列表

2.4 接口示例

//m.maoyan.com/ajax/moreComingList?token=&movieIds=1214652%2C1229799%2C1251606%2C1215114

{
  "coming": [
    {
      "id": 1214652,
      "comingTitle": "2月22日 周五",
      "globalReleased": true,
      "haspromotionTag": false,
      "img": "http://p0.meituan.net/w.h/movie/979266668d0e94dc83956a70d22b4eaa184105.jpg",
      "nm": "朝花夕誓-于离别之朝束起约定之花",
      "preShow": false,
      "rt": "2019-02-22",
      "sc": "9.2",
      "showInfo": "今天10家影院放映21场",
      "showst": "3",
      "star": "石见舞菜香,入野自由,茅野爱衣",
      "version": "",
      "wish": 76220,
      "wishst": 0,
    },
    ...略
  ]
}
复制代码

上面两大坨数据,就是作者整理的api接口文档,仔细观察两个api接口的返回字段,都有一个coming字段,作者最初的灵感也是来自于它们,api_1接口的数据列表放在movieList字段中,我们下面就将以Promise来处理coming和movieList。

有朋友关注api_2接口依赖于api_1接口,猫眼的api就是这么设计的,api_1接口返回了部分电影列表、全部的电影id和电影总数,api_2接口请求只需传递电影id就可以了。其他公司设计的api接口请求参数可能就是offset和limit。

三. 方案

要进行接口合并,无非要解决两个问题, 判断接口、处理数据

1 判断接口

api_2接口请求数据的时候必定需要知道请求的是那些电影的ID,那么我们肯定要在本地定义一个offset作为数据的偏移量,作者的项目是vue写的,就放在了vue的组件实例上了。我们将offset设为0,第一次请求时offset必定为0,我们就将offset的值作为判断接口的依据。

下面直接上代码

/***
*  业务逻辑部分
*  1. isFirst判断是否第一次请求
*  2. getInfoListAction(isFirst) 得到最终的api操作函数 getMovieInfoList
*  关于 getInfoListAction请参看下文 @src src\api\index.js
***/
import { getInfoListAction } from '@/api'

const { offset, limit, total } = this
const isFirst = offset === 0
const getMovieInfoList = getInfoListAction(isFirst)
getMovieInfoList(params).then(data => {
    // ....数据处理此处略,详见下文
})
复制代码

难道直接用if-esle来硬编码判断?作者当然不会这么糊弄大家了。

/** 
*  @addr src/api/index.js
*  @ getMovieOnInfoList 初始api的操作函数
*  @ getMoreComingList 加载更多数据的操作函数
*  @ getInfoListAction通过上文的isFirst作为参数调用来判断返回 getMovieOnInfoList还是 getMoreComingList(也就是上文提到的getMovieInfoList)
 *  关于 getDataByAction 参看下文 @addr src/util/index.js
**/
import request from '@/util/request'
import { getDataByAction } from '@/util'

const getMovieOnInfoList = request('/movieOnInfoList')
const getMoreComingList = request('/moreComingList')
export const getInfoListAction = getDataByAction(getMovieOnInfoList, getMoreComingList)

/*** 
*  @addr src/util/index.js
*  @getDataByAction 使用函数柯里化,接受两个操作函数返回一个新函数,在业务逻辑中返回最终的api操作函数
**/
export const getDataByAction = (initAction, nextAction) => (isFirst) => isFirst ? initAction : nextAction

// @addr src/util/request.js
import Axios from 'axios'
let baseURL = process.env.VUE_APP_URL

const defaultConfig = {
  baseURL
}

const STATUS_CODE = 200

const instance = Axios.create(defaultConfig)

const request = (url, method = 'get') => (params) => {
  return instance({
    url,
    method,
    ...params
  }).then(resp => {
    if (resp.status === STATUS_CODE) {
      return resp.data
    }
  })
}
export default request
复制代码

请忽略作者的request函数的丑陋封装,没有做错误处理,(逃

2 数据处理

由上文可知,我们最终的api调用函数调用之后其实是返回了一个Promise{<resolve>:data}

我们在vue组件实例上定义了movieList存放数据,movieIds存放第一次返回时movieIds字段的数据,total数据总数。

// 接上文的省略的代码部分
// 暂时忽略params参数,下文有处理详解

/**
*  1. 在promise.then的函数中,我们从data数据里取 movieIds, movieList, coming, total字段
*  2.1 我们以movieIds判断是第一次调用api接口(其他字段也可以,这里先偷懒),那么我们赋值需要的数据 movieIds,total,直接返回movieList数据.
*  2.2 如果2.1没有执行,那么肯定是加载更多数据的接口api_2,我们直接返回coming字段
*  3. 从2.1、2.2我们获得了最后的数据Array,判断数据的长度,更新offset偏移量和movieList数据
*  ps: setImgSize是处理图片的函数,不必理会
**/
getMovieInfoList(params).then(data => {
  const { movieIds, movieList, coming, total } = data
  if (movieIds) {
    this.movieIds = movieIds
    this.total = total
    return movieList
  }
  return coming
}).then(data => {
  if (data.length) {
    this.offset += data.length
    this.movieList.push(...setImgSize(data))
    $state.loaded()
  } else {
    $state.complete()
  }
})
复制代码

3 参数处理

const { offset, limit, total } = this
const isFirst = offset === 0
if (offset && offset > total) return
const movieIds = this.movieIds
  .slice(offset, offset + limit)
  .join()
const params = { params: { ...this.params, movieIds } }
复制代码

结尾

水到现在终于要结束了,挤一挤好像也没啥干货,关于接口合并,智者见智,万一以后改接口爆炸了也说不定,作者只是记录下当前遇到类似情况的一种处理方案,或者有更好的方案欢迎大家分享,行文错漏、改善之处欢迎提出来探讨。

Tips: 每天水一篇,生活乐无边。