目錄結構
│
├ lib/
│ ├ adapters/
│ ├ cancel/
│ ├ core/
│ ├ helpers/
│ ├ axios.js
│ ├ defaults.js // 預設配置
│ └ utils.js
│
├ index.js // 入口
複製程式碼
從入口開始
從入口我們可以知道axios提供了些什麼
1. 從webpack.config
得知入口檔案是index.js
// index.js
module.exports = require('./lib/axios');
複製程式碼
從這裡我們知道庫的入口檔案是
lib/axios.js
2. lib/axios.js
匯出了什麼
// lib/axios.js
module.exports = axios;
module.exports.default = axios;
複製程式碼
匯出了 "axios" 這個物件,並且還有相容import寫法
3. 匯出的axios
是啥
var defaults = require('./defaults');
function createInstance(defaultConfig) {
// ...
}
var axios = createInstance(defaults);
複製程式碼
這裡可以看出axios
是一個例項,並且使用了預設引數。
4. 匯出的axios
提供了啥
a. axios.Axios
// lib/axios.js
axios.Axios = Axios;
複製程式碼
這裡的Axios
是該例項的類
b. axios.create
// lib/axios.js
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
複製程式碼
axios
還提供了給使用者自己建立例項的方法,並且使用者傳入的config
能覆蓋預設config
(mergeConfig
的邏輯)
c. axios.Cancel
、axios.CancelToken
、axios.isCancel
// lib/axios.js
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
複製程式碼
axios
提供了取消請求的方法
d. axios.all
// lib/axios.js
axios.all = function all(promises) {
return Promise.all(promises);
};
複製程式碼
對Promise.all
的封裝
e. axios.spread
// lib/axios.js
axios.spread = require('./helpers/spread');
複製程式碼
引數解構的封裝,類似es6陣列的...
操作符
Axios類
從入口來看,我們發現了重點方法axios.create
,並且知道它是用方法createInstance
建立例項的。
// lib/axios.js
var Axios = require('./core/Axios');
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
// ...
}
複製程式碼
這裡用到了Axios類,並且它是整個庫的核心,所以,接下來我們看看這個類是怎麼定義的。
1. 構造器
// core/Axios.js
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
複製程式碼
構造器做了兩件事:i.初始化配置、ii.初始化攔截器。
2. 原型鏈方法
a. Axios.prototype.request
發起一個請求
傳送請求,以及對請求的處理,這部分我們放在下一節詳細分析。
b. Axios.prototype.getUri
獲取請求完整地址
c. Axios.prototype.get
,Axios.prototype.post
...
請求的別名
- 語法糖,使得請求呼叫更加的語義化,更加的方便,其實現是基於
Axios.prototype.request
的 - 對於
delete
,get
,head
,options
function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
}
複製程式碼
- 對於
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
}));
};
}
複製程式碼
3. Axios.prototype.request
這裡是axios的核心實現,包含了配置合併,請求傳送,攔截器加入
// core/Axios.js
Axios.prototype.request = function request(config) {
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
// ...
}
複製程式碼
對引數約定進行判斷,使得呼叫的時候可以更靈活。
比如:axios.request('api.example.com', config)
,axios.request(config)
。
// core/Axios.js
config = mergeConfig(this.defaults, config);
config.method = config.method ? config.method.toLowerCase() : 'get';
複製程式碼
合併配置,並且對方法有小寫處理,所以config傳入不用在意大小寫。
重點來了,這裡不僅處理了請求,還用了一個很巧妙的方法去處理請求和攔截器,以及攔截器之間的先後順序。
// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
複製程式碼
首先執行到迴圈前我們看看chain
是啥結構
[請求攔截1成功,請求攔截1失敗,...,dispatchRequest, undefined,響應攔截1成功,響應攔截1失敗,...]
複製程式碼
入口通過Promise.resolve(config)
將配置傳入,在經歷請求攔截器處理後,發起請求,請求成功獲得相應,再依次經歷響應攔截器處理。
dispatchRequest
發起請求的主要方法
處理URL
// core/dispatchRequest.js
if (config.baseURL && !isAbsoluteURL(config.url)) {
config.url = combineURLs(config.baseURL, config.url);
}
複製程式碼
區分相對路徑和絕對路徑
處理請求的data
// core/dispatchRequest.js
// Transform request data
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
複製程式碼
處理PUT
, POST
, PATCH
傳入的data
處理Headers
// core/dispatchRequest.js
// Flatten headers
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers || {}
);
複製程式碼
這裡的config已經是合併以及處理過的,這裡分了三個headers
- 預設的通用headers
- 請求方法對應的headers
- 使用者傳入的headers
優先順序依次遞增,將這三個headers合併平鋪到config.headers
下
緊接著刪除config.headers
下非http header的屬性
// core/dispatchRequest.js
utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);
複製程式碼
★請求介面卡★
在不同環境下,發起請求的方式可能會不一樣,比如:在Node環境下發起請求可能是基於http
模組,而在瀏覽器環境下發起請求又可能是基於XMLHttpRequest
物件
但是介面卡的存在,建立了一個通用的介面,它有通用的輸入輸出。即不用管內部實現的差異,只需要按照約定輸入,以及處理約定的輸出即可。
預設介面卡:
// core/dispatchRequest.js
var defaults = require('../defaults');
var adapter = config.adapter || defaults.adapter;
複製程式碼
// defaults.js
function getDefaultAdapter() {
var adapter;
// Only Node.JS has a process variable that is of [[Class]] process
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
}
return adapter;
}
var defaults = {
adapter: getDefaultAdapter()
// ...
}
複製程式碼
axios
內建了兩個介面卡adapters/http
,adapters/xhr
,在初始化預設配置時,它判斷了當前環境,並且應用了相應的介面卡。
當然你也可以傳入自己的介面卡,並且會被優先使用。介面卡可以應用於小程式開發等有自己的請求方法的場景。之後我們再來看看如何新建一個自定義介面卡。
接下來是處理請求介面卡的返回
// core/dispatchRequest.js
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
// Transform response data
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
複製程式碼
這裡就是簡單的處理了成功和失敗情況,格式化資料。
這裡的transformData
做的事就是將使用者傳入的config.transformResponse
全部執行一遍
throwIfCancellationRequested
後面再細講。
4. InterceptorManager
攔截管理器,攔截器的增刪,遍歷
構造器
function InterceptorManager() {
this.handlers = [];
}
複製程式碼
this.handlers
存的就是所有攔截器
InterceptorManager.prototype.use
// core/InterceptorManager.js
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};
複製程式碼
新增一個攔截器並且返回攔截器序號(id)
InterceptorManager.prototype.eject
// core/InterceptorManager.js
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
複製程式碼
根據序號(id)刪除一個攔截器
InterceptorManager.prototype.forEach
// core/InterceptorManager.js
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};
複製程式碼
遍歷攔截器
使用
- 在
Axios
的建構函式中,初始化了兩個攔截管理器
// core/Axios.js
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
複製程式碼
對於每一個Axios
例項,都可以訪問自己的兩個攔截管理器例項,對其進行增刪
- 在
Axios.prototype.request
方法中,將攔截器全部丟到佇列裡執行。
請求取消
首先我們舉個例子來看看如何取消請求
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
})
});
// cancel the request
cancel();
複製程式碼
在lib/axios.js
中
// lib/axios.js
axios.CancelToken = require('./cancel/CancelToken');
複製程式碼
接下來我們看看CancelToken
類做了什麼
CancelToken類
1. 構造器
// cancel/CancelToken.js
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
複製程式碼
構造器中用到了兩個屬性
cancelToken.promise
cancelToken.reason
構造器接收一個引數executor
,並且在構造器最後執行,傳入一個cancel function
作為引數
即在例子中傳入的config
{
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
}
複製程式碼
這裡的c
引用的就是那個cancel function
這個cancel function
可以隨時呼叫,並且在呼叫後,會將cancelToken.promise
reslove掉,這有什麼用嗎你可能會問。
// adapters/xhr.js
var request = new XMLHttpRequest();
// ...
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
// ...
});
複製程式碼
當呼叫cancel function
的時候,會終止掉請求,這就是cancel的實現原理
2. CancelToken.source
// cancel/CancelToken.js
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
複製程式碼
靜態方法source
,自動新建cancelToken例項,並且將cancel function
繫結返回
3. CancelToken.prototype.throwIfRequested
例項方法
// cancel/CancelToken.js
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
複製程式碼
當取消時,拋錯
在Axios.prototype.request
的dispatchRequest
中
// core/dispathRequest.js
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
// ...
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// ...
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
// ...
}
});
}
複製程式碼
在請求前後都呼叫了throwIfCancellationRequested
方法,在請求前取消不會發起請求,在請求後取消導致reject
Cancel類
1. 構造器
function Cancel(message) {
this.message = message;
}
複製程式碼
2. 重寫了toString
方法
3. Cancel.prototype.__CANCEL__
預設為true
4. 使用
// cancel/CancelToken.js
token.reason = new Cancel(message);
複製程式碼
token.reason
是Cancel
的例項,它表示了cancelToken的狀態。而驗證狀態是由接下來這個方法實現的。
isCancel方法
module.exports = function isCancel(value) {
return !!(value && value.__CANCEL__);
};
複製程式碼
xhrAdapter
瀏覽器端的請求介面卡
// adapters/xhr.js
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest() {})
})
複製程式碼
接收配置config
作為引數,返回一個promise
dispatchXhrRequest
就是ajax的封裝,來看看他是怎麼封裝的
- 首先,最基礎的一個請求建立,傳送流程
// adapters/xhr.js
var requestData = config.data;
var requestHeaders = config.headers;
var request = new XMLHttpRequest();
// ...
request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true);
// ...
request.onreadystatechange = function handleLoad() {}
// ...
// 設定headers
if ('setRequestHeader' in request) {
utils.forEach(requestHeaders, function setRequestHeader(val, key) {
if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
// Remove Content-Type if data is undefined
delete requestHeaders[key];
} else {
// Otherwise add header to the request
request.setRequestHeader(key, val);
}
});
}
// ...
if (requestData === undefined) {
requestData = null;
}
request.send(requestData);
複製程式碼
- 設定超時
// adapters/xhr.js
request.timeout = config.timeout;
// ...
request.ontimeout = function handleTimeout() {
reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',
request));
// Clean up request
request = null;
};
// ...
複製程式碼
- 上傳下載進度
// adapters/xhr.js
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}
// ...
複製程式碼
- 取消請求,之前已經提及過
// adapters/xhr.js
if (config.cancelToken) {
// ...
}
複製程式碼
- 處理錯誤
request.onerror = function handleError() {
// Real errors are hidden from us by the browser
// onerror should only fire if it's a network error
reject(createError('Network Error', config, null, request));
// Clean up request
request = null;
};
複製程式碼
- 處理中斷
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(createError('Request aborted', config, 'ECONNABORTED', request));
// Clean up request
request = null;
};
複製程式碼
由於在手動觸發cancel時有reject,因此這裡判斷當沒有request的時候不重複reject
wxAdapter
在處理非預設環境時,可以自定義介面卡
import axios from 'axios'
const wxrequest = axios.create({
adapter: function (config) {
return new Promise(function (resolve, reject) {
var response = {
statusText: '',
config: config
}
var request = wx.request({
url: config.url,
data: config.data,
method: config.method.toUpperCase(),
header: config.headers,
responseType: config.responseType,
success(res) {
response.data = res.data
response.status = res.statusCode
response.headers = res.headers
resolve(response)
},
fail(err) {
reject(err)
request = null
}
})
response.request = request
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return
}
request.abort()
reject(cancel)
request = null
})
}
})
}
})
export default wxrequest
複製程式碼
在介面卡中加入取消功能,格式化返回資料
總結
攔截器和介面卡的設計,使得axios十分的靈活,更易擴充套件,其實現方式也值得學習使用。