本文不會(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 特性
- 基于 Promise 封裝
- 作用于 node 和瀏覽器,node 創(chuàng)建 http 請(qǐng)求,瀏覽器創(chuàng)建 XMLHttpRequest
- 請(qǐng)求響應(yīng)攔截器
- 數(shù)據(jù)轉(zhuǎn)換
- 成功失敗狀態(tài)碼自定義
- XSRF 防御
- 取消請(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):
- 獲取數(shù)據(jù)
- 提交數(shù)據(jù)
- 普通提交,格式為 json 或者 FormData 實(shí)例
- 文件提交,請(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ù):
- Fulfilled
- Rejected
- Options,可選
- synchronous,boolean 型
- 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 件事:
- 初始化 config 配置
- 創(chuàng)建請(qǐng)求頭
- 處理攔截器
- 發(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)用攔截器。步驟:
- 按順序同步調(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
}
}
- 發(fā)起網(wǎng)絡(luò)請(qǐng)求
// 發(fā)起網(wǎng)絡(luò)請(qǐng)求
try {
promise = dispatchRequest.call(this, newConfig)
} catch (error) {
return Promise.reject(error)
}
- 異步鏈?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í)適配
源碼文件:

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

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

xhr
- 創(chuàng)建 XMLHttpRequest 對(duì)象
- 設(shè)置超時(shí)時(shí)間、請(qǐng)求頭、響應(yīng)類(lèi)型、鑒權(quán)、跨域攜帶憑證等
- 監(jiān)聽(tīng)各種事件,比如 onreadystatechange、onabort、onerror、ontimeout、onDownloadProgress、onUploadProgress 等
- 發(fā)送請(qǐng)求
默認(rèn)成功狀態(tài)碼是 status >= 200 & status < 300,也可通過(guò) validateStatus 自行設(shè)定。
http
- 一系列初始化工作
- http/https/data 等請(qǐng)求
取消請(qǐng)求
兩種方式可以取消請(qǐng)求:
- AbortController, 這種是以 fetch API 方式
const controller = new AbortController()
axios
.get('/foo', {
signal: controller.signal
})
.then(function (response) {
//...
})
// 取消請(qǐng)求
controller.abort() // 不支持 message 參數(shù)
- 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)思路。

在配置對(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)用兩次?

因?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)。