前言
這是
學習源碼整體架構系列
第六篇。整體架構這詞語好像有點大,姑且就算是源碼整體結構吧,主要就是學習是代碼整體結構,不深究其他不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。
學習源碼整體架構系列
文章如下:
1.學習 jQuery 源碼整體架構,打造屬于自己的 js 類庫
2.學習underscore源碼整體架構,打造屬于自己的函數式編程類庫
3.學習 lodash 源碼整體架構,打造屬于自己的函數式編程類庫
4.學習 sentry 源碼整體架構,打造屬于自己的前端異常監控SDK
5.學習 vuex 源碼整體架構,打造屬于自己的狀態管理庫
感興趣的讀者可以點擊閱讀。下一篇可能是vue-router
源碼。
本文比較長,手機上閱讀,可以滑到有圖的地方直接看文中的幾張圖即可。建議點贊或收藏后在電腦上閱讀,按照文中調試方式自己調試或許更容易吸收消化。
導讀
文章詳細介紹了 axios
調試方法。詳細介紹了 axios
構造函數,攔截器,取消等功能的實現。最后還對比了其他請求庫。
本文學習的版本是v0.19.0
。克隆的官方倉庫的master
分支。截至目前(2019 年 12 月 14 日),最新一次commit
是2019-12-09 15:52 ZhaoXC
dc4bc49673943e352
,fix: fix ignore set withCredentials false (#2582)
。
本文倉庫在這里若川的 axios-analysis github 倉庫。求個star
呀。
如果你是求職者,項目寫了運用了axios
,面試官可能會問你:
1.為什么
axios
既可以當函數調用,也可以當對象使用,比如axios({})
、axios.get
。
2.簡述axios
調用流程。
3.有用過攔截器嗎?原理是怎樣的?
4.有使用axios
的取消功能嗎?是怎么實現的?
5.為什么支持瀏覽器中發送請求也支持node
發送請求?
諸如這類問題。
chrome 和 vscode 調試 axios 源碼方法
前不久,筆者在知乎回答了一個問題一年內的前端看不懂前端框架源碼怎么辦?推薦了一些資料,閱讀量還不錯,大家有興趣可以看看。主要有四點:
1.借助調試
2.搜索查閱相關高贊文章
3.把不懂的地方記錄下來,查閱相關文檔
4.總結
看源碼,調試很重要,所以筆者詳細寫下 axios
源碼調試方法,幫助一些可能不知道如何調試的讀者。
chrome 調試瀏覽器環境的 axios
調試方法
axios
打包后有sourcemap
文件。
# 可以克隆筆者的這個倉庫代碼
git clone https://github.com/lxchuan12/axios-analysis.git
cd axios-analaysis/axios
npm install
npm start
# open [http://localhost:3000](http://localhost:3000)
# chrome F12 source 控制面板 webpack// . lib 目錄下,根據情況自行斷點調試
本文就是通過上述的例子axios/sandbox/client.html
來調試的。
順便簡單提下調試example
的例子,雖然文章最開始時寫了這部分,后來又刪了,最后想想還是寫下。
找到文件axios/examples/server.js
,修改代碼如下:
server = http.createServer(function (req, res) {var url = req.url;// 調試 examplesconsole.log(url);// Process axios itselfif (/axios\.min\.js$/.test(url)) {// 原來的代碼 是 axios.min.js// pipeFileToResponse(res, '../dist/axios.min.js', 'text/javascript');pipeFileToResponse(res, '../dist/axios.js', 'text/javascript');return;}// 原來的代碼 是 axios.min.map// if (/axios\.min.map$/.test(url)) {if (/axios\.map$/.test(url)) {// 原來的代碼 是 axios.min.map// pipeFileToResponse(res, '../dist/axios.min.map', 'text/javascript');pipeFileToResponse(res, '../dist/axios.map', 'text/javascript');return;}
}
# 上述安裝好依賴后
# npm run examples 不能同時開啟,默認都是3000端口
# 可以指定端口 5000
# npm run examples === node ./examples/server.js
node ./examples/server.js -p 5000
打開http://localhost:5000,然后就可以開心的在Chrome
瀏覽器中調試examples
里的例子了。
axios
是支持 node
環境發送請求的。接下來看如何用 vscode
調試 node
環境下的axios
。
vscode 調試 node 環境的 axios
在根目錄下 axios-analysis/
創建.vscode/launch.json
文件如下:
{// 使用 IntelliSense 了解相關屬性。// 懸停以查看現有屬性的描述。// 欲了解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387"version": "0.2.0","configurations": [{"type": "node","request": "launch","name": "Launch Program","program": "${workspaceFolder}/axios/sandbox/client.js","skipFiles": ["<node_internals>/**"]},]
}
按F5
開始調試即可,按照自己的情況,單步跳過(F10)
、單步調試(F11)
斷點調試。
其實開源項目一般都有貢獻指南axios/CONTRIBUTING.md
,筆者只是把這個指南的基礎上修改為引用sourcemap
的文件可調試。
先看 axios 結構是怎樣的
git clone https://github.com/lxchuan12/axios-analysis.git
cd axios-analaysis/axios
npm install
npm start
按照上文說的調試方法, npm start
后,直接在 chrome
瀏覽器中調試。打開 http://localhost:3000,在控制臺打印出axios
,估計很多人都沒打印出來看過。
console.log({axios: axios});
層層點開來看,axios
的結構是怎樣的,先有一個大概印象。
筆者畫了一張比較詳細的圖表示。
看完結構圖,如果看過jQuery
、underscore
和lodash
源碼,會發現其實跟axios
源碼設計類似。
jQuery
別名 $
,underscore
loadsh
別名 _
也既是函數,也是對象。比如jQuery
使用方式。$('#id')
, $.ajax
。
接下來看具體源碼的實現。可以跟著斷點調試一下。
斷點調試要領:
賦值語句可以一步跳過,看返回值即可,后續詳細再看。
函數執行需要斷點跟著看,也可以結合注釋和上下文倒推這個函數做了什么。
axios 源碼 初始化
看源碼第一步,先看package.json
。一般都會申明 main
主入口文件。
// package.json
{"name": "axios","version": "0.19.0","description": "Promise based HTTP client for the browser and node.js","main": "index.js",// ...
}
主入口文件
// index.js
module.exports = require('./lib/axios');
lib/axios.js
主文件
axios.js
文件 代碼相對比較多。分為三部分展開敘述。
第一部分:引入一些工具函數
utils
、Axios
構造函數、默認配置defaults
等。第二部分:是生成實例對象
axios
、axios.Axios
、axios.create
等。第三部分取消相關 API 實現,還有
all
、spread
、導出等實現。
第一部分
引入一些工具函數utils
、Axios
構造函數、默認配置defaults
等。
// 第一部分:
// lib/axios
// 嚴格模式
'use strict';
// 引入 utils 對象,有很多工具方法。
var utils = require('./utils');
// 引入 bind 方法
var bind = require('./helpers/bind');
// 核心構造函數 Axios
var Axios = require('./core/Axios');
// 合并配置方法
var mergeConfig = require('./core/mergeConfig');
// 引入默認配置
var defaults = require('./defaults');
第二部分
是生成實例對象 axios
、axios.Axios
、axios.create
等。
/*** Create an instance of Axios** @param {Object} defaultConfig The default config for the instance* @return {Axios} A new instance of Axios*/
function createInstance(defaultConfig) {// new 一個 Axios 生成實例對象var context = new Axios(defaultConfig);// bind 返回一個新的 wrap 函數,// 也就是為什么調用 axios 是調用 Axios.prototype.request 函數的原因var instance = bind(Axios.prototype.request, context);// Copy axios.prototype to instance// 復制 Axios.prototype 到實例上。// 也就是為什么 有 axios.get 等別名方法,// 且調用的是 Axios.prototype.get 等別名方法。utils.extend(instance, Axios.prototype, context);// Copy context to instance// 復制 context 到 intance 實例// 也就是為什么默認配置 axios.defaults 和攔截器 axios.interceptors 可以使用的原因// 其實是new Axios().defaults 和 new Axios().interceptorsutils.extend(instance, context);// 最后返回實例對象,以上代碼,在上文的圖中都有體現。這時可以仔細看下上圖。return instance;
}// Create the default instance to be exported
// 導出 創建默認實例
var axios = createInstance(defaults);
// Expose Axios class to allow class inheritance
// 暴露 Axios class 允許 class 繼承 也就是可以 new axios.Axios()
// 但 axios 文檔中 并沒有提到這個,我們平時也用得少。
axios.Axios = Axios;// Factory for creating new instances
// 工廠模式 創建新的實例 用戶可以自定義一些參數
axios.create = function create(instanceConfig) {return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
這里簡述下工廠模式。axios.create
,也就是用戶不需要知道內部是怎么實現的。
舉個生活的例子,我們買手機,不需要知道手機是怎么做的,就是工廠模式。
看完第二部分,里面涉及幾個工具函數,如bind
、extend
。接下來講述這幾個工具方法。
工具方法之 bind
axios/lib/helpers/bind.js
'use strict';
// 返回一個新的函數 wrap
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];}// 把 argument 對象放在數組 args 里return fn.apply(thisArg, args);};
};
傳遞兩個參數函數和thisArg
指向。
把參數arguments
生成數組,最后調用返回參數結構。
其實現在 apply
支持 arguments
這樣的類數組對象了,不需要手動轉數組。
那么為啥作者要轉數組,為了性能?當時不支持?抑或是作者不知道?這就不得而知了。有讀者知道歡迎評論區告訴筆者呀。
關于apply
、call
和bind
等不是很熟悉的讀者,可以看筆者的另一個面試官問系列
。
面試官問:能否模擬實現 JS 的 bind 方法
舉個例子
function fn(){console.log.apply(console, arguments);
}
fn(1,2,3,4,5,6, '若川');
// 1 2 3 4 5 6 '若川'
工具方法之 utils.extend
axios/lib/utils.js
function extend(a, b, thisArg) {forEach(b, function assignValue(val, key) {if (thisArg && typeof val === 'function') {a[key] = bind(val, thisArg);} else {a[key] = val;}});return a;
}
其實就是遍歷參數 b
對象,復制到 a
對象上,如果是函數就是則用 bind
調用。
工具方法之 utils.forEach
axios/lib/utils.js
遍歷數組和對象。設計模式稱之為迭代器模式。很多源碼都有類似這樣的遍歷函數。比如大家熟知的jQuery
$.each
。
/*** @param {Object|Array} obj The object to iterate* @param {Function} fn The callback to invoke for each item*/
function forEach(obj, fn) {// Don't bother if no value provided// 判斷 null 和 undefined 直接返回if (obj === null || typeof obj === 'undefined') {return;}// Force an array if not already something iterable// 如果不是對象,放在數組里。if (typeof obj !== 'object') {/*eslint no-param-reassign:0*/obj = [obj];}// 是數組 則用for 循環,調用 fn 函數。參數類似 Array.prototype.forEach 的前三個參數。if (isArray(obj)) {// Iterate over array valuesfor (var i = 0, l = obj.length; i < l; i++) {fn.call(null, obj[i], i, obj);}} else {// Iterate over object keys// 用 for in 遍歷對象,但 for in 會遍歷原型鏈上可遍歷的屬性。// 所以用 hasOwnProperty 來過濾自身屬性了。// 其實也可以用Object.keys來遍歷,它不遍歷原型鏈上可遍歷的屬性。for (var key in obj) {if (Object.prototype.hasOwnProperty.call(obj, key)) {fn.call(null, obj[key], key, obj);}}}
}
如果對Object
相關的API
不熟悉,可以查看筆者之前寫過的一篇文章。JavaScript 對象所有 API 解析
第三部分
取消相關 API 實現,還有all
、spread
、導出等實現。
// Expose Cancel & CancelToken
// 導出 Cancel 和 CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');// Expose all/spread
// 導出 all 和 spread API
axios.all = function all(promises) {return Promise.all(promises);
};
axios.spread = require('./helpers/spread');module.exports = axios;// Allow use of default import syntax in TypeScript
// 也就是可以以下方式引入
// import axios from 'axios';
module.exports.default = axios;
這里介紹下 spread
,取消的API
暫時不做分析,后文再詳細分析。
假設你有這樣的需求。
function f(x, y, z) {}
var args = [1, 2, 3];
f.apply(null, args);
那么可以用spread
方法。用法:
axios.spread(function(x, y, z) {})([1, 2, 3]);
實現也比較簡單。源碼實現:
/*** @param {Function} callback* @returns {Function}*/
module.exports = function spread(callback) {return function wrap(arr) {return callback.apply(null, arr);};
};
上文var context = new Axios(defaultConfig);
,接下來介紹核心構造函數Axios
。
核心構造函數 Axios
axios/lib/core/Axios.js
構造函數Axios
。
function Axios(instanceConfig) {// 默認參數this.defaults = instanceConfig;// 攔截器 請求和響應攔截器this.interceptors = {request: new InterceptorManager(),response: new InterceptorManager()};
}
Axios.prototype.request = function(config){// 省略,這個是核心方法,后文結合例子詳細描述// code ...var promise = Promise.resolve(config);// code ...return promise;
}
// 這是獲取 Uri 的函數,這里省略
Axios.prototype.getUri = function(){}
// 提供一些請求方法的別名
// Provide aliases for supported request methods
// 遍歷執行
// 也就是為啥我們可以 axios.get 等別名的方式調用,而且調用的是 Axios.prototype.request 方法
// 這個也在上面的 axios 結構圖上有所體現。
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {/*eslint func-names:0*/Axios.prototype[method] = function(url, config) {return this.request(utils.merge(config || {}, {method: method,url: url}));};
});utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {/*eslint func-names:0*/Axios.prototype[method] = function(url, data, config) {return this.request(utils.merge(config || {}, {method: method,url: url,data: data}));};
});module.exports = Axios;
接下來看攔截器部分。
攔截器管理構造函數 InterceptorManager
請求前攔截,和請求后攔截。
在Axios.prototype.request
函數里使用,具體怎么實現的攔截的,后文配合例子詳細講述。
axios github 倉庫 攔截器文檔
如何使用:
// Add a request interceptor
// 添加請求前攔截器
axios.interceptors.request.use(function (config) {// Do something before request is sentreturn config;
}, function (error) {// Do something with request errorreturn Promise.reject(error);
});// Add a response interceptor
// 添加請求后攔截器
axios.interceptors.response.use(function (response) {// Any status code that lie within the range of 2xx cause this function to trigger// Do something with response datareturn response;
}, function (error) {// Any status codes that falls outside the range of 2xx cause this function to trigger// Do something with response errorreturn Promise.reject(error);
});
如果想把攔截器,可以用eject
方法。
const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);
攔截器也可以添加自定義的實例上。
const instance = axios.create();
instance.interceptors.request.use(function () {/*...*/});
源碼實現:
構造函數,handles
用于存儲攔截器函數。
function InterceptorManager() {this.handlers = [];
}
接下來聲明了三個方法:使用、移除、遍歷。
InterceptorManager.prototype.use 使用
傳遞兩個函數作為參數,數組中的一項存儲的是{fulfilled: function(){}, rejected: function(){}}
。返回數字 ID
,用于移除攔截器。
/*** @param {Function} fulfilled The function to handle `then` for a `Promise`* @param {Function} rejected The function to handle `reject` for a `Promise`** @return {Number} 返回ID 是為了用 eject 移除*/
InterceptorManager.prototype.use = function use(fulfilled, rejected) {this.handlers.push({fulfilled: fulfilled,rejected: rejected});return this.handlers.length - 1;
};
InterceptorManager.prototype.eject 移除
根據 use
返回的 ID
移除 攔截器。
/*** @param {Number} id The ID that was returned by `use`*/
InterceptorManager.prototype.eject = function eject(id) {if (this.handlers[id]) {this.handlers[id] = null;}
};
有點類似定時器setTimeout
和 setInterval
,返回值是id
。用clearTimeout
和clearInterval
來清除定時器。
// 提一下 定時器回調函數是可以傳參的,返回值 timer 是數字
var timer = setInterval((name) => {console.log(name);
}, 1000, '若川');
console.log(timer); // 數字 ID
// 在控制臺等會再輸入執行這句,定時器就被清除了
clearInterval(timer);
InterceptorManager.prototype.forEach 遍歷
遍歷執行所有攔截器,傳遞一個回調函數(每一個攔截器函數作為參數)調用,被移除的一項是null
,所以不會執行,也就達到了移除的效果。
/*** @param {Function} fn The function to call for each interceptor*/
InterceptorManager.prototype.forEach = function forEach(fn) {utils.forEach(this.handlers, function forEachHandler(h) {if (h !== null) {fn(h);}});
};
實例結合
上文敘述的調試時運行npm start
是用axios/sandbox/client.html
路徑的文件作為示例的,讀者可以自行調試。
以下是一段這個文件中的代碼。
axios(options)
.then(function (res) {response.innerHTML = JSON.stringify(res.data, null, 2);
})
.catch(function (res) {response.innerHTML = JSON.stringify(res.data, null, 2);
});
先看調用棧流程
如果不想一步步調試,有個偷巧的方法。
知道 axios
使用了XMLHttpRequest
。
可以在項目中搜索:new XMLHttpRequest
。
定位到文件 axios/lib/adapters/xhr.js
在這條語句 var request = new XMLHttpRequest();
chrome
瀏覽器中 打個斷點調試下,再根據調用棧來細看具體函數等實現。
Call Stack
dispatchXhrRequest (xhr.js:19)
xhrAdapter (xhr.js:12)
dispatchRequest (dispatchRequest.js:60)
Promise.then (async)
request (Axios.js:54)
wrap (bind.js:10)
submit.onclick ((index):138)
簡述下流程:
Send Request
按鈕點擊submit.onclick
調用
axios
函數實際上是調用Axios.prototype.request
函數,而這個函數使用bind
返回的一個名為wrap
的函數。調用
Axios.prototype.request
(有請求攔截器的情況下執行請求攔截器),中間會執行
dispatchRequest
方法dispatchRequest
之后調用adapter (xhrAdapter)
最后調用
Promise
中的函數dispatchXhrRequest
,(有響應攔截器的情況下最后會再調用響應攔截器)
如果仔細看了文章開始的axios 結構關系圖
,其實對這個流程也有大概的了解。
接下來看 Axios.prototype.request
具體實現。
Axios.prototype.request 請求核心方法
這個函數是核心函數。主要做了這幾件事:
1.判斷第一個參數是字符串,則設置 url,也就是支持
axios('example/url', [, config])
,也支持axios({})
。
2.合并默認參數和用戶傳遞的參數
3.設置請求的方法,默認是是get
方法
4.將用戶設置的請求和響應攔截器、發送請求的dispatchRequest
組成Promise
鏈,最后返回還是Promise
實例。
也就是保證了請求前攔截器先執行,然后發送請求,再響應攔截器執行這樣的順序。<br>
也就是為啥最后還是可以`then`,`catch`方法的緣故。<br>
Axios.prototype.request = function request(config) {/*eslint no-param-reassign:0*/// Allow for axios('example/url'[, config]) a la fetch API// 這一段代碼 其實就是 使 axios('example/url', [, config])// config 參數可以省略if (typeof config === 'string') {config = arguments[1] || {};config.url = arguments[0];} else {config = config || {};}// 合并默認參數和用戶傳遞的參數config = mergeConfig(this.defaults, config);// Set config.method// 設置 請求方法,默認 get 。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// 組成`Promise`鏈 這段拆開到后文再講述
};
組成Promise
鏈,返回Promise
實例
這部分:用戶設置的請求和響應攔截器、發送請求的
dispatchRequest
組成Promise
鏈。也就是保證了請求前攔截器先執行,然后發送請求,再響應攔截器執行這樣的順序
也就是保證了請求前攔截器先執行,然后發送請求,再響應攔截器執行這樣的順序<br>
也就是為啥最后還是可以`then`,`catch`方法的緣故。<br>
如果讀者對Promise
不熟悉,建議讀阮老師的書籍《ES6 標準入門》。阮一峰老師 的 ES6 Promise-resolve 和 JavaScript Promise 迷你書(中文版)
// 組成`Promise`鏈// Hook up interceptors middleware// 把 xhr 請求 的 dispatchRequest 和 undefined 放在一個數組里var chain = [dispatchRequest, undefined];// 創建 Promise 實例var promise = Promise.resolve(config);// 遍歷用戶設置的請求攔截器 放到數組的 chain 前面this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {chain.unshift(interceptor.fulfilled, interceptor.rejected);});// 遍歷用戶設置的響應攔截器 放到數組的 chain 后面this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {chain.push(interceptor.fulfilled, interceptor.rejected);});// 遍歷 chain 數組,直到遍歷 chain.length 為 0while (chain.length) {// 兩兩對應移出來 放到 then 的兩個參數里。promise = promise.then(chain.shift(), chain.shift());}return promise;
var promise = Promise.resolve(config);
解釋下這句。作用是生成Promise
實例。
var promise = Promise.resolve({name: '若川'})
// 等價于
// new Promise(resolve => resolve({name: '若川'}))
promise.then(function (config){console.log(config)
});
// {name: "若川"}
同樣解釋下后文會出現的Promise.reject(error);
:
Promise.reject(error);
var promise = Promise.reject({name: '若川'})
// 等價于
// new Promise(reject => reject({name: '若川'}))// promise.then(null, function (config){
// console.log(config)
// });
// 等價于
promise.catch(function (config){console.log(config)
});
// {name: "若川"}
接下來結合例子,來理解這段代碼。
很遺憾,在example
文件夾沒有攔截器的例子。筆者在example
中在example/get
的基礎上添加了一個攔截器的示例。axios/examples/interceptors
,便于讀者調試。
node ./examples/server.js -p 5000
promise = promise.then(chain.shift(), chain.shift());
這段代碼打個斷點。
會得到這樣的這張圖。
特別關注下,右側,local
中的chain
數組。也就是這樣的結構。
var chain = ['請求成功攔截2', '請求失敗攔截2','請求成功攔截1', '請求失敗攔截1',dispatch, undefined,'響應成功攔截1', '響應失敗攔截1','響應成功攔截2', '響應失敗攔截2',
]
這段代碼相對比較繞。也就是會生成如下類似的代碼,中間會調用dispatchRequest
方法。
// config 是 用戶配置和默認配置合并的
var promise = Promise.resolve(config);
promise.then('請求成功攔截2', '請求失敗攔截2')
.then('請求成功攔截1', '請求失敗攔截1')
.then(dispatchRequest, undefined)
.then('響應成功攔截1', '響應失敗攔截1')
.then('響應成功攔截2', '響應失敗攔截2').then('用戶寫的業務處理函數')
.catch('用戶寫的報錯業務處理函數');
這里提下promise
then
和catch
知識:Promise.prototype.then
方法的第一個參數是resolved
狀態的回調函數,第二個參數(可選)是rejected
狀態的回調函數。所以是成對出現的。Promise.prototype.catch
方法是.then(null, rejection)
或.then(undefined, rejection)
的別名,用于指定發生錯誤時的回調函數。then
方法返回的是一個新的Promise
實例(注意,不是原來那個Promise
實例)。因此可以采用鏈式寫法,即then
方法后面再調用另一個then
方法。
結合上述的例子更詳細一點,代碼則是這樣的。
var promise = Promise.resolve(config);
// promise.then('請求成功攔截2', '請求失敗攔截2')
promise.then(function requestSuccess2(config) {console.log('------request------success------2');return config;
}, function requestError2(error) {console.log('------response------error------2');return Promise.reject(error);
})// .then('請求成功攔截1', '請求失敗攔截1')
.then(function requestSuccess1(config) {console.log('------request------success------1');return config;
}, function requestError1(error) {console.log('------response------error------1');return Promise.reject(error);
})// .then(dispatchRequest, undefined)
.then( function dispatchRequest(config) {/*** 適配器返回的也是Promise 實例adapter = function xhrAdapter(config) {return new Promise(function dispatchXhrRequest(resolve, reject) {})}**/return adapter(config).then(function onAdapterResolution(response) {// 省略代碼 ...return response;}, function onAdapterRejection(reason) {// 省略代碼 ...return Promise.reject(reason);});
}, undefined)// .then('響應成功攔截1', '響應失敗攔截1')
.then(function responseSuccess1(response) {console.log('------response------success------1');return response;
}, function responseError1(error) {console.log('------response------error------1');return Promise.reject(error);
})// .then('響應成功攔截2', '響應失敗攔截2')
.then(function responseSuccess2(response) {console.log('------response------success------2');return response;
}, function responseError2(error) {console.log('------response------error------2');return Promise.reject(error);
})// .then('用戶寫的業務處理函數')
// .catch('用戶寫的報錯業務處理函數');
.then(function (response) {console.log('哈哈哈,終于獲取到數據了', response);
})
.catch(function (err) {console.log('哎呀,怎么報錯了', err);
});
仔細看這段Promise
鏈式調用,代碼都類似。then
方法最后返回的參數,就是下一個then
方法第一個參數。catch
錯誤捕獲,都返回Promise.reject(error)
,這是為了便于用戶catch
時能捕獲到錯誤。
舉個例子:
var p1 = new Promise((resolve, reject) => {reject(new Error({name: '若川'}));
});p1.catch(err => {console.log(res, 'err');return Promise.reject(err)
})
.catch(err => {console.log(err, 'err1');
})
.catch(err => {console.log(err, 'err2');
});
err2
不會捕獲到,也就是不會執行,但如果都返回了return Promise.reject(err)
,則可以捕獲到。
最后畫個圖總結下 Promise
鏈式調用。
小結:1. 請求和響應的攔截器可以寫
Promise
。
如果設置了多個請求響應器,后設置的先執行。
如果設置了多個響應攔截器,先設置的先執行。
dispatchRequest(config)
這里的config
是請求成功攔截器返回的。接下來看dispatchRequest
函數。
dispatchRequest 最終派發請求
這個函數主要做了如下幾件事情:
1.如果已經取消,則
throw
原因報錯,使Promise
走向rejected
。
2.確保config.header
存在。
3.利用用戶設置的和默認的請求轉換器轉換數據。
4.拍平config.header
。
5.刪除一些config.header
。
6.返回適配器adapter
(Promise
實例)執行后then
執行后的Promise
實例。返回結果傳遞給響應攔截器處理。
'use strict';
// utils 工具函數
var utils = require('./../utils');
// 轉換數據
var transformData = require('./transformData');
// 取消狀態
var isCancel = require('../cancel/isCancel');
// 默認參數
var defaults = require('../defaults');/*** 拋出 錯誤原因,使`Promise`走向`rejected`*/
function throwIfCancellationRequested(config) {if (config.cancelToken) {config.cancelToken.throwIfRequested();}
}/*** Dispatch a request to the server using the configured adapter.** @param {object} config The config that is to be used for the request* @returns {Promise} The Promise to be fulfilled*/
module.exports = function dispatchRequest(config) {// 取消相關throwIfCancellationRequested(config);// Ensure headers exist// 確保 headers 存在config.headers = config.headers || {};// Transform request data// 轉換請求的數據config.data = transformData(config.data,config.headers,config.transformRequest);// Flatten headers// 拍平 headersconfig.headers = utils.merge(config.headers.common || {},config.headers[config.method] || {},config.headers || {});// 以下這些方法 刪除 headersutils.forEach(['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],function cleanHeaderConfig(method) {delete config.headers[method];});// adapter 適配器部分 拆開 放在下文講
};
dispatchRequest 之 transformData 轉換數據
上文的代碼里有個函數 transformData
,這里解釋下。其實就是遍歷傳遞的函數數組 對數據操作,最后返回數據。
axios.defaults.transformResponse
數組中默認就有一個函數,所以使用concat
鏈接自定義的函數。
使用:
文件路徑axios/examples/transform-response/index.html
這段代碼其實就是對時間格式的字符串轉換成時間對象,可以直接調用getMonth
等方法。
var ISO_8601 = /(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})Z/;
function formatDate(d) {return (d.getMonth() + 1) + '/' + d.getDate() + '/' + d.getFullYear();
}axios.get('https://api.github.com/users/mzabriskie', {transformResponse: axios.defaults.transformResponse.concat(function (data, headers) {Object.keys(data).forEach(function (k) {if (ISO_8601.test(data[k])) {data[k] = new Date(Date.parse(data[k]));}});return data;})
})
.then(function (res) {document.getElementById('created').innerHTML = formatDate(res.data.created_at);
});
源碼:
就是遍歷數組,調用數組里的傳遞 data
和 headers
參數調用函數。
module.exports = function transformData(data, headers, fns) {/*eslint no-param-reassign:0*/utils.forEach(fns, function transform(fn) {data = fn(data, headers);});return data;
};
dispatchRequest 之 adapter 適配器執行部分
適配器,在設計模式中稱之為適配器模式。講個生活中簡單的例子,大家就容易理解。
我們常用以前手機耳機孔都是圓孔,而現在基本是耳機孔和充電接口合二為一。統一為typec
。
這時我們需要需要一個typec轉圓孔的轉接口
,這就是適配器。
// adapter 適配器部分var adapter = config.adapter || defaults.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);});
接下來看具體的 adapter
。
adapter 適配器 真正發送請求
var adapter = config.adapter || defaults.adapter;
看了上文的 adapter
,可以知道支持用戶自定義。比如可以通過微信小程序 wx.request
按照要求也寫一個 adapter
。
接著來看下 defaults.ddapter
。
文件路徑:axios/lib/defaults.js
根據當前環境引入,如果是瀏覽器環境引入xhr
,是node
環境則引入http
。
類似判斷node
環境,也在sentry-javascript
源碼中有看到。
function getDefaultAdapter() {var adapter;// 根據 XMLHttpRequest 判斷if (typeof XMLHttpRequest !== 'undefined') {// For browsers use XHR adapteradapter = require('./adapters/xhr');// 根據 process 判斷} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {// For node use HTTP adapteradapter = require('./adapters/http');}return adapter;
}
var defaults = {adapter: getDefaultAdapter(),// ...
};
xhr
接下來就是我們熟悉的 XMLHttpRequest
對象。
可能讀者不了解可以參考XMLHttpRequest MDN 文檔。
主要提醒下:onabort
是請求取消事件,withCredentials
是一個布爾值,用來指定跨域 Access-Control
請求是否應帶有授權信息,如 cookie
或授權 header
頭。
這塊代碼有刪減,具體可以看若川的axios-analysis
倉庫,也可以克隆筆者的axios-analysis
倉庫調試時再具體分析。
module.exports = function xhrAdapter(config) {return new Promise(function dispatchXhrRequest(resolve, reject) {// 這塊代碼有刪減var request = new XMLHttpRequest();request.open()request.timeout = config.timeout;// 監聽 state 改變request.onreadystatechange = function handleLoad() {if (!request || request.readyState !== 4) {return;}// ...}// 取消request.onabort = function(){};// 錯誤request.onerror = function(){};// 超時request.ontimeout = function(){};// cookies 跨域攜帶 cookies 面試官常喜歡考這個// 一個布爾值,用來指定跨域 Access-Control 請求是否應帶有授權信息,如 cookie 或授權 header 頭。// Add withCredentials to request if neededif (!utils.isUndefined(config.withCredentials)) {request.withCredentials = !!config.withCredentials;}// 上傳下載進度相關// Handle progress if neededif (typeof config.onDownloadProgress === 'function') {request.addEventListener('progress', config.onDownloadProgress);}// Not all browsers support upload eventsif (typeof config.onUploadProgress === 'function' && request.upload) {request.upload.addEventListener('progress', config.onUploadProgress);}// Send the request// 發送請求request.send(requestData);});
}
而實際上現在 fetch
支持的很好了,阿里開源的 umi-request 請求庫,就是用fetch
封裝的,而不是用XMLHttpRequest
。文章末尾,大概講述下 umi-request
和 axios
的區別。
http
http
這里就不詳細敘述了,感興趣的讀者可以自行查看,若川的axios-analysis
倉庫。
module.exports = function httpAdapter(config) {return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {});
};
上文 dispatchRequest
有取消模塊,我覺得是重點,所以放在最后來細講:
dispatchRequest 之 取消模塊
可以使用cancel token
取消請求。
axios cancel token API 是基于撤銷的 promise
取消提議。
The axios cancel token API is based on the withdrawn cancelable promises proposal.
axios 文檔 cancellation
文檔上詳細描述了兩種使用方式。
很遺憾,在example
文件夾也沒有取消的例子。筆者在example
中在example/get
的基礎上添加了一個取消的示例。axios/examples/cancel
,便于讀者調試。
node ./examples/server.js -p 5000
request
中的攔截器和dispatch
中的取消這兩個模塊相對復雜,可以多調試調試,吸收消化。
const CancelToken = axios.CancelToken;
const source = CancelToken.source();axios.get('/get/server', {cancelToken: source.token
}).catch(function (err) {if (axios.isCancel(err)) {console.log('Request canceled', err.message);} else {// handle error}
});// cancel the request (the message parameter is optional)
// 取消函數。
source.cancel('哎呀,我被若川取消了');
取消請求模塊代碼示例
結合源碼取消流程大概是這樣的。這段放在代碼在axios/examples/cancel-token/index.html
。
參數的 config.cancelToken
是觸發了source.cancel('哎呀,我被若川取消了');
才生成的。
// source.cancel('哎呀,我被若川取消了');
// 點擊取消時才會 生成 cancelToken 實例對象。
// 點擊取消后,會生成原因,看懂了這段在看之后的源碼,可能就好理解了。
var config = {name: '若川',// 這里簡化了cancelToken: {promise: new Promise(function(resolve){resolve({ message: '哎呀,我被若川取消了'})}),reason: { message: '哎呀,我被若川取消了' }},
};
// 取消 拋出異常方法
function throwIfCancellationRequested(config){// 取消的情況下執行這句if(config.cancelToken){// 這里源代碼 便于執行,我改成具體代碼// config.cancelToken.throwIfRequested();// if (this.reason) {// throw this.reason;// }if(config.cancelToken.reason){throw config.cancelToken.reason;}}
}function dispatchRequest(config){// 有可能是執行到這里就取消了,所以拋出錯誤會被err2 捕獲到throwIfCancellationRequested(config);// adapter xhr適配器return new Promise((resovle, reject) => {var request = new XMLHttpRequest();console.log('request', request);if (config.cancelToken) {// Handle cancellationconfig.cancelToken.promise.then(function onCanceled(cancel) {if (!request) {return;}request.abort();reject(cancel);// Clean up requestrequest = null;});}}).then(function(res){// 有可能是執行到這里就才取消 取消的情況下執行這句throwIfCancellationRequested(config);console.log('res', res);return res;}).catch(function(reason){// 有可能是執行到這里就才取消 取消的情況下執行這句throwIfCancellationRequested(config);console.log('reason', reason);return Promise.reject(reason);});
}var promise = Promise.resolve(config);// 沒設置攔截器的情況下是這樣的
promise
.then(dispatchRequest, undefined)
// 用戶定義的then 和 catch
.then(function(res){console.log('res1', res);return res;
})
.catch(function(err){console.log('err2', err);return Promise.reject(err);
});
// err2 {message: "哎呀,我被若川取消了"}
接下來看取消模塊的源碼
看如何通過生成config.cancelToken
。
文件路徑:
axios/lib/cancel/CancelToken.js
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
source.cancel('哎呀,我被若川取消了');
由示例看 CancelToken.source
的實現,
CancelToken.source = function source() {var cancel;var token = new CancelToken(function executor(c) {cancel = c;});// tokenreturn {token: token,cancel: cancel};
};
執行后source
的大概結構是這樣的。
{token: {promise: new Promise(function(resolve){resolve({ message: '哎呀,我被若川取消了'})}),reason: { message: '哎呀,我被若川取消了' }},cancel: function cancel(message) {if (token.reason) {// Cancellation has already been requested// 已經取消return;}token.reason = {message: '哎呀,我被若川取消了'};}
}
接著看 new CancelToken
// CancelToken
// 通過 CancelToken 來取消請求操作
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);});
}module.exports = CancelToken;
發送請求的適配器里是這樣使用的。
// xhr
if (config.cancelToken) {// Handle cancellationconfig.cancelToken.promise.then(function onCanceled(cancel) {if (!request) {return;}request.abort();reject(cancel);// Clean up requestrequest = null;});
}
dispatchRequest
中的throwIfCancellationRequested
具體實現:throw 拋出異常。
// 拋出異常函數
function throwIfCancellationRequested(config) {if (config.cancelToken) {config.cancelToken.throwIfRequested();}
}
// 拋出異常 用戶 { message: '哎呀,我被若川取消了' }
CancelToken.prototype.throwIfRequested = function throwIfRequested() {if (this.reason) {throw this.reason;}
};
取消流程調用棧
1.source.cancel()
2.resolvePromise(token.reason);
3.config.cancelToken.promise.then(function onCanceled(cancel) {})
最后進入request.abort();``reject(cancel);
到這里取消的流程就介紹完畢了。主要就是通過傳遞配置參數cancelToken
,取消時才會生成cancelToken
,判斷有,則拋出錯誤,使Promise
走向rejected
,讓用戶捕獲到消息{message: '用戶設置的取消信息'}。
文章寫到這里就基本到接近尾聲了。
能讀到最后,說明你已經超過很多人啦^_^
axios
是非常優秀的請求庫,但肯定也不能滿足所有開發者的需求,接下來對比下其他庫,看看其他開發者有什么具體需求。
對比其他請求庫
KoAjax
FCC 成都社區負責人水歌開源的KoAJAX。
如何用開源軟件辦一場技術大會?以下這篇文章中摘抄的一段。
前端請求庫 —— KoAJAX 國內前端同學最常用的 HTTP 請求庫應該是 axios 了吧?雖然它的 Interceptor(攔截器)API 是 .use(),但和 Node.js 的 Express、Koa 等框架的中間件模式完全不同,相比 jQuery .ajaxPrefilter()、dataFilter() 并沒什么實質改進;上傳、下載進度比 jQuery.Deferred() 還簡陋,只是兩個專門的回調選項。所以,它還是要對特定的需求記憶特定的 API,不夠簡潔。
幸運的是,水歌在研究如何用 ES 2018 異步迭代器實現一個類 Koa 中間件引擎的過程中,做出了一個更有實際價值的上層應用 —— KoAJAX。它的整個執行過程基于 Koa 式的中間件,而且它自己就是一個中間件調用棧。除了 RESTful API 常用的 .get()、.post()、.put()、.delete() 等快捷方法外,開發者就只需記住 .use() 和 next(),其它都是 ES 標準語法和 TS 類型推導。
umi-request 阿里開源的請求庫
umi-request github 倉庫
umi-request
與 fetch
, axios
異同。
umi-request
與 fetch
, axios
異同不得不說,umi-request
確實強大,有興趣的讀者可以閱讀下其源碼。
看懂axios
的基礎上,看懂umi-request
源碼應該不難。
比如 umi-request
取消模塊代碼幾乎與axios
一模一樣。
總結
文章詳細介紹了 axios
調試方法。詳細介紹了 axios
構造函數,攔截器,取消等功能的實現。最后還對比了其他請求庫。
最后畫個圖總結一下 axios 的總體大致流程。
解答下文章開頭提的問題:
如果你是求職者,項目寫了運用了axios
,面試官可能會問你:
1.為什么
axios
既可以當函數調用,也可以當對象使用,比如axios({})
、axios.get
。
答:axios
本質是函數,賦值了一些別名方法,比如get
、post
方法,可被調用,最終調用的還是Axios.prototype.request
函數。
2.簡述axios
調用流程。
答:實際是調用的Axios.prototype.request
方法,最終返回的是promise
鏈式調用,實際請求是在dispatchRequest
中派發的。
3.有用過攔截器嗎?原理是怎樣的?
答:用過,用axios.interceptors.request.use
添加請求成功和失敗攔截器函數,用axios.interceptors.response.use
添加響應成功和失敗攔截器函數。在Axios.prototype.request
函數組成promise
鏈式調用時,Interceptors.protype.forEach
遍歷請求和響應攔截器添加到真正發送請求dispatchRequest
的兩端,從而做到請求前攔截和響應后攔截。攔截器也支持用Interceptors.protype.eject
方法移除。
4.有使用axios
的取消功能嗎?是怎么實現的?
答:用過,通過傳遞config
配置cancelToken
的形式,來取消的。判斷有傳cancelToken
,在promise
鏈式調用的dispatchRequest
拋出錯誤,在adapter
中request.abort()
取消請求,使promise
走向rejected
,被用戶捕獲取消信息。
5.為什么支持瀏覽器中發送請求也支持node
發送請求?
答:axios.defaults.adapter
默認配置中根據環境判斷是瀏覽器還是node
環境,使用對應的適配器。適配器支持自定義。
回答面試官的問題,讀者也可以根據自己的理解,組織語言,筆者的回答只是做一個參考。
axios
源碼相對不多,打包后一千多行,比較容易看完,非常值得學習。
建議 clone
若川的 axios-analysis github 倉庫,按照文中方法自己調試,印象更深刻。
基于Promise
,構成Promise
鏈,巧妙的設置請求攔截,發送請求,再試試響應攔截器。
request
中的攔截器和dispatch
中的取消這兩個模塊相對復雜,可以多調試調試,吸收消化。
axios
既是函數,是函數時調用的是Axios.prototype.request
函數,又是對象,其上面有get
、post
等請求方法,最終也是調用Axios.prototype.request
函數。
axios
源碼中使用了挺多設計模式。比如工廠模式、迭代器模式、適配器模式等。如果想系統學習設計模式,一般比較推薦豆瓣評分 9.1 的JavaScript 設計模式與開發實踐
如果讀者發現有不妥或可改善之處,再或者哪里沒寫明白的地方,歡迎加我微信?lxchuan12?交流。另外覺得寫得不錯,對您有些許幫助,可以點贊、評論、轉發分享,也是對筆者的一種支持,非常感謝呀。
原創精選文章
工作一年后,我有些感悟(寫于2017年)
高考七年后、工作三年后的感悟
面試官問:JS的繼承
前端使用puppeteer 爬蟲生成《React.js 小書》PDF并合并
微信公眾號
作者:常以若川為名混跡于江湖。前端路上 | PPT 愛好者 | 所知甚少,唯善學。
博客:https://lxchuan12.cn/posts/
,閱讀體驗可能更好些。
主要發布
前端 | PPT | 生活 | 效率
相關的文章,長按掃碼關注。歡迎加我微信lxchuan12
(注明來源,基本來者不拒),拉您進【前端視野交流群】,長期交流學習~
由于公眾號限制外鏈,點擊閱讀原文,或許閱讀體驗更佳,覺得文章不錯,可以點個在看呀^_^