這是
學習源碼總體架構系列
第六篇。總體架構這詞語好像有點大,姑且就算是源碼總體結構吧,主要就是學習是代碼總體結構,不深究其餘不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。javascript
學習源碼總體架構系列
文章以下:html
1.學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫
2.學習 underscore 源碼總體架構,打造屬於本身的函數式編程類庫
3.學習 lodash 源碼總體架構,打造屬於本身的函數式編程類庫
4.學習 sentry 源碼總體架構,打造屬於本身的前端異常監控SDK
5.學習 vuex 源碼總體架構,打造屬於本身的狀態管理庫
6.學習 axios 源碼總體架構,打造屬於本身的請求庫
前端
感興趣的讀者能夠點擊閱讀。下一篇多是vue-router
源碼。vue
本文比較長,手機上閱讀,能夠直接看文中的幾張圖便可。建議收藏後在電腦上閱讀,按照文中調試方式本身調試或許更容易吸取消化。java
導讀
文章詳細介紹了 axios
調試方法。詳細介紹了 axios
構造函數,攔截器,取消等功能的實現。最後還對比了其餘請求庫。node
本文學習的版本是v0.19.0
。克隆的官方倉庫的master
分支。 截至目前(2019年12月14日),最新一次commit
是2019-12-09 15:52 ZhaoXC
dc4bc49673943e352
,fix: fix ignore set withCredentials false (#2582)
。webpack
本文倉庫在這裏若川的 axios-analysis github 倉庫。求個star
呀。ios
若是你是求職者,項目寫了運用了axios
,面試官可能會問你:git
1.爲何
axios
既能夠當函數調用,也能夠當對象使用,好比axios({})
、axios.get
。
2.簡述axios
調用流程。
3. 有用過攔截器嗎?原理是怎樣的?
4.有使用axios
的取消功能嗎?是怎麼實現的
5.爲何支持瀏覽器中發送請求也支持node
發送請求
諸如這類問題。es6
前不久,筆者在知乎回答了一個問題一年內的前端看不懂前端框架源碼怎麼辦? 推薦了一些資料,閱讀量還不錯,你們有興趣能夠看看。主要有四點:
1.藉助調試
2.搜索查閱相關高贊文章
3.把不懂的地方記錄下來,查閱相關文檔
4.總結
看源碼,調試很重要,因此筆者詳細寫下 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;
// 調試 examples
console.log(url);
// Process axios itself
if (/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
。
在根目錄下 axios-analysis/
建立.vscode/launch
文件以下:
{
// 使用 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
的文件可調試。
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
。
接下來看具體源碼的實現。能夠跟着斷點調試一下。
斷點調試要領:
賦值語句能夠一步跳過,看返回值便可,後續詳細再看。
函數執行須要斷點跟着看,也能夠結合註釋和上下文倒推這個函數作了什麼。
看源碼第一步,先看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().interceptors
utils.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
。接下來說述這幾個工具方法。
./helpers/bind
'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.log, arguments);
}
fn(1,2,3,4,5,6, '若川');
// 1 2 3 4 5 6 '若川'
複製代碼
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
調用。
遍歷數組和對象。設計模式稱之爲迭代器模式。不少源碼都有相似這樣的遍歷函數。好比你們熟知的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 values
for (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
。
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;
複製代碼
接下來看攔截器部分。
請求前攔截,和請求後攔截。
在Axios.prototype.request
函數裏使用,具體怎麼實現的攔截的,後文配合例子詳細講述。
如何使用:
// Add a request interceptor
// 添加請求前攔截器
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return 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 data
return response;
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return 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 = [];
}
複製代碼
接下來聲明瞭三個方法:使用、移除、遍歷。
傳遞兩個函數做爲參數,數組中的一項存儲的是{fulfilled, rejected}
。返回數字 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} An ID used to remove interceptor later */
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};
複製代碼
根據 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);
複製代碼
遍歷執行 攔截器
/** * @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
具體實現。
這個函數是核心函數。 主要作了這幾件事:
- 判斷第一個參數是字符串,則設置 url,也就是支持
axios('example/url', [, config])
,也支持axios({})
。- 合併默認參數和用戶傳遞的參數
- 設置請求的方法,默認是是
get
方法- 將用戶設置的請求和響應攔截器、發送請求的
dispatchRequest
組成Promise
鏈,最後返回仍是Promise
實例。
也就是保證了請求前攔截器先執行,而後發送請求,再響應攔截器執行這樣的順序
也就是爲啥最後仍是能夠then
,catch
方法的緣故。
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
鏈。也就是保證了請求前攔截器先執行,而後發送請求,再響應攔截器執行這樣的順序
也就是保證了請求前攔截器先執行,而後發送請求,再響應攔截器執行這樣的順序
也就是爲啥最後仍是能夠then
,catch
方法的緣故。
若是讀者對Promise
不熟悉,建議讀阮老師的書籍《ES6 標準入門》。 阮一峯老師 的 ES6 Promise-resolve 和 JavaScript 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 爲 0
while (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)
,則能夠捕獲到。
dispatchRequest(config)
這裏的config
是請求成功攔截器返回的。接下來看dispatchRequest
函數。
小結:1. 請求和響應的攔截器能夠寫
Promise
。
2. 若是設置了多個請求響應器,後設置的先執行。
3. 若是設置了多個響應攔截器,先設置的先執行。
這個函數主要作了以下幾件事情:
- 若是已經取消,則
throw
緣由報錯,使Promise
走向rejected
。- 確保
config.header
存在。- 利用用戶設置的和默認的請求轉換器轉換數據。
- 拍平
config.header
。- 刪除一些
config.header
。- 返回適配器
adapter
(Promise
實例)執行後then
執行後的Promise
實例。返回結果傳遞給響應攔截器處理。
'use strict';
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
/** * Throws a `Cancel` if cancellation has been requested. */
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
// 拍平 headers
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers || {}
);
// 如下這些方法 刪除 headers
utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);
// adapter 適配器部分 拆開 放在下文講
};
複製代碼
上文的代碼裏有個函數 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;
};
複製代碼
適配器,在設計模式中稱之爲適配器模式。講個生活中簡單的例子,你們就容易理解。
咱們經常使用之前手機耳機孔都是圓孔,而如今基本是耳機孔和充電接口合二爲一。統一爲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
。
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;
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;
}
var defaults = {
adapter: getDefaultAdapter(),
// ...
};
複製代碼
xhr
接下來就是咱們熟悉的 XMLHttpRequest
對象。
可能讀者不瞭解能夠參考XMLHttpRequest MDN 文檔。
主要提醒下:onabort
是請求取消事件,withCredentials
是一個布爾值,用來指定跨域 Access-Control
請求是否應帶有受權信息,如 cookie
或受權 header
頭。
這塊代碼有刪減,具體能夠看axios 倉庫 xhr.js,也能夠克隆筆者的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 needed
if (!utils.isUndefined(config.withCredentials)) {
request.withCredentials = !!config.withCredentials;
}
// 上傳下載進度相關
// Handle progress if needed
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}
// Not all browsers support upload events
if (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
這裏就不詳細敘述了,感興趣的讀者能夠自行查看。
module.exports = function httpAdapter(config) {
return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
});
};
複製代碼
上文 dispatchRequest
有取消模塊,我以爲是重點,因此放在最後來細講:
可使用cancel token
取消請求。
axios cancel token API 是基於撤銷的 promise
取消提議。
The axios cancel token API is based on the withdrawn cancelable promises proposal.
文檔上詳細描述了兩種使用方式。
很遺憾,在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-simple/index.html
。
// source.cancel('哎呀,我被若川取消了');
// 點擊取消時纔會 生成 cancelToken 實例對象。
// 點擊取消後,會生成緣由,看懂了這段在看以後的源碼,可能就好理解了。
var config = {
name: '若川',
// 這裏簡化了
cancelToken: {
reason: '',
promise: '',
}
};
var reason = {message: '哎呀,我被若川取消了'};
// 取消 拋出異常方法
function throwIfCancellationRequested(config){
// 取消的狀況下執行這句
if(config.cancelToken){
throw reason;
}
}
function dispatchRequest(config){
// 有多是執行到這裏就取消了,因此拋出錯誤會被err2 捕獲到
throwIfCancellationRequested(config);
// adapter xhr適配器
return new Promise((resovle, reject) => {
var request = new XMLHttpRequest();
console.log('request', request);
// if( 用戶點取消了 ){} source.cancel('哎呀,我被若川取消了')
// 取消的狀況下執行這兩句
// 取消
request.abort();
reject(reason);
})
.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: "哎呀,我被若川取消了"}
複製代碼
// 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;
複製代碼
// 拋出異常函數
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
// 拋出異常 用戶 { message: '哎呀,我被若川取消了' }
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
複製代碼
發送請求的適配器裏是這樣使用的。
// xhr
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
複製代碼
取消流程調用棧
1.source.cancel()
2.resolvePromise(token.reason);
3.config.cancelToken.promise.then(function onCanceled(cancel) {})
最後進入request.abort();``reject(cancel);
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
source.cancel('哎呀,我被若川取消了');
複製代碼
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
// token
return {
token: token,
cancel: cancel
};
};
複製代碼
CancelToken.source();
source.cancel('哎呀,我被若川取消了');
複製代碼
執行後的大概結構是這樣的。
{
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: '哎呀,我被若川取消了'};
}
}
複製代碼
結合源碼取消流程大概是這樣的。這段放在代碼在axios/examples/cancel-token/index.html
。
// 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 cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = 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: "哎呀,我被若川取消了"}
複製代碼
到這裏取消的流程就介紹完畢了。主要就是經過傳遞配置參數cancelToken
,判斷有,則拋出錯誤,使Promise
走向rejected
,讓用戶捕獲到消息{message: '用戶設置的取消信息'}。
能讀到最後,說明你已經超過不少人啦^_^
文章寫到這裏就基本到接近尾聲了。
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
與 fetch
, axios
異同。
不得不說,umi-request
確實強大,有興趣的讀者能夠閱讀下其源碼。
看懂axios
的基礎上,看懂umi-request
源碼應該不難。
好比 umi-request
取消模塊代碼幾乎與axios
如出一轍。
文章詳細介紹了 axios
調試方法。詳細介紹了 axios
構造函數,攔截器,取消等功能的實現。最後還對比了其餘請求庫。
axios
源碼相對很少,打包後一千多行,比較容易看完,很是值得學習。
建議 clone
若川的 axios-analysis github 倉庫,按照文中方法本身調試
基於Promise
,request
中的攔截器和dispatch
中的取消這兩個模塊相對複雜,能夠多調試調試,吸取消化。
axios
既是函數,是函數時調用的是Axios.prototype.request
函數,又是對象,其上面有get
、post
等請求方法,最終也是調用Axios.prototype.request
函數。
axios
源碼中使用了挺多設計模式。好比工廠模式、迭代器模式、適配器模式等。若是想系統學習設計模式,通常比較推薦豆瓣評分9.1的JavaScript設計模式與開發實踐
若是讀者發現有不妥或可改善之處,再或者哪裏沒寫明白的地方,歡迎評論指出。另外以爲寫得不錯,對您有些許幫助,能夠點贊、評論、轉發分享,也是對筆者的一種支持,很是感謝呀。
寫文章前,搜索瞭如下幾篇文章泛讀了一下。有興趣在對比看看如下這幾篇,有代碼調試的基礎上,看起來也快。
一直以爲多搜索幾篇文章看,對本身學習知識更有用。有個詞語叫主題閱讀。大概意思就是一個主題一系列閱讀。
@叫我小明呀:Axios 源碼解析
@尼庫尼庫桑:深刻淺出 axios 源碼
@小賊先生_ronffy:Axios源碼深度剖析 - AJAX新王者
逐行解析Axios源碼
[譯]axios 是如何封裝 HTTP 請求的
知乎@Lee : TypeScript 重構 Axios 經驗分享
面試官問:JS的繼承
面試官問:JS的this指向
面試官問:可否模擬實現JS的call和apply方法
面試官問:可否模擬實現JS的bind方法
面試官問:可否模擬實現JS的new操做符
做者:常以若川爲名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,惟善學。
若川的博客,使用vuepress
重構了,閱讀體驗可能更好些
掘金專欄,歡迎關注~
segmentfault
前端視野專欄,歡迎關注~
知乎前端視野專欄,歡迎關注~
github blog,相關源碼和資源都放在這裏,求個star
^_^~
可能比較有趣的微信公衆號,長按掃碼關注。歡迎加筆者微信lxchuan12
(註明來源,基原本者不拒),拉您進【前端視野交流羣】,長期交流學習~