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 爲例,方法能夠擴展到全部請求工具網絡

相關文章
相關標籤/搜索