0、寫在前面
先掌握源碼結構再到實際的運行使用中去復盤源碼。就是 源碼—>使用—>源碼 的學習線路。
思維導圖配合文章更清晰
0.1 取源碼
源碼取到打開已經打包好的文件 dist/axios.js 看一下,注釋加上空行也就兩千行不到。
git clone https://github.com/axios/axios.git
0.2 入口文件
打開package.json找到入口為index.js
index.js
module.exports = require('./lib/axios');
主要看的就是紅框里這部分內容了。
由上至下大致分析一下文件及文件夾的主要內容。
adapter:適配器,瀏覽器環境用xhr,node環境用http
cancel:取消請求
core:核心代碼
helpers:功能助手
axios.js:聲明定義文件
default.js:默認的配置參數
utils.js:一些小工具函數集合
下面我們就從定義文件開始看起。
1、axios.js
1.1 直接找到聲明語句開始
var axios = createInstance(defaults);
1.1.1 看傳入的默認配置 defaults 定義了哪些東西
adapter:適配器根據運行環境選擇哪種請求方式(瀏覽器用xhr—lib/adapters/xhr.js,node環境用http—lib/adapters/http.js)
transformRequest:請求數據處理方法數組
transformResponse:響應數據處理方法數組
timeout:響應最長返回時間,超過這個時間就取消請求
xsrfCookieName:xsrf在cookie內名稱
xsrfHeaderName:xsrf在HTTP頭內名稱
maxContentLength:允許的響應內容的最大尺寸
maxBodyLength:(僅在node中生效)允許發送HTTP請求內容的最大尺寸
validateStatus:是否 2** 類型的狀態碼
headers:定義了共用的請求頭 common 和各種不同請求方法的請求頭(用了兩個循環完成)
再看 function createInstance(defaultConfig) 里面的語句,也就5行代碼,一行一行看。
1.1.2 返回 Axios 對象
var context = new Axios(defaultConfig);
打開 core/Axios.js 查看 Axios 所擁有的屬性方法:
defaults:剛剛的默認配置賦值;
interceptors:攔截器,包括 request、response 兩個對象;
request:發送的請求處理,里面主要處理有合并默認配置和自定義配置,確認請求方法,攔截器處理,最后循環執行攔截器至promise對象并返回該對象;
getUri:獲取請求的完整地址;
兩個forEach:循環定義HTTP請求方法(delete、get、head、options、post、put、patch),注意前四個與后三個傳參的不同 function(url, config) 和 function(url, data, config)。另外再看一下攔截器 interceptors 所使用的對象,在 InterceptorManager.js中:
handlers:攔截處理數組;
use:增加一個攔截器;
eject:移除一個攔截器;
forEach:遍歷處理,將有效的攔截器綁定上處理方法(這個可以在 Axios 對象方法 request 里看攔截器處理那段)。
1.1.3 定義返回 wrap 函數
var instance = bind(Axios.prototype.request, context);
打開 helpers/bind.js 查看 bind 函數,得到這里 instance 是被定義成了一個 wrap 函數,該函數返回值是Axios.prototype.request 方法調用結果,并且 request 內部this指向傳入的值 context,傳入參數為 args ,另外在前面我們看到這個 request 結果是返回 promise 類型對象的。
module.exports = function bind(fn, thisArg) {
return function wrap() {
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return fn.apply(thisArg, args);
};
};
1.1.4 這是將 Axios.prototype 定義的 request、getUri以及delete、get、head、options、post、put、patch 都以返回 wrap 函數的方式復制到 intance 上,形同上面的 bind(Axios.prototype.request, context)。
utils.extend(instance, Axios.prototype, context);
1.1.5 將創建的 context 對象擴展復制到 instance 上
utils.extend(instance, context);
1.1.6 返回 instance ,即 wrap 函數賦值給變量 axios
return instance;
1.2 將 Axios 暴露出來,允許繼承
axios.Axios = Axios;
1.3 實例函數工廠,用這個可以建立自己的默認配置和攔截器等的實例
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
1.4 定義取消請求方法
具體使用看下一節 2.4。
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
1.5 定義多請求方法,需要可以與spread配合將返回值數組包裝成多個變量返回值形式
這里結合后面的 2.5 使用更清楚。
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
以上就把整個axios從新建到內部核心動作大致看完了,下面我們通過使用再具體看一下實際使用過程中的運行情況。
2、從使用運行回到源碼學習
2.1 安裝
安裝有兩種方式,一種是包管理,一種是 script 標簽引入,不管是哪種安裝,我們都會得到一個全局變量 axios(包管理使用時需要自己導入定義一下)。
2.2 使用
2.2.1 通過方法名
看一下常用的 get 示例請求
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
})
先看為什么可以直接用 get 這個方法名呢?
這個就是 1.1.4 那節的代碼 utils.extend(instance, Axios.prototype, context); 的作用,把 request、getUri以及delete、get、head、options、post、put、patch 都復制到了 axios 上,所以同樣的道理可以得到以下幾種請求方法,getUri 不是請求方法也不咋用到在這里先忽略。
axios.request(config)
axios.delete(url[, config])
axios.get(url[, config])
axios.head(url[, config])
axios.options(url[, config]])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
現在我們通過上面的示例get請求來看代碼是怎么運行的。
Axios.js 中是通過以下代碼來定義 get 請求的,那么示例代碼中唯一的實參 '/user?ID=12345' 就代表著定義中的形參 url ,并沒有傳入config的實參。
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
}));
};
});
定義里返回的是this.request(......)方法,我們之前在 1.1.3 說過這個 this 是指向傳入的 context 的,這個 this.request 就是里面的 request 方法 ,是返回 promise 對象的,所以在示例里我們可以接著 axios.get('/user?ID=12345') 寫 then和catch 方法。
再看 this.request 里對傳入參數的處理。
config || {}:這里我們沒有傳入 congfig 實參,所以 config 是 undefined ,這個結果就是后面的空對象 {}
{method: method,url: url}:method 是定義方法時就賦值的,跟定義的方法同名,url就是我們傳入的 '/user?ID=12345'
utils.merge(.....):這個方法就是把上面的兩個對象合并成一個對象傳入 this.request
程序走到這里合成的這個對象應該如下:
{
method:"get",
url:"/user?ID=12345"
}
看下 request 定義代碼,這里的 function request(config) 中的 config 就是上面合成的對象值。
Axios.prototype.request = function request(config) {
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
config = mergeConfig(this.defaults, config);
// Set config.method
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
// 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;
};
先略掉開頭一小段,其中有個 config 是否為 string 類型的判斷和請求方法methods的處理,這個主要是 axios('/user?ID=12345') 使用形式的,我們待會說?,F在從 var chain = [dispatchRequest, undefined]; 看起。
這是定義了一個攔截器數組 chain ,并且給了初始值 [dispatchRequest, undefined]。
var promise = Promise.resolve(config);
定義 promise 對象,方便后面的鏈式調用。
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);
});
增加請求和響應攔截器,我們沒有另外定義,這里過后 chain 是沒有變化的。
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
開始調用運行 chain 數據,這里參數為什么是 (chain.shift(), chain.shift()) 呢,可以看下上兩句請求攔截器中(interceptor.fulfilled, interceptor.rejected) 成功與失敗處理函數都是成對添加到數組的,所以這也就解釋了為什么初始化 chain 的時候是 dispatchRequest 和一個undefined 的了。當然是為了保證后續的響應攔截器的成功與失敗處理函數是成對的。
這個 dispatchRequest 定義在 dispatchRequest.js 文件里。我們對照源碼一一分析。
throwIfCancellationRequested(config);
......
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
.....
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
在處理請求前先判斷有沒有取消該請求,有了就拋出異常。
// Ensure headers exist
config.headers = config.headers || {};
定義賦值請求頭,在get示例我們并沒有定義,所以這里的 config.headers 只有之前 default.js 文件中預先定義的部分,即:
headers:{
common:{
'Accept': 'application/json, text/plain, */*'
},
delete:{},
get:{},
head:{},
post:{
'Content-Type': 'application/x-www-form-urlencoded'
},
put:{
'Content-Type': 'application/x-www-form-urlencoded'
},
patch:{
'Content-Type': 'application/x-www-form-urlencoded'
},
}
// Transform request data
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
格式化請求數據,但是我們的get示例是沒有請求 data 的,這里經過一圈處理 config.data 為 undefined。
// Flatten headers
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers
);
將幾個對象合成一個對象賦值給 config.headers。
utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);
刪除默認的 headers 配置
var adapter = config.adapter || defaults.adapter;
賦值適配器,需返回promise對象的,我們沒有定義,就用默認的 default.adapter。
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);
});
終于到這了,config 進入 adapter
adapter: getDefaultAdapter()
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}
打開兩個文件看下都是返回的 new Promise(),如果調用過取消請求的方法會執行 function onAdapterRejection(reason) ,我們這里自然進入到了 function onAdapterResolution(response) ,這里就得到帶有響應值的 promise 對象,這就保證了示例get中 axios.get() 后還可以再添加 then 方法。
關于 xhr.js 和 http.js 后續會出專門的文章解釋。
2.2.2 參數調用
我們平時還會用一下的兩方式去調用
axios("/user?ID=12345")
axios({
url:"/user?ID=12345",
method:'get" //post等方法一樣
})
為什么我們可以直接用這種形式調用呢,請看 1.1.3 var instance = bind(Axios.prototype.request, context); 這個在定義的時候就把 request 這個給復制給了 變量本身,所以 axios() 中是直接用的 Axios.prototype.request 方法。
再看Axios.prototype.request中之前略過的幾行代碼,就是為了這種類型的使用做的判斷。
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
config = mergeConfig(this.defaults, config);
// Set config.method
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
第一個判斷, axios("/user?ID=12345") 中的config 就是 string 類型。
第二個判斷,axios("/user?ID=12345") 中沒有定義請求方法所以默認get。
經過第二個判斷處理以后我們知道會擴展識別 config 的 method 屬性,也就是說雖然 default.js 里沒有定義到,但是程序處理的時候程序是可以讀取或者建立 method 這個屬性的,所以 axios({ url:"/user?ID=12345"}) 、axios({ url:"/user?ID=12345",method:'post"}) 也都是可以的。
另外中間還有有個 mergeConfig 方法定義在 mergeConfig.js 中,那么在這個文件里,我們看到所有可以設置的配置參數。
var valueFromConfig2Keys = ['url', 'method', 'data'];
var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params'];
var defaultToConfig2Keys = [
'baseURL', 'url', 'transformRequest', 'transformResponse', 'paramsSerializer',
'timeout', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName',
'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress',
'maxContentLength', 'maxBodyLength', 'validateStatus', 'maxRedirects', 'httpAgent',
'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding'
];
這里比較單一自己多看看。
2.3 axios.create()——自己創建 axios 實例
這個在文章 四、vue+ElementUI開發后臺管理模板—方法指令、接口數據 第2大點 獲取接口數據、模擬數據 里實際運用了,這里不再多說。
2.4 axios.CancelToken——取消請求
如果我們瀏覽SAP頁面,在資源大、多或者響應比較慢的頁面在發生跳轉后,前一個頁面的請求還會繼續等待響應,那最好的是到新頁面后,之前的請求應該全部取消,將資源留給新頁面用。
我們先用 GitHub/chizijijiadami/vue-elementui-5 的代碼看下這樣的請求效果,這個是要配合之前用 phpstudy 寫的跨域接口的,具體見文章 四、vue+ElementUI開發后臺管理模板—方法指令、接口數據 底部部分。
獲取代碼后,做如下修改:
src>pages>Index>index.vue
created() {
this.getList();
- //this.getCrossDomainList();
+ this.getCrossDomainList();
},
src>pages>List>Detail>index.vue
+ created(){
+ console.log('list-detail');
+ }
對phpstudy中建立的網站文件增加一句 sleep(5) 代表延時5秒鐘返回,可以模擬服務器延時情況。
上面這些修改后運行項目,打開首頁后立即切換至詳情頁,在等待數秒后,控制臺如下圖。
要想實現跳轉至新頁面后,立即取消前面頁面的請求,我們可以做如下操作:
src>data>store>modules>app.js
system: {
title: "大米工廠",
+ requestCancel:[]
},
......
mutations: {
+ ADD_SYSTEM_REQUEST_CANCEL:(state,c)=>{
+ state.system.requestCancel.push(c)
+ },
+ SET_SYSTEM_REQUEST_CANCEL:state=>{
+ state.system.requestCancel=[]
+ },
......
},
actions: {
+ addSystemRequestCancel({ commit },c){
+ commit('ADD_SYSTEM_REQUEST_CANCEL',c)
+ },
+ setSystemRequestCancel({ commit }){
+ commit('SET_SYSTEM_REQUEST_CANCEL')
+ },
......
}
src>common>utils>axiosApi.js,給每個請求添加取消方法。
import store from 'data/store'
import axios from 'axios'
import ErrorMessage from './errorMessage'
import { MessageBox } from 'element-ui'
var instance = axios.create({
baseURL: '',
timeout: 5000
});
+ const CancelToken = axios.CancelToken;
// 添加請求攔截器
instance.interceptors.request.use(
// 在發送請求之前做些什么
config => {
config.headers['version'] = '1'
+ config.cancelToken = new CancelToken((c) => {
+ store.dispatch('addSystemRequestCancel', c)
+ });
return config
},
error => {
Promise.reject(error)
}
);
src>common>routerFilter>index.js,登錄權限信息肯定是不能取消的,這里應該在登錄后實行這種機制。
傳遞取消信息,并且每到一個新頁面前 requestCancel 數組應該清零。
if (to.path === '/login') {
next('/')
} else {
- next()
+ store.getters.app.system.requestCancel.forEach(cancel => {
+ cancel('Cancel')
+ })
+ store.dispatch("setSystemRequestCancel").then(() => {
+ next()
+ })
}
現在我們再運行切換頁面,看下控制臺在進入 detail 頁之前那個跨域請求被取消報了錯,并且彈出了錯誤提示,這里就要進一步修改,取消請求的情況是不應該彈窗報錯的
src>common>utils>axiosApi.js
// 對響應錯誤做點什么
if (!error.response) {
// 服務器請求失敗時錯誤提示
+ if (error.message !== 'Cancel') {
MessageBox({
message: `請求超時${ErrorMessage.API_ERROR_LOAD}`,
showCancelButton: false,
confirmButtonText: '確定',
type: 'error',
callback() { }
})
+ }
}
src>pages>Index>index.vue,這里添加錯誤處理代碼
getCrossDomainList() {
api
.getCrossDomainList()
.then(res => {
console.log(res);
})
+ .catch(() => { });
}
以上修改完成后我們再切換看看,沒有任何報錯了
這里是axios 取消的一種方式,還有另一種使用方式差不多,自己試試這里就不多說了。
2.5 axios.all()——多請求并發
src>pages>Index>index.vue
created() {
// this.getList();
// this.getCrossDomainList();
+ this.all();
},
methods:{
......
all() {
axios.all([api.getList(), api.getCrossDomainList()]).then(arr => {
console.log(arr, "arr");
});
axios.all([api.getList(), api.getCrossDomainList()]).then(
axios.spread(function(one, two) {
console.log(one, two, "spread");
})
);
}
}
運行結果如下圖,就是拆分了返回值數組。還有一點可以看到這個all里面會等請求執行成功以后才一起返回,如果取消了其中一個,就會返回 reject。
感謝閱讀,喜歡的話點個贊吧:)
更多內容請關注后續文章。。。