基類 Axios
跟隨入口 index.js 進入/lib/axios.js
,第一個方法則是createInstance
創建Axios
實例。先理解一些屬性后,再看 /core/Axios.js 的代碼。
- interceptors,攔截器 /core/InterceptorManager.js
interceptors.request,interceptors.response為InterceptorManager的實例
InterceptorManager
的本質是一個訂閱發布者模型
handlers
是收集訂閱者的容器
use
是訂閱方法,向容器中添加{ fulfilled, rejected },分別代表Promise的resolve和reject的兩種狀態
eject
是退訂方法
forEach
進行了重寫,綁定方法,遍歷通知訂閱回調函數的執行發布
- dispatchRequest,請求的觸發
dispatchRequest 的本質是調用了config中的adapter
方法,adapter在客戶端是返回一個Promise
,內部邏輯是對XMLHttpRequest
的封裝,服務端是一個基于Node.js
的 http server。后面會講到 adapter。
/core/dispatchRequest
module.exports = function dispathRequest(config) {
// ...
// config.adapter 返回Promise,在客戶端本質上是對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
是用于執行 XMLHttpRequest 中斷請求的方法abort
,內部通過高階函數實現,稍顯繞腦,作者的設計思路,尤其是外部調用 Promise 中的 resolve 方法讓人眼前一亮
,我們放在最后講。
基類Axios /core/Axios.js
function Axios(instanceConfig) {
// 緩存請求設置
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
}
}
// axios[method]實際上就是調用的request
Axios.prototype.request = function request(config) {
if (typeof config === 'string') {
// 滿足axios('example/url')調用
config = arguments[1] || {};
config.url = arguments[0]
} else {
config = config || {};
}
// ...
// 優先入參中的方法,其次為實例化時默認的方法,再次默認為 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);
});
// 攔截器響應訂閱放在dispatchRequest后
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// 攔截器依次執行,并修改原訂閱數組,觸發dispatchRequest,執行請求后,執行響應攔截器
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift())
}
return promise;
}
// 返回請求路徑,處理了get請求的queryString拼接
Axios.prototype.getUri = function (...) {
// ...
}
// ...
module.exports = Axios;
- methods,請求方法
優先參數設置,默認為GET
方法
'delete', 'get', 'head', 'options'方法類似于get,request參數中接收method,url但不接收data
'post', 'put', 'patch'方法類似于post,request參數中接收method,url以及data
axios[method]實際上就是調用的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 首先會創建一個默認請求設置的Axios
實例,默認設置中adapter
屬性在客戶端指向文件/adapters/xhr
,導出一個方法,即請求的發起 new XMLHttpRequest(),并返回一個Promise。
module.exports = function xhrAdapter(config) {
// ...
if (utils.isFormData(requestData)) {
// 如果提交的是form表單,則要瀏覽器去設置Content-Type,"multipart/form-data"
delete requestHeaders['Content-Type'];
}
// 實例化XMLHttpRequest對象
var request = new XMLHttpRequest();
if (config.auth) {
// ...
// 設置 Authorization 頭信息
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
// ...
// 初始化一個異步請求
request.open(method, url, true)
// 設置超時時間
request.timeout = confit.timeout;
// 當request的readyState變化時,觸發
// 0-UNSENT-代理被創建,但尚未調用open()方法
// 1-OPENED-open()方法已經被調用
// 2-HEADERS_RECEIVED-send()方法已經被調用,并且頭部和狀態已經可獲得
// 3-LOADING-下載中,responseText已包含部分數據
// 4-DONE-下載操作已完成
request.onreadystatechange = function handleLoad() {
// 處理已完成的請求
if (!request || request.readyState !==4) return;
// status-只讀狀態碼,請求完成前以及請求出錯,狀態碼均為0
// responseURL-響應的序列化URL
// 處理已正常完成,且響應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中可以設置屬性timeoutErrorMessage
// 這個屬性是axios官方沒有說的,定義用于reject提供的異常message
request.ontimeout = function...
// 配置XMLHttpRequest頭信息屬性 responseType, withCredentials等...
// 綁定進度函數 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狀態reject
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) return;
request.abort();
reject(cancel);
request = null;
})
}
// 發送請求
request.send(requestData)
}
請求的流程
到這里,整個請求的流程已經清晰了。
- 當執行axios(url)或者axios[method]對應的都是Axios中的request方法
- 攔截器interceptors收集訂閱,順序為,請求攔截器,dispatchRequest,響應攔截器
- 攔截器Promise.then(chain.shift())執行,首先執行請求攔截器,并改變原訂閱數組
- 直至dispatchRequest觸發config.adapter(客戶端是XMLHttpRequest, 返回Promise)
- 后繼續Promise.then(chain.shift()),執行響應攔截器,直至訂閱數組長度為0
在過程4,dispatchRequest觸發請求即XMLHttpRequest的執行過程是,open初始化,綁定所有方法,添加屬性和配置后,send發起請求。
過程中執行綁定的方法,非預期時reject;只有當readyState為4時,才有可能resolve拿到我們期望的數據。
常見的使用Axios的方法總是配合著then + catch
或async/await + catch
使用。
CancelToken,用于中斷取消請求
首先對比下CancelToken的源碼與CancelToken的使用方式
CancelToken 源碼 /cancel/CancelToken.js
function CancelToken(executor) {
if (typeof executor !== 'function') {
// executor必須是函數
throw new TypeError('executor must be a function.');
}
// 很關鍵??!
// promise可以在外部被調用resolve
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
// 標記1
executor(function cancel(message) {
// message是執行后面source.cancel()傳入的參數
if (token.reason) {
// dispatchRequest 已經從通過 config.adapter 接收到響應結果了,會調用下面的 throwIfRequested 方法
// 無法手動終止請求
return;
}
// reason 理解成一個非空字符串就好
token.reason = new Cancel(message);
// 很關鍵!!
// 非Promise內部執行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 的參數 function cancel(message) 就對應下面的參數 c
// 定義 cancel 來接收c
// ?。。∧敲矗琧ancel() 就可以調用 token.promise 中的 Promise.resolve
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
// token 中有 Promise
token: token,
// cancel 可以調用 token.promise 中的 Promise.resolve
cancel: cancel
};
};
module.exports = CancelToken;
example: 本質上就是 cancel 執行了 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)
// 執行了 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源碼解析到這里就結束了,希望大家能夠看明白,能夠喜歡!