基類 Axios
跟隨入口 index.js 進入/lib/axios.js
验懊,第一個方法則是createInstance
創(chuàng)建Axios
實例擅羞。先理解一些屬性后尸变,再看 /core/Axios.js 的代碼。
- interceptors减俏,攔截器 /core/InterceptorManager.js
interceptors.request召烂,interceptors.response為InterceptorManager的實例
InterceptorManager
的本質(zhì)是一個訂閱發(fā)布者模型
handlers
是收集訂閱者的容器
use
是訂閱方法,向容器中添加{ fulfilled, rejected }娃承,分別代表Promise的resolve和reject的兩種狀態(tài)
eject
是退訂方法
forEach
進行了重寫奏夫,綁定方法怕篷,遍歷通知訂閱回調(diào)函數(shù)的執(zhí)行發(fā)布
- dispatchRequest,請求的觸發(fā)
dispatchRequest 的本質(zhì)是調(diào)用了config中的adapter
方法酗昼,adapter在客戶端是返回一個Promise
廊谓,內(nèi)部邏輯是對XMLHttpRequest
的封裝,服務(wù)端是一個基于Node.js
的 http server麻削。后面會講到 adapter蒸痹。
/core/dispatchRequest
module.exports = function dispathRequest(config) {
// ...
// config.adapter 返回Promise,在客戶端本質(zhì)上是對XMLHttpRequest的封裝
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
// ...
return response;
}, function onAdapterRejection(reason) {
// ...
return Promise.reject(reason)
})
}
- cancelToken 取消請求的令牌
cancelToken
是用于執(zhí)行 XMLHttpRequest 中斷請求的方法abort
呛哟,內(nèi)部通過高階函數(shù)實現(xiàn)叠荠,稍顯繞腦,作者的設(shè)計思路扫责,尤其是外部調(diào)用 Promise 中的 resolve 方法讓人眼前一亮
榛鼎,我們放在最后講。
基類Axios /core/Axios.js
function Axios(instanceConfig) {
// 緩存請求設(shè)置
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
}
}
// axios[method]實際上就是調(diào)用的request
Axios.prototype.request = function request(config) {
if (typeof config === 'string') {
// 滿足axios('example/url')調(diào)用
config = arguments[1] || {};
config.url = arguments[0]
} else {
config = config || {};
}
// ...
// 優(yōu)先入?yún)⒅械姆椒ū罟拢浯螢閷嵗瘯r默認的方法者娱,再次默認為 GET請求
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
// 攔截器請求訂閱放在dispatchRequest前
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 攔截器響應(yīng)訂閱放在dispatchRequest后
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// 攔截器依次執(zhí)行,并修改原訂閱數(shù)組苏揣,觸發(fā)dispatchRequest肺然,執(zhí)行請求后,執(zhí)行響應(yīng)攔截器
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift())
}
return promise;
}
// 返回請求路徑腿准,處理了get請求的queryString拼接
Axios.prototype.getUri = function (...) {
// ...
}
// ...
module.exports = Axios;
- methods际起,請求方法
優(yōu)先參數(shù)設(shè)置,默認為GET
方法
'delete', 'get', 'head', 'options'方法類似于get吐葱,request參數(shù)中接收method,url但不接收data
'post', 'put', 'patch'方法類似于post街望,request參數(shù)中接收method,url以及data
axios[method]實際上就是調(diào)用的request({ method, url, ... })
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
Axios.prototype[method] = function(url, data, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data
}));
};
});
defaultConfig adapter - xhrAdapter,客戶端XMLHttpRequest
/lib/axios.js 首先會創(chuàng)建一個默認請求設(shè)置的Axios
實例弟跑,默認設(shè)置中adapter
屬性在客戶端指向文件/adapters/xhr
灾前,導(dǎo)出一個方法都弹,即請求的發(fā)起 new XMLHttpRequest()网杆,并返回一個Promise。
module.exports = function xhrAdapter(config) {
// ...
if (utils.isFormData(requestData)) {
// 如果提交的是form表單啤挎,則要瀏覽器去設(shè)置Content-Type饲嗽,"multipart/form-data"
delete requestHeaders['Content-Type'];
}
// 實例化XMLHttpRequest對象
var request = new XMLHttpRequest();
if (config.auth) {
// ...
// 設(shè)置 Authorization 頭信息
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
// ...
// 初始化一個異步請求
request.open(method, url, true)
// 設(shè)置超時時間
request.timeout = confit.timeout;
// 當request的readyState變化時炭玫,觸發(fā)
// 0-UNSENT-代理被創(chuàng)建,但尚未調(diào)用open()方法
// 1-OPENED-open()方法已經(jīng)被調(diào)用
// 2-HEADERS_RECEIVED-send()方法已經(jīng)被調(diào)用貌虾,并且頭部和狀態(tài)已經(jīng)可獲得
// 3-LOADING-下載中吞加,responseText已包含部分數(shù)據(jù)
// 4-DONE-下載操作已完成
request.onreadystatechange = function handleLoad() {
// 處理已完成的請求
if (!request || request.readyState !==4) return;
// status-只讀狀態(tài)碼,請求完成前以及請求出錯,狀態(tài)碼均為0
// responseURL-響應(yīng)的序列化URL
// 處理已正常完成衔憨,且響應(yīng)URL為非文件的請求
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) return;
var response = {
data: requset.responseType === 'text' ? requset.responseText : request.response,
status: request.status,
statusText: request.statusText,
headers: parseHeaders(request.getAllResponseHeaders()),
config: config,
request: request
}
resolve(response);
request = null;
}
// 請求終止
request.onabort = function ...
// 請求異常
request.onerror = function ...
// 請求超時叶圃,config中可以設(shè)置屬性timeoutErrorMessage
// 這個屬性是axios官方?jīng)]有說的,定義用于reject提供的異常message
request.ontimeout = function...
// 配置XMLHttpRequest頭信息屬性 responseType, withCredentials等...
// 綁定進度函數(shù) config.onDownloadProgress config.onUploadProgress
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}
// 上傳進度還需要判斷瀏覽器是否支持践图,loadstart, loadend, progress等進度都需要綁定在upload上
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress)
}
// 取消令牌掺冠,終止請求,Promise狀態(tài)reject
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) return;
request.abort();
reject(cancel);
request = null;
})
}
// 發(fā)送請求
request.send(requestData)
}
請求的流程
到這里码党,整個請求的流程已經(jīng)清晰了赫舒。
- 當執(zhí)行axios(url)或者axios[method]對應(yīng)的都是Axios中的request方法
- 攔截器interceptors收集訂閱,順序為闽瓢,請求攔截器接癌,dispatchRequest,響應(yīng)攔截器
- 攔截器Promise.then(chain.shift())執(zhí)行扣讼,首先執(zhí)行請求攔截器缺猛,并改變原訂閱數(shù)組
- 直至dispatchRequest觸發(fā)config.adapter(客戶端是XMLHttpRequest, 返回Promise)
- 后繼續(xù)Promise.then(chain.shift()),執(zhí)行響應(yīng)攔截器椭符,直至訂閱數(shù)組長度為0
在過程4荔燎,dispatchRequest觸發(fā)請求即XMLHttpRequest的執(zhí)行過程是,open初始化销钝,綁定所有方法有咨,添加屬性和配置后,send發(fā)起請求蒸健。
過程中執(zhí)行綁定的方法座享,非預(yù)期時reject;只有當readyState為4時似忧,才有可能resolve拿到我們期望的數(shù)據(jù)渣叛。
常見的使用Axios的方法總是配合著then + catch
或async/await + catch
使用。
CancelToken盯捌,用于中斷取消請求
首先對比下CancelToken的源碼與CancelToken的使用方式
CancelToken 源碼 /cancel/CancelToken.js
function CancelToken(executor) {
if (typeof executor !== 'function') {
// executor必須是函數(shù)
throw new TypeError('executor must be a function.');
}
// 很關(guān)鍵4狙谩!
// promise可以在外部被調(diào)用resolve
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
// 標記1
executor(function cancel(message) {
// message是執(zhí)行后面source.cancel()傳入的參數(shù)
if (token.reason) {
// dispatchRequest 已經(jīng)從通過 config.adapter 接收到響應(yīng)結(jié)果了饺著,會調(diào)用下面的 throwIfRequested 方法
// 無法手動終止請求
return;
}
// reason 理解成一個非空字符串就好
token.reason = new Cancel(message);
// 很關(guān)鍵s锱省!
// 非Promise內(nèi)部執(zhí)行CancelToken.promise的Promise.resolve(token.reason)
resolvePromise(token.reason);
});
}
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
CancelToken.source = function source() {
var cancel;
// 定義 token 接收一個 CancelToken 實例
// 上文的標記1中的 executor 的參數(shù) function cancel(message) 就對應(yīng)下面的參數(shù) c
// 定義 cancel 來接收c
// S姿ァQヵ恕!那么塑顺,cancel() 就可以調(diào)用 token.promise 中的 Promise.resolve
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
// token 中有 Promise
token: token,
// cancel 可以調(diào)用 token.promise 中的 Promise.resolve
cancel: cancel
};
};
module.exports = CancelToken;
example: 本質(zhì)上就是 cancel 執(zhí)行了 token.promise 中的 Promise.resolve
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// cancel the request (the message parameter is optional)
// 執(zhí)行了 config.cancelToken.promise 中的 Promise.resolve
source.cancel('手動中斷請求'');
Promise.resolve那么然后呢汤求?還記得最初提到的XMLHttpRequest的abort方法嗎俏险?
xhr /adapters/xhr.js
// ...
// 都清晰了吧
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) return;
request.abort();
reject(cancel);
request = null;
})
}
// ...
好啦严拒!Axios源碼解析到這里就結(jié)束了扬绪,希望大家能夠看明白,能夠喜歡裤唠!