Ajax 如何保证请求接口数据符合预期?如何去重?

先描述两个场景:ios

  1. 快速点击分页码1.2.3.4.5...。假设网络很差或接口速度不佳,此时可能有多个pending中请求。而咱们没法控制返回顺序。假如用户最后点击到分页5,而最后一个返回的接口是第三页的。那如今虽然页码为5,但实际展现的数据倒是第三页的。
  2. 以Vue为例,created中调用接口A,某watch中也调用接口A。那在页面初始化时,A可能被调用了两次,若是两次结果一致,那除了浪费,也不会形成其余严重问题。可结果不一致,会几率复现场景1中描述的问题。

解决办法其实不少,好比:ajax

  • 调用时加锁,判断该接口是否处于pending?
  • pending状态时,禁用操做按钮;

但这些方法不可避免的会引入多余状态。若是同页面出现N个接口,状况会更糟糕。如何维护那么多状态呢?json

其实咱们能够在拦截器中解决这些问题,直接贴代码。逻辑看注释:axios

如下以 axios 为例。api

请求管理器

单独封装管理器,是为了拦截器中的代码逻辑更清晰,也为扩展性。假设你须要在其余地方获取全部pending中的请求,并将其所有取消。
注意 cancel() 方法中的 this.pendings[name].source.cancel(),要想此方法有效,咱们须要在 register请求时,将ajax工具中包含取消请求api的对象做为 source 存入管理器。详见过滤器中代码。
/**
 * requestManage.js 请求管理器
 */
class RequestManage {
  constructor () {
    if (!RequestManage.instance) {
      // 这个属性能够用来判断是人为操做,仍是机器。
      this.nerveVelocity = 100
      // 进行中的请求
      this.pendings = {}
      RequestManage.instance = this
    }
    return RequestManage.instance
  }

  /**
   * 向管理器中注册请求
   * @param {String,Number} name - 请求标识
   * @param {*} [payload] - 负载信息,用来保存你指望管理器帮你存储的内容
   */
  register (name, payload = {}) {
    payload.time = new Date() * 1
    this.pendings[name] = payload
  }

  /**
   * 取消请求
   * @param {String,Number} name - 请求标识
   * @param {*} [payload] - 包含负载信息时,销毁后会从新注册
   */
  cancel (name, payload) {
    this.pendings[name].source.cancel()
    if (payload) {
      this.register(name, payload)
    }
  }

  /**
   * 在管理器中移除制定请求
   * @param {String,Number} name - 请求标识
   */
  remove (name) {
    delete this.pendings[name]
  }
}

export default new RequestManage()

过滤器

// request.js

import axios from 'axios
import { requestManage } from 'utils'

const request = axios.create({
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest'
  }
})

/**------------------------------------------------
 * axiox request 拦截器
 * 总体逻辑:
 * 1. 使用请求地址 url 做为请求标识。
 * 2. 将 axios 的 source,和包含所有请求参数的字符串存入管理器。(由于source中包含axios的cancel方法)
 * 3. 请求发起前,查看管理器中是否已存在一个请求?若是不存在,那注册一个进去。
 * 4. 若是已经存在,则对比参数,及判断是否为机器。如知足条件,则当前请求不须要发起。抛出错误 currentRequestCancelled。
 * 5. 若是参数不一样,或者是人为操做,则视为两个不一样请求。此时取消 pending 中的,并将当前请求从新注册。
 * 6. 使用 escape 配置,人为控制一些特殊接口不受约束。
 */

request.interceptors.request.use(config => {
  const { data, params, url, escape } = config
  const
    requestTime = new Date() * 1,
    source = axios.CancelToken.source(),
    currentBody = `${JSON.stringify(data)}${JSON.stringify(params)}`,
    pendingRequest = requestManage.pendings[url],
    pendingBody = pendingRequest && pendingRequest.body,
    isRobot = pendingRequest && requestTime - pendingRequest.time < requestManage.nerveVelocity

  if (pendingRequest) {
    if (currentBody === pendingBody && isRobot) {
      return Promise.reject(new Error('currentRequestCancelled'))
    } else if (!escape) {
      requestManage.cancel(url, {
        source: source,
        body: currentBody
      })
    }
  } else {
    requestManage.register(url, {
      source: source,
      body: currentBody
    })
  }

  config.cancelToken = source.token

  return config
}, error => {
  // 请求错误时作些事
  return Promise.reject(error)
})


/** ------------------------------------------------------------
 * axios response 拦截器
 * 接口正常返回后,在管理器中把对应请求移除。
 * 对 request 时抛出的错误作处理。
 */

request.interceptors.response.use(response => {
  const { url } = response.config
  requestManage.remove(url)

  return response
}, error => {
  if (axios.isCancel(error)) {
    throw new Error('cancelled')
  } else if (error.message === 'currentRequestCancelled') {
    throw new Error('currentRequestCancelled')
  } else {
    return Promise.reject(error)
  }
})

export default request

封装API

// api.js
import request from '@/utils/request'

/**
 * escape: true 会跳过全部约束。
 * 一般只有一种场景须要这么作:
 * 页面初始化时,相同接口同时发起多个请求,但参数不一致,且屡次返回的结果都会被使用。若是不设置此项,则只会保留最后一次,前面的请求会被 cancel 掉。
 */
export default function (params) {
  return request({
    url: `api_path`,
    method: 'GET',
    params: params,
    // escape: true
  })
}

使用

import api from 'api.js'

async function getData () {
  try {
    const req = await api({
      a:1,
      b:2
    })
  } catch (error) {
    console.log(error)
  }
}


getData()
getData()
getData()
getData()

// 屡次调用,控制台中只有第一次请求完成,并打印 `currentRequestCancelled`. (由于这几回请求彻底同样)

// 若是不捕获错误,控制台将报 cancelled 或 currentRequestCancelled 错误。

以上仅以 Axios 为例,方法能够扩展到全部请求工具网络

相关文章
相关标签/搜索