Axios 源碼解析

本文不會(huì)細(xì)摳某些功能的具體實(shí)現(xiàn)方式,比如 config 的 merge 方式、utils 中的工具方法。而是抓住主干、梳理脈絡(luò),重點(diǎn)介紹經(jīng)典的、優(yōu)秀的實(shí)現(xiàn)思想。比如 adapter 怎么兼容 browser 和 node、Interceptor 簡(jiǎn)單而精巧的實(shí)現(xiàn)。

過(guò)去八年,axiox 以 github97k+的 star 和 npm2000w+的周下載量占據(jù)著網(wǎng)絡(luò)請(qǐng)求庫(kù)的絕對(duì)地位,但 1.0.0 版本在二十天前才正式發(fā)布。具體改動(dòng)查看 V1.0.0。

Axios 特性

  1. 基于 Promise 封裝
  2. 作用于 node 和瀏覽器,node 創(chuàng)建 http 請(qǐng)求,瀏覽器創(chuàng)建 XMLHttpRequest
  3. 請(qǐng)求響應(yīng)攔截器
  4. 數(shù)據(jù)轉(zhuǎn)換
  5. 成功失敗狀態(tài)碼自定義
  6. XSRF 防御
  7. 取消請(qǐng)求

源碼解析

axios 和 Axios 的關(guān)系

axios 是通過(guò) bind 對(duì) Axios.prototype.request 硬綁定了 Axios 的實(shí)例的函數(shù)。其上邊添加了 Axios、CanceledError、CancelToken、formToJSON、create 等靜態(tài)方法,又通過(guò) extends 的方式將 Axios.prototype 上的方法擴(kuò)展到 axios 上。所以可以通過(guò) axios(config)、axios.get()的方式創(chuàng)建請(qǐng)求,也可以通過(guò) new axios.Axios()、axios.create()的方式創(chuàng)建新的 Axios 實(shí)例。

axios 入口

function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig)

  // instance為綁定了context實(shí)例的函數(shù),函數(shù)內(nèi)部調(diào)用了Axios原型上的request方法
  const instance = bind(Axios.prototype.request, context)

  // 將Axios原型上的方法擴(kuò)展到instance上,包括請(qǐng)求方法等
  utils.extend(instance, Axios.prototype, context, { allOwnKeys: true })

  // 將context上的屬性擴(kuò)展到instance上,比如攔截器等
  utils.extend(instance, context, null, { allOwnKeys: true })

  // 提供了一個(gè)工廠函數(shù),用來(lái)生成instance實(shí)例
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig))
  }

  return instance
}

// 對(duì)外暴露axios
const axios = createInstance(defaults)

axios.Axios = Axios

export default axios

Axios

class Axios {
  constructor(instanceConfig) {
    this.defaults = instanceConfig
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    }
  }
}

export default Axios

原型上擴(kuò)展請(qǐng)求方法,分為兩類(lèi):

  1. 獲取數(shù)據(jù)
  2. 提交數(shù)據(jù)
    1. 普通提交,格式為 json 或者 FormData 實(shí)例
    2. 文件提交,請(qǐng)求方式增加 Form 后綴,設(shè)置 Content-Type 為 multipart/form-data

Multipart/Form-Data是一種編碼類(lèi)型,它允許在將文件傳輸?shù)椒?wù)器進(jìn)行處理之前將文件包含在表單數(shù)據(jù)中。

// 獲取數(shù)據(jù)的方法
utils.forEach(
  ['delete', 'get', 'head', 'options'],
  function forEachMethodNoData(method) {
    Axios.prototype[method] = function (url, config) {
      return this.request(
        mergeConfig(config || {}, {
          method,
          url,
          data: (config || {}).data
        })
      )
    }
  }
)

// 提交數(shù)據(jù)的方法
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  function generateHTTPMethod(isForm) {
    return function httpMethod(url, data, config) {
      return this.request(
        mergeConfig(config || {}, {
          method,
          headers: isForm
            ? {
                'Content-Type': 'multipart/form-data'
              }
            : {},
          url,
          data
        })
      )
    }
  }

  Axios.prototype[method] = generateHTTPMethod()
  Axios.prototype[method + 'Form'] = generateHTTPMethod(true)
})

所有的請(qǐng)求都是去調(diào)用 Axios 原型上的 request 方法,分析 request 之前先分析攔截器的實(shí)現(xiàn)。

InterceptorManager

創(chuàng)建攔截器管理器

class Axios {
  constructor(instanceConfig) {
    this.defaults = instanceConfig
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    }
  }
}

攔截器構(gòu)造器

class InterceptorManager {
  constructor() {
    this.handlers = []
  }

  use(fulfilled, rejected, options) {
    this.handlers.push({
      fulfilled,
      rejected,

      // 同步執(zhí)行攔截器
      synchronous: options ? options.synchronous : false,
      runWhen: options ? options.runWhen : null
    })

    // 返回?cái)r截器的索引
    return this.handlers.length - 1
  }

  // 根據(jù)索引移除攔截器
  eject(id) {
    if (this.handlers[id]) {
      this.handlers[id] = null
    }
  }

  // 清除所有攔截器
  clear() {
    if (this.handlers) {
      this.handlers = []
    }
  }

  forEach(fn) {
    utils.forEach(this.handlers, function forEachHandler(h) {
      if (h !== null) {
        fn(h)
      }
    })
  }
}

export default InterceptorManager

實(shí)例創(chuàng)建時(shí)會(huì)生成 reques 和 response 兩種類(lèi)型的攔截器。并且每種可以注冊(cè)多個(gè)。每個(gè)攔截器接受三個(gè)參數(shù):

  1. Fulfilled
  2. Rejected
  3. Options,可選
    1. synchronous,boolean 型
    2. runWhen,函數(shù)類(lèi)型

fulfilled 為成功時(shí)調(diào)用
rejected 為拋出錯(cuò)誤時(shí)調(diào)用

攔截器的返回值是當(dāng)前攔截器的索引。由此可以看到當(dāng) fulfilled 中出現(xiàn)錯(cuò)誤時(shí)并不會(huì)被 rejected 捕獲,request 中的錯(cuò)誤會(huì)中斷后續(xù)攔截器的執(zhí)行,進(jìn)而中斷請(qǐng)求的發(fā)起,但是 fulfilled 中的錯(cuò)誤不會(huì)被 rejected 捕獲,會(huì)冒泡到全局,通過(guò) promise 的 catch 捕獲。比如:

axios(url)
  .then(res => {})
  .catch(err => {
    // do something...
  })

// OR
try {
  await axios(url)
} catch {
  // do something...
}

攔截器的執(zhí)行和 Options 的兩個(gè)屬性在 reques 中具體解析。

eject:根據(jù)攔截器在 handlers 中的索引移除特定的攔截器,比如:

const interceptor = axios.interceptors.request.use(function () {})

axios.interceptors.request.eject(interceptor)

clear:v1.0.0 新增的方法,用來(lái)移除所有攔截器

axios.interceptors.request.clear()

request

class Axios {
  request(configOrUrl, config) {
    if (typeof configOrUrl === 'string') {
      config = config || {}
      config.url = configOrUrl
    } else {
      config = configOrUrl || {}
    }

    config = mergeConfig(this.defaults, config)

    // Set config.method 默認(rèn) get 請(qǐng)求
    config.method = (
      config.method ||
      this.defaults.method ||
      'get'
    ).toLowerCase()

    // Flatten headers
    const defaultHeaders =
      config.headers &&
      utils.merge(config.headers.common, config.headers[config.method])

    defaultHeaders &&
      utils.forEach(
        ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
        function cleanHeaderConfig(method) {
          delete config.headers[method]
        }
      )

    // 創(chuàng)建請(qǐng)求頭
    config.headers = new AxiosHeaders(config.headers, defaultHeaders)
    // 攔截器的 fulfilled 和 rejected 全部平鋪到一個(gè)數(shù)組中
    // 請(qǐng)求攔截器,遵循先進(jìn)(注冊(cè))后出(執(zhí)行)的原則 棧結(jié)構(gòu)
    const requestInterceptorChain = []
    let synchronousRequestInterceptors = true

    this.interceptors.request.forEach(function unshiftRequestInterceptors(
      interceptor
    ) {
      if (
        typeof interceptor.runWhen === 'function' &&
        interceptor.runWhen(config) === false
      ) {
        return
      }

      synchronousRequestInterceptors =
        synchronousRequestInterceptors && interceptor.synchronous

      requestInterceptorChain.unshift(
        interceptor.fulfilled,

        interceptor.rejected
      )
    })

    // 響應(yīng)攔截器 遵循先進(jìn)先出的原則
    const responseInterceptorChain = []

    this.interceptors.response.forEach(function pushResponseInterceptors(
      interceptor
    ) {
      // 同樣平鋪
      responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected)
    })

    let promise
    let i = 0
    let len

    if (!synchronousRequestInterceptors) {
      const chain = [dispatchRequest.bind(this), undefined]

      chain.unshift.apply(chain, requestInterceptorChain)
      chain.push.apply(chain, responseInterceptorChain)
      len = chain.length
      promise = Promise.resolve(config)

      while (i < len) {
        promise = promise.then(chain[i++], chain[i++])
      }

      return promise
    }

    len = requestInterceptorChain.length
    let newConfig = config
    i = 0

    // 同步執(zhí)行所有請(qǐng)求攔截器
    while (i < len) {
      const onFulfilled = requestInterceptorChain[i++]
      const onRejected = requestInterceptorChain[i++]

      try {
        newConfig = onFulfilled(newConfig)
      } catch (error) {
        onRejected.call(this, error)
        break
      }
    }

    // 發(fā)起網(wǎng)絡(luò)請(qǐng)求
    try {
      promise = dispatchRequest.call(this, newConfig)
    } catch (error) {
      return Promise.reject(error)
    }

    i = 0
    len = responseInterceptorChain.length

    // 執(zhí)行所有響應(yīng)攔截器
    while (i < len) {
      promise = promise.then(
        responseInterceptorChain[i++],
        responseInterceptorChain[i++]
      )
    }

    return promise
  }
}

request 中主要做了 4 件事:

  1. 初始化 config 配置
  2. 創(chuàng)建請(qǐng)求頭
  3. 處理攔截器
  4. 發(fā)起網(wǎng)絡(luò)請(qǐng)求

具體分析攔截器的處理:

Request Interceptor

const requestInterceptorChain = []
let synchronousRequestInterceptors = true

this.interceptors.request.forEach(function unshiftRequestInterceptors(
  interceptor
) {
  if (
    typeof interceptor.runWhen === 'function' &&
    interceptor.runWhen(config) === false
  ) {
    return
  }

  synchronousRequestInterceptors =
    synchronousRequestInterceptors && interceptor.synchronous

  requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected)
})

這一步是把請(qǐng)求攔截器的 fulfilled 和 rejected 以先進(jìn)(注冊(cè))后出(執(zhí)行)的規(guī)則全部存儲(chǔ)到棧結(jié)構(gòu)。
如果某個(gè)攔截器的配置項(xiàng)定義了 runWhen,則不入棧。

synchronousRequestInterceptors 則表示請(qǐng)求攔截器是否同步執(zhí)行。只要有一個(gè)攔截器的配置為 false,那么 synchronousRequestInterceptors 的最終結(jié)果都是 false。具體執(zhí)行方式稍后分析。

最終請(qǐng)求攔截器形成的棧結(jié)構(gòu)結(jié)果如下:

const requestInterceptorChain = [..., requestFulfilled3, requestRejected3, requestFulfilled2, requestRejected2, requestFulfilled1, requestRejected1]

Response Interceptor

const responseInterceptorChain = []

this.interceptors.response.forEach(function pushResponseInterceptors(
  interceptor
) {
  responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected)
})

響應(yīng)攔截器遵循先進(jìn)(注冊(cè))先出(執(zhí)行)的順序。

最終的結(jié)果如下:

const responseInterceptorChain = [responseFulfilled1, responseRejected1,  responseFulfilled2, responseRejected2, responseFulfilled3, responseRejected3, ...]

通常情況下請(qǐng)求攔截器的配置項(xiàng) synchronous 都不會(huì)設(shè)置,默認(rèn)為 false,即不是同步調(diào)用。所以通過(guò) promise 的 then 異步鏈?zhǔn)秸{(diào)用。會(huì)走到下面邏輯:

let promise
let i = 0
let len

if (!synchronousRequestInterceptors) {
  const chain = [dispatchRequest.bind(this), undefined]

  chain.unshift.apply(chain, requestInterceptorChain)
  chain.push.apply(chain, responseInterceptorChain)
  len = chain.length
  promise = Promise.resolve(config)

  while (i < len) {
    promise = promise.then(chain[i++], chain[i++])
  }

  return promise
}

chain 最終形成的結(jié)構(gòu)是:

const chain = [
  requestFulfilled3,
  requestRejected3,
  requestFulfilled2,
  requestRejected2,
  requestFulfilled1,
  requestRejected1,
  dispatchRequest.bind(this),
  undefined,
  responseFulfilled1,
  responseRejected1,
  responseFulfilled2,
  responseRejected2,
  responseFulfilled3,
  responseRejected3
]

chain 數(shù)組中以 dispatchRequest 為分界點(diǎn),前面是請(qǐng)求攔截器,后面是響應(yīng)攔截器,dispatchRequest 為真正發(fā)起請(qǐng)求的函數(shù),索引為偶數(shù)的是 fulfilled,奇數(shù)的是 rejected。最終返回 promise,使得開(kāi)發(fā)者可以鏈?zhǔn)秸{(diào)用。

synchronousRequestInterceptors 為 false 時(shí),異步鏈?zhǔn)秸{(diào)用請(qǐng)求攔截器。如下:

promise = Promise.resolve(config)

while (i < len) {
  promise = promise.then(chain[i++], chain[i++])
}

這里真是巧妙。兩次 i++,取出來(lái)的兩個(gè)函數(shù)正好對(duì)應(yīng)到 then 的兩個(gè)參數(shù)。
當(dāng) synchronousRequestInterceptors 為 true,即同步調(diào)用攔截器。步驟:

  1. 按順序同步調(diào)用請(qǐng)求攔截器
len = requestInterceptorChain.length
let newConfig = config
i = 0

while (i < len) {
  const onFulfilled = requestInterceptorChain[i++]
  const onRejected = requestInterceptorChain[i++]

  try {
    newConfig = onFulfilled(newConfig)
  } catch (error) {
    onRejected.call(this, error)
    break
  }
}
  1. 發(fā)起網(wǎng)絡(luò)請(qǐng)求
// 發(fā)起網(wǎng)絡(luò)請(qǐng)求
try {
  promise = dispatchRequest.call(this, newConfig)
} catch (error) {
  return Promise.reject(error)
}
  1. 異步鏈?zhǔn)秸{(diào)用響應(yīng)攔截器
i = 0
len = responseInterceptorChain.length

// 異步鏈?zhǔn)綀?zhí)行所有響應(yīng)攔截器
while (i < len) {
  promise = promise.then(
    responseInterceptorChain[i++],
    responseInterceptorChain[i++]
  )
}

至此,真正發(fā)起網(wǎng)絡(luò)請(qǐng)求前的工作全部完成。接下來(lái)是網(wǎng)絡(luò)請(qǐng)求環(huán)節(jié)。

dispatchRequest

dispatchRequest 中發(fā)起真正的網(wǎng)絡(luò)請(qǐng)求。

function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested()
  }

  if (config.signal && config.signal.aborted) {
    throw new CanceledError()
  }
}

function dispatchRequest(config) {
  throwIfCancellationRequested(config)

  config.headers = AxiosHeaders.from(config.headers)
  config.data = transformData.call(config, config.transformRequest)

  // 獲取請(qǐng)求適配器
  const adapter = config.adapter || defaults.adapter

  // 發(fā)起請(qǐng)求
  return adapter(config).then(
    function onAdapterResolution(response) {
      throwIfCancellationRequested(config)

      response.data = transformData.call(
        config,
        config.transformResponse,
        response
      )
      response.headers = AxiosHeaders.from(response.headers)

      return response
    },

    function onAdapterRejection(reason) {
      if (!isCancel(reason)) {
        throwIfCancellationRequested(config)

        if (reason && reason.response) {
          reason.response.data = transformData.call(
            config,
            config.transformResponse,
            reason.response
          )
          reason.response.headers = AxiosHeaders.from(reason.response.headers)
        }
      }

      return Promise.reject(reason)
    }
  )
}

adapter

由于 axios 即可在瀏覽器中也可在 node.js 中使用。不僅會(huì)在運(yùn)行時(shí)根據(jù)環(huán)境區(qū)分,而且可以做到應(yīng)用程序打包構(gòu)建時(shí)根據(jù)目標(biāo)環(huán)境只加載對(duì)應(yīng)環(huán)境的包。

運(yùn)行時(shí)適配

import httpAdapter from './http.js'
import xhrAdapter from './xhr.js'

const adapters = {
  http: httpAdapter,
  xhr: xhrAdapter
}

export default {
  getAdapter: nameOrAdapter => {
    const adapter = adapters[nameOrAdapter]
    return adapter
  },
  adapters
}

// 獲取運(yùn)行時(shí)環(huán)境
function getDefaultAdapter() {
  let adapter

  if (typeof XMLHttpRequest !== 'undefined') {
    adapter = adapters.getAdapter('xhr')
  } else if (
    typeof process !== 'undefined' &&
    utils.kindOf(process) === 'process'
  ) {
    adapter = adapters.getAdapter('http')
  }

  return adapter
}

xhrAdapter 為瀏覽器環(huán)境,通過(guò)創(chuàng)建 XMLHttprequest 請(qǐng)求。
httpAdapter 為 node.js 環(huán)境,創(chuàng)建 http 請(qǐng)求。

構(gòu)建時(shí)適配

源碼文件:

image.png

目標(biāo)環(huán)境為瀏覽器的項(xiàng)目構(gòu)建后:

image.png

之所以做到這一點(diǎn)是,我們?cè)跇?gòu)建時(shí)一般默認(rèn)目標(biāo)環(huán)境是 web,在 axios 源碼包的 package.json 中,配置了 browser 字段。

image.png

xhr

  1. 創(chuàng)建 XMLHttpRequest 對(duì)象
  2. 設(shè)置超時(shí)時(shí)間、請(qǐng)求頭、響應(yīng)類(lèi)型、鑒權(quán)、跨域攜帶憑證等
  3. 監(jiān)聽(tīng)各種事件,比如 onreadystatechange、onabort、onerror、ontimeout、onDownloadProgress、onUploadProgress 等
  4. 發(fā)送請(qǐng)求

默認(rèn)成功狀態(tài)碼是 status >= 200 & status < 300,也可通過(guò) validateStatus 自行設(shè)定。

http

  1. 一系列初始化工作
  2. http/https/data 等請(qǐng)求

取消請(qǐng)求

兩種方式可以取消請(qǐng)求:

  1. AbortController, 這種是以 fetch API 方式
const controller = new AbortController()

axios
  .get('/foo', {
    signal: controller.signal
  })
  .then(function (response) {
    //...
  })

// 取消請(qǐng)求
controller.abort() // 不支持 message 參數(shù)
  1. CancelToken
const CancelToken = axios.CancelToken
const source = CancelToken.source()

axios
  .get('/user', {
    cancelToken: source.token
  })
  .catch(function (thrown) {
    if (axios.isCancel(thrown)) {
      console.log('Request canceled', thrown.message)
    } else {
      // 處理錯(cuò)誤
    }
  })

// 取消請(qǐng)求(message 參數(shù)是可選的)
source.cancel('取消請(qǐng)求~')

也可以通過(guò)傳遞一個(gè) executor 函數(shù)到 CancelToken 的構(gòu)造函數(shù)來(lái)創(chuàng)建一個(gè) cancel token:

const CancelToken = axios.CancelToken
let cancel

axios.get('/user', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函數(shù)接收一個(gè) cancel 函數(shù)作為參數(shù)
    cancel = c
  })
})

//取消請(qǐng)求
cancel()

這種方式將會(huì)廢棄。不做過(guò)多討論。只分析基于 AbortController 方式取消請(qǐng)求的實(shí)現(xiàn)思路。

image.png

在配置對(duì)象上設(shè)置 signal 為 AbortController 的實(shí)例,當(dāng)調(diào)用 dispatchRequest 的時(shí)候首先判斷 config.signal.aborted 的狀態(tài),如果是 true,則說(shuō)明請(qǐng)求已經(jīng)被取消了,然后拋出錯(cuò)誤,阻斷請(qǐng)求的發(fā)起。

function throwIfCancellationRequested(config) {
  if (config.signal && config.signal.aborted) {
    throw new CanceledError()
  }
}

這里為什么調(diào)用兩次?

image.png

因?yàn)檎?qǐng)求攔截器的執(zhí)行分為同步和異步。
如果是異步的,進(jìn)入到 dispatchRequest 中時(shí)取消請(qǐng)求的動(dòng)作已經(jīng)完成,所以直接拋出錯(cuò)誤阻斷請(qǐng)求的發(fā)起即可。
如果是同步,那么從請(qǐng)求攔截器到發(fā)起請(qǐng)求的動(dòng)作都是同步的,所以執(zhí)行取消的動(dòng)作在發(fā)起請(qǐng)求之后了。所以要攔截本次請(qǐng)求只能在請(qǐng)求結(jié)束后 then 中阻斷了。
可能會(huì)疑惑,請(qǐng)求都結(jié)束了,取消動(dòng)作的執(zhí)行還有什么意義,其實(shí)細(xì)想,作為開(kāi)發(fā)者,或者說(shuō)在實(shí)際業(yè)務(wù)開(kāi)發(fā)中,我們只是不想要本次請(qǐng)求的結(jié)果,比如,頁(yè)面初始化后,同時(shí)并發(fā)了三個(gè)請(qǐng)求,但是一旦發(fā)現(xiàn)沒(méi)登陸,那么就需要執(zhí)行 A 操作,如果不做取消的處理,三個(gè)請(qǐng)求的結(jié)果都是沒(méi)登陸,那么就需要執(zhí)行三次 A 操作,大可不必,或者不合理不正確。

以上就是這三天對(duì) axios 源碼的解讀所做的總結(jié)。最主要的就是攔截器和適配器的實(shí)現(xiàn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 基類(lèi) Axios 跟隨入口 index.js 進(jìn)入/lib/axios.js,第一個(gè)方法則是createInsta...
    丶梅邊閱讀 805評(píng)論 0 1
  • 實(shí)在來(lái)不及自己寫(xiě)了 把讀過(guò)的文章先轉(zhuǎn)過(guò)來(lái) 明天再進(jìn)行編輯 axios項(xiàng)目目錄結(jié)構(gòu) 注:因?yàn)槲覀冃枰吹拇a都是...
    vivianXIa閱讀 1,141評(píng)論 0 1
  • Axios是近幾年非?;鸬腍TTP請(qǐng)求庫(kù),官網(wǎng)上介紹Axios 是一個(gè)基于 promise 的 HTTP 庫(kù),可以...
    milletmi閱讀 3,619評(píng)論 0 9
  • axios如何實(shí)現(xiàn)多種請(qǐng)求方式 原理: 通過(guò)數(shù)組循環(huán)來(lái)批量注冊(cè)接口,統(tǒng)一調(diào)用同一個(gè)方法,參數(shù)差異:通過(guò)until....
    前端的爬行之旅閱讀 363評(píng)論 0 0
  • 一、攔截器介紹 先看下官方文檔[https://axios-http.com/docs/interceptors]...
    前端艾希閱讀 1,039評(píng)論 0 1

友情鏈接更多精彩內(nèi)容