摘要:我們先來看看構(gòu)造函數(shù)構(gòu)造函數(shù)就是用來實現(xiàn)攔截器的,這個構(gòu)造函數(shù)原型上有個方法。關(guān)于源碼,其實是比較簡單的,都是用來操作該構(gòu)造函數(shù)的實例屬性的。存放攔截器方法,數(shù)組內(nèi)每一項都是有兩個屬性的對象,兩個屬性分別對應(yīng)成功和失敗后執(zhí)行的函數(shù)。
Axios源碼分析 - XHR篇
文章源碼托管在github上,歡迎fork指正!
axios 是一個基于 Promise 的http請求庫,可以用在瀏覽器和node.js中,目前在github上有 42K 的star數(shù)
備注:每一小節(jié)都會從兩個方面介紹:如何使用 -> 源碼分析
[工具方法簡單介紹]一節(jié)可先跳過,后面用到了再過來查看
axios最核心的技術(shù)點是如何攔截請求響應(yīng)并修改請求參數(shù)修改響應(yīng)數(shù)據(jù) 和 axios是如何用promise搭起基于xhr的異步橋梁的
axios項目目錄結(jié)構(gòu)├── /dist/ # 項目輸出目錄 ├── /lib/ # 項目源碼目錄 │ ├── /cancel/ # 定義取消功能 │ ├── /core/ # 一些核心功能 │ │ ├── Axios.js # axios的核心主類 │ │ ├── dispatchRequest.js # 用來調(diào)用http請求適配器方法發(fā)送請求 │ │ ├── InterceptorManager.js # 攔截器構(gòu)造函數(shù) │ │ └── settle.js # 根據(jù)http響應(yīng)狀態(tài),改變Promise的狀態(tài) │ ├── /helpers/ # 一些輔助方法 │ ├── /adapters/ # 定義請求的適配器 xhr、http │ │ ├── http.js # 實現(xiàn)http適配器 │ │ └── xhr.js # 實現(xiàn)xhr適配器 │ ├── axios.js # 對外暴露接口 │ ├── defaults.js # 默認配置 │ └── utils.js # 公用工具 ├── package.json # 項目信息 ├── index.d.ts # 配置TypeScript的聲明文件 └── index.js # 入口文件
注:因為我們需要要看的代碼都是/lib/目錄下的文件,所以以下所有涉及到文件路徑的地方,
我們都會在/lib/下進行查找
攔截器 interceptors
(如果你熟悉中間件,那么就很好理解了,因為它起到的就是基于promise的中間件的作用)
攔截器分為請求攔截器和響應(yīng)攔截器,顧名思義:
請求攔截器(interceptors.request)是指可以攔截住每次或指定http請求,并可修改配置項
響應(yīng)攔截器(interceptors.response)可以在每次http請求后攔截住每次或指定http請求,并可修改返回結(jié)果項。
這里先簡單說明,后面會做詳細的介紹如何攔截請求響應(yīng)并修改請求參數(shù)修改響應(yīng)數(shù)據(jù)。
數(shù)據(jù)轉(zhuǎn)換器 (其實就是對數(shù)據(jù)進行轉(zhuǎn)換,比如將對象轉(zhuǎn)換為JSON字符串)
數(shù)據(jù)轉(zhuǎn)換器分為請求轉(zhuǎn)換器和響應(yīng)轉(zhuǎn)換器,顧名思義:
請求轉(zhuǎn)換器(transformRequest)是指在請求前對數(shù)據(jù)進行轉(zhuǎn)換,
響應(yīng)轉(zhuǎn)換器(transformResponse)主要對請求響應(yīng)后的響應(yīng)體做數(shù)據(jù)轉(zhuǎn)換。
http請求適配器(其實就是一個方法)
在axios項目里,http請求適配器主要指兩種:XHR、http。
XHR的核心是瀏覽器端的XMLHttpRequest對象,
http核心是node的http[s].request方法
當然,axios也留給了用戶通過config自行配置適配器的接口的,
不過,一般情況下,這兩種適配器就能夠滿足從瀏覽器端向服務(wù)端發(fā)請求或者從node的http客戶端向服務(wù)端發(fā)請求的需求。
本次分享主要圍繞XHR。
config配置項 (其實就是一個對象)
此處我們說的config,在項目內(nèi)不是真的都叫config這個變量名,這個名字是我根據(jù)它的用途起的一個名字,方便大家理解。
在axios項目中的,設(shè)置讀取config時,
有的地方叫它defaults(/lib/defaults.js),這兒是默認配置項,
有的地方叫它config,如Axios.prototype.request的參數(shù),再如xhrAdapterhttp請求適配器方法的參數(shù)。
config在axios項目里的是非常重要的一條鏈,是用戶跟axios項目內(nèi)部“通信”的主要橋梁。
axios內(nèi)部的運作流程圖 工具方法簡單介紹(注:本節(jié)可先跳過,后面用到了再過來查看)
有一些方法在項目中多處使用,簡單介紹下這些方法
1.bind: 給某個函數(shù)指定上下文,也就是this指向
bind(fn, context);
實現(xiàn)效果同Function.prototype.bind方法: fn.bind(context)
2.forEach:遍歷數(shù)組或?qū)ο?/p>
var utils = require("./utils"); var forEach = utils.forEach; // 數(shù)組 utils.forEach([], (value, index, array) => {}) // 對象 utils.forEach({}, (value, key, object) => {})
3.merge:深度合并多個對象為一個對象
var utils = require("./utils"); var merge = utils.merge; var obj1 = { a: 1, b: { bb: 11, bbb: 111, } }; var obj2 = { a: 2, b: { bb: 22, } }; var mergedObj = merge(obj1, obj2);
mergedObj對象是:
{ a: 2, b: { bb: 22, bbb: 111 } }
4.extend:將一個對象的方法和屬性擴展到另外一個對象上,并指定上下文
var utils = require("./utils"); var extend = utils.extend; var context = { a: 4, }; var target = { k: "k1", fn(){ console.log(this.a + 1) } }; var source = { k: "k2", fn(){ console.log(this.a - 1) } }; let extendObj = extend(target, source, context);
extendObj對象是:
{ k: "k2", fn: source.fn.bind(context), }
執(zhí)行extendObj.fn(), 打印3
axios為何會有多種使用方式 如何使用// 首先將axios包引進來 import axios from "axios"
第1種使用方式:axios(option)
axios({ url, method, headers, })
第2種使用方式:axios(url[, option])
axios(url, { method, headers, })
第3種使用方式(對于get、delete等方法):axios[method](url[, option])
axios.get(url, { headers, })
第4種使用方式(對于post、put等方法):axios[method](url[, data[, option]])
axios.post(url, data, { headers, })
第5種使用方式:axios.request(option)
axios.request({ url, method, headers, })源碼分析
作為axios項目的入口文件,我們先來看下axios.js的源碼
能夠?qū)崿F(xiàn)axios的多種使用方式的核心是createInstance方法:
// /lib/axios.js function createInstance(defaultConfig) { // 創(chuàng)建一個Axios實例 var context = new Axios(defaultConfig); // 以下代碼也可以這樣實現(xiàn):var instance = Axios.prototype.request.bind(context); // 這樣instance就指向了request方法,且上下文指向context,所以可以直接以 instance(option) 方式調(diào)用 // Axios.prototype.request 內(nèi)對第一個參數(shù)的數(shù)據(jù)類型判斷,使我們能夠以 instance(url, option) 方式調(diào)用 var instance = bind(Axios.prototype.request, context); // 把Axios.prototype上的方法擴展到instance對象上, // 這樣 instance 就有了 get、post、put等方法 // 并指定上下文為context,這樣執(zhí)行Axios原型鏈上的方法時,this會指向context utils.extend(instance, Axios.prototype, context); // 把context對象上的自身屬性和方法擴展到instance上 // 注:因為extend內(nèi)部使用的forEach方法對對象做for in 遍歷時,只遍歷對象本身的屬性,而不會遍歷原型鏈上的屬性 // 這樣,instance 就有了 defaults、interceptors 屬性。(這兩個屬性后面我們會介紹) utils.extend(instance, context); return instance; } // 接收默認配置項作為參數(shù)(后面會介紹配置項),創(chuàng)建一個Axios實例,最終會被作為對象導(dǎo)出 var axios = createInstance(defaults);
以上代碼看上去很繞,其實createInstance最終是希望拿到一個Function,這個Function指向Axios.prototype.request,這個Function還會有Axios.prototype上的每個方法作為靜態(tài)方法,且這些方法的上下文都是指向同一個對象。
那么在來看看Axios、Axios.prototype.request的源碼是怎樣的?
Axios是axios包的核心,一個Axios實例就是一個axios應(yīng)用,其他方法都是對Axios內(nèi)容的擴展
而Axios構(gòu)造函數(shù)的核心方法是request方法,各種axios的調(diào)用方式最終都是通過request方法發(fā)請求的
// /lib/core/Axios.js function Axios(instanceConfig) { this.defaults = instanceConfig; this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() }; } Axios.prototype.request = function request(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 })); }; }); 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 })); }; });
通過以上代碼,我們就可以以多種方式發(fā)起http請求了: axios()、axios.get()、axios.post()
一般情況,項目使用默認導(dǎo)出的axios實例就可以滿足需求了,
如果不滿足需求需要創(chuàng)建新的axios實例,axios包也預(yù)留了接口,
看下面的代碼:
// /lib/axios.js - 31行 axios.Axios = Axios; axios.create = function create(instanceConfig) { return createInstance(utils.merge(defaults, instanceConfig)); };
說完axios為什么會有這么多種使用方式,可能你心中會有一個疑問:
使用axios時,無論get方法還是post方法,最終都是調(diào)用的Axios.prototype.request方法,那么這個方法是怎么根據(jù)我們的config配置發(fā)請求的呢?
在開始說Axios.prototype.request之前,我們先來捋一捋在axios項目中,用戶配置的config是怎么起作用的?
用戶配置的config是怎么起作用的這里說的config,指的是貫穿整個項目的配置項對象,
通過這個對象,可以設(shè)置:
`http請求適配器、請求地址、請求方法、請求頭header、
請求數(shù)據(jù)、請求或響應(yīng)數(shù)據(jù)的轉(zhuǎn)換、請求進度、http狀態(tài)碼驗證規(guī)則、超時、取消請求等`
可以發(fā)現(xiàn),幾乎axios所有的功能都是通過這個對象進行配置和傳遞的,
既是axios項目內(nèi)部的溝通橋梁,也是用戶跟axios進行溝通的橋梁。
首先我們看看,用戶能以什么方式定義配置項:
import axios from "axios" // 第1種:直接修改Axios實例上defaults屬性,主要用來設(shè)置通用配置 axios.defaults[configName] = value; // 第2種:發(fā)起請求時最終會調(diào)用Axios.prototype.request方法,然后傳入配置項,主要用來設(shè)置“個例”配置 axios({ url, method, headers, }) // 第3種:新建一個Axios實例,傳入配置項,此處設(shè)置的是通用配置 let newAxiosInstance = axios.create({ [configName]: value, })
看下 Axios.prototype.request 方法里的一行代碼: (/lib/core/Axios.js - 第35行)
config = utils.merge(defaults, {method: "get"}, this.defaults, config);
可以發(fā)現(xiàn)此處將默認配置對象defaults(/lib/defaults.js)、Axios實例屬性this.defaults、request請求的參數(shù)config進行了合并。
由此得出,多處配置的優(yōu)先級由低到高是:
—> 默認配置對象defaults(/lib/defaults.js)
—> { method: "get" }
—> Axios實例屬性this.defaults
—> request請求的參數(shù)config
留給大家思考一個問題: defaults 和 this.defaults 什么時候配置是相同的,什么時候是不同的?
至此,我們已經(jīng)得到了將多處merge后的config對象,那么這個對象在項目中又是怎樣傳遞的呢?
Axios.prototype.request = function request(config) { // ... config = utils.merge(defaults, {method: "get"}, this.defaults, config); var chain = [dispatchRequest, undefined]; // 將config對象當作參數(shù)傳給Primise.resolve方法 var promise = Promise.resolve(config); // ...省略代碼 while (chain.length) { // config會按序通過 請求攔截器 - dispatchRequest方法 - 響應(yīng)攔截器 // 關(guān)于攔截器 和 dispatchRequest方法,下面會作為一個專門的小節(jié)來介紹。 promise = promise.then(chain.shift(), chain.shift()); } return promise; };
至此,config走完了它傳奇的一生 -_-
下一節(jié)就要說到重頭戲了: Axios.prototype.request
這里面的代碼比較復(fù)雜,一些方法需要追根溯源才能搞清楚,
所以只需對chain數(shù)組有個簡單的了解就好,涉及到的攔截器、[dispatchRequest]后面都會詳細介紹
chain數(shù)組是用來盛放攔截器方法和dispatchRequest方法的,
通過promise從chain數(shù)組里按序取出回調(diào)函數(shù)逐一執(zhí)行,最后將處理后的新的promise在Axios.prototype.request方法里返回出去,
并將response或error傳送出去,這就是Axios.prototype.request的使命了。
查看源碼:
// /lib/core/Axios.js Axios.prototype.request = function request(config) { // ... 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; };
此時,你一定對攔截器充滿了好奇,這個攔截器到底是個什么家伙,下一節(jié)就讓我們一探究竟吧
如何攔截請求響應(yīng)并修改請求參數(shù)修改響應(yīng)數(shù)據(jù) 如何使用// 添加請求攔截器 const myRequestInterceptor = axios.interceptors.request.use(config => { // 在發(fā)送http請求之前做些什么 return config; // 有且必須有一個config對象被返回 }, error => { // 對請求錯誤做些什么 return Promise.reject(error); }); // 添加響應(yīng)攔截器 axios.interceptors.response.use(response => { // 對響應(yīng)數(shù)據(jù)做點什么 return response; // 有且必須有一個response對象被返回 }, error => { // 對響應(yīng)錯誤做點什么 return Promise.reject(error); }); // 移除某次攔截器 axios.interceptors.request.eject(myRequestInterceptor);思考
是否可以直接 return error?
axios.interceptors.request.use(config => config, error => { // 是否可以直接 return error ? return Promise.reject(error); });
如何實現(xiàn)promise的鏈式調(diào)用
new People("whr").sleep(3000).eat("apple").sleep(5000).eat("durian"); // 打印結(jié)果 // (等待3s)--> "whr eat apple" -(等待5s)--> "whr eat durian"源碼分析
關(guān)于攔截器,名詞解釋一節(jié)已經(jīng)做過簡單說明。
每個axios實例都有一個interceptors實例屬性,
interceptors對象上有兩個屬性request、response。
function Axios(instanceConfig) { // ... this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() }; }
這兩個屬性都是一個InterceptorManager實例,而這個InterceptorManager構(gòu)造函數(shù)就是用來管理攔截器的。
我們先來看看InterceptorManager構(gòu)造函數(shù):
InterceptorManager構(gòu)造函數(shù)就是用來實現(xiàn)攔截器的,這個構(gòu)造函數(shù)原型上有3個方法:use、eject、forEach。
關(guān)于源碼,其實是比較簡單的,都是用來操作該構(gòu)造函數(shù)的handlers實例屬性的。
// /lib/core/InterceptorManager.js function InterceptorManager() { this.handlers = []; // 存放攔截器方法,數(shù)組內(nèi)每一項都是有兩個屬性的對象,兩個屬性分別對應(yīng)成功和失敗后執(zhí)行的函數(shù)。 } // 往攔截器里添加攔截方法 InterceptorManager.prototype.use = function use(fulfilled, rejected) { this.handlers.push({ fulfilled: fulfilled, rejected: rejected }); return this.handlers.length - 1; }; // 用來注銷指定的攔截器 InterceptorManager.prototype.eject = function eject(id) { if (this.handlers[id]) { this.handlers[id] = null; } }; // 遍歷this.handlers,并將this.handlers里的每一項作為參數(shù)傳給fn執(zhí)行 InterceptorManager.prototype.forEach = function forEach(fn) { utils.forEach(this.handlers, function forEachHandler(h) { if (h !== null) { fn(h); } }); };
那么當我們通過axios.interceptors.request.use添加攔截器后,
axios內(nèi)部又是怎么讓這些攔截器能夠在請求前、請求后拿到我們想要的數(shù)據(jù)的呢?
先看下代碼:
// /lib/core/Axios.js Axios.prototype.request = function request(config) { // ... var chain = [dispatchRequest, undefined]; // 初始化一個promise對象,狀態(tài)微resolved,接收到的參數(shù)微config對象 var promise = Promise.resolve(config); // 注意:interceptor.fulfilled 或 interceptor.rejected 是可能為undefined 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數(shù)組大概會是這樣的: // [ // requestFulfilledFn, requestRejectedFn, ..., // dispatchRequest, undefined, // responseFulfilledFn, responseRejectedFn, ...., // ] // 只要chain數(shù)組長度不為0,就一直執(zhí)行while循環(huán) while (chain.length) { // 數(shù)組的 shift() 方法用于把數(shù)組的第一個元素從其中刪除,并返回第一個元素的值。 // 每次執(zhí)行while循環(huán),從chain數(shù)組里按序取出兩項,并分別作為promise.then方法的第一個和第二個參數(shù) // 按照我們使用InterceptorManager.prototype.use添加攔截器的規(guī)則,正好每次添加的就是我們通過InterceptorManager.prototype.use方法添加的成功和失敗回調(diào) // 通過InterceptorManager.prototype.use往攔截器數(shù)組里添加攔截器時使用的數(shù)組的push方法, // 對于請求攔截器,從攔截器數(shù)組按序讀到后是通過unshift方法往chain數(shù)組數(shù)里添加的,又通過shift方法從chain數(shù)組里取出的,所以得出結(jié)論:對于請求攔截器,先添加的攔截器會后執(zhí)行 // 對于響應(yīng)攔截器,從攔截器數(shù)組按序讀到后是通過push方法往chain數(shù)組里添加的,又通過shift方法從chain數(shù)組里取出的,所以得出結(jié)論:對于響應(yīng)攔截器,添加的攔截器先執(zhí)行 // 第一個請求攔截器的fulfilled函數(shù)會接收到promise對象初始化時傳入的config對象,而請求攔截器又規(guī)定用戶寫的fulfilled函數(shù)必須返回一個config對象,所以通過promise實現(xiàn)鏈式調(diào)用時,每個請求攔截器的fulfilled函數(shù)都會接收到一個config對象 // 第一個響應(yīng)攔截器的fulfilled函數(shù)會接受到dispatchRequest(也就是我們的請求方法)請求到的數(shù)據(jù)(也就是response對象),而響應(yīng)攔截器又規(guī)定用戶寫的fulfilled函數(shù)必須返回一個response對象,所以通過promise實現(xiàn)鏈式調(diào)用時,每個響應(yīng)攔截器的fulfilled函數(shù)都會接收到一個response對象 // 任何一個攔截器的拋出的錯誤,都會被下一個攔截器的rejected函數(shù)收到,所以dispatchRequest拋出的錯誤才會被響應(yīng)攔截器接收到。 // 因為axios是通過promise實現(xiàn)的鏈式調(diào)用,所以我們可以在攔截器里進行異步操作,而攔截器的執(zhí)行順序還是會按照我們上面說的順序執(zhí)行,也就是 dispatchRequest 方法一定會等待所有的請求攔截器執(zhí)行完后再開始執(zhí)行,響應(yīng)攔截器一定會等待 dispatchRequest 執(zhí)行完后再開始執(zhí)行。 promise = promise.then(chain.shift(), chain.shift()); } return promise; };
現(xiàn)在,你應(yīng)該已經(jīng)清楚了攔截器是怎么回事,以及攔截器是如何在Axios.prototype.request方法里發(fā)揮作用的了,
那么處于"中游位置"的dispatchRequest是如何發(fā)送http請求的呢?
dispatchRequest主要做了3件事:
1,拿到config對象,對config進行傳給http請求適配器前的最后處理;
2,http請求適配器根據(jù)config配置,發(fā)起請求
3,http請求適配器請求完成后,如果成功則根據(jù)header、data、和config.transformResponse(關(guān)于transformResponse,下面的數(shù)據(jù)轉(zhuǎn)換器會進行講解)拿到數(shù)據(jù)轉(zhuǎn)換后的response,并return。
// /lib/core/dispatchRequest.js module.exports = function dispatchRequest(config) { throwIfCancellationRequested(config); // Support baseURL config if (config.baseURL && !isAbsoluteURL(config.url)) { config.url = combineURLs(config.baseURL, config.url); } // Ensure headers exist config.headers = config.headers || {}; // 對請求data進行轉(zhuǎn)換 config.data = transformData( config.data, config.headers, config.transformRequest ); // 對header進行合并處理 config.headers = utils.merge( config.headers.common || {}, config.headers[config.method] || {}, config.headers || {} ); // 刪除header屬性里無用的屬性 utils.forEach( ["delete", "get", "head", "post", "put", "patch", "common"], function cleanHeaderConfig(method) { delete config.headers[method]; } ); // http請求適配器會優(yōu)先使用config上自定義的適配器,沒有配置時才會使用默認的XHR或http適配器,不過大部分時候,axios提供的默認適配器是能夠滿足我們的 var adapter = config.adapter || defaults.adapter; return adapter(config).then(/**/); };
好了,看到這里,我們是時候梳理一下:axios是如何用promise搭起基于xhr的異步橋梁的?
axios是如何用promise搭起基于xhr的異步橋梁的axios是如何通過Promise進行異步處理的?
如何使用import axios from "axios" axios.get(/**/) .then(data => { // 此處可以拿到向服務(wù)端請求回的數(shù)據(jù) }) .catch(error => { // 此處可以拿到請求失敗或取消或其他處理失敗的錯誤對象 })源碼分析
先來一個圖簡單的了解下axios項目里,http請求完成后到達用戶的順序流:
通過axios為何會有多種使用方式我們知道,
用戶無論以什么方式調(diào)用axios,最終都是調(diào)用的Axios.prototype.request方法,
這個方法最終返回的是一個Promise對象。
Axios.prototype.request = function request(config) { // ... var chain = [dispatchRequest, undefined]; // 將config對象當作參數(shù)傳給Primise.resolve方法 var promise = Promise.resolve(config); while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } return promise; };
Axios.prototype.request方法會調(diào)用dispatchRequest方法,而dispatchRequest方法會調(diào)用xhrAdapter方法,xhrAdapter方法返回的是還一個Promise對象
// /lib/adapters/xhr.js function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { // ... 省略代碼 }); };
xhrAdapter內(nèi)的XHR發(fā)送請求成功后會執(zhí)行這個Promise對象的resolve方法,并將請求的數(shù)據(jù)傳出去,
反之則執(zhí)行reject方法,并將錯誤信息作為參數(shù)傳出去。
// /lib/adapters/xhr.js var request = new XMLHttpRequest(); var loadEvent = "onreadystatechange"; request[loadEvent] = function handleLoad() { // ... // 往下走有settle的源碼 settle(resolve, reject, response); // ... }; request.onerror = function handleError() { reject(/**/); request = null; }; request.ontimeout = function handleTimeout() { reject(/**/); request = null; };
驗證服務(wù)端的返回結(jié)果是否通過驗證:
// /lib/core/settle.js function settle(resolve, reject, response) { var validateStatus = response.config.validateStatus; if (!response.status || !validateStatus || validateStatus(response.status)) { resolve(response); } else { reject(/**/); } };
回到dispatchRequest方法內(nèi),首先得到xhrAdapter方法返回的Promise對象,
然后通過.then方法,對xhrAdapter返回的Promise對象的成功或失敗結(jié)果再次加工,
成功的話,則將處理后的response返回,
失敗的話,則返回一個狀態(tài)為rejected的Promise對象,
return adapter(config).then(function onAdapterResolution(response) { // ... return response; }, function onAdapterRejection(reason) { // ... return Promise.reject(reason); }); };
那么至此,用戶調(diào)用axios()方法時,就可以直接調(diào)用Promise的.then或.catch進行業(yè)務(wù)處理了。
回過頭來,我們在介紹dispatchRequest一節(jié)時說到的數(shù)據(jù)轉(zhuǎn)換,而axios官方也將數(shù)據(jù)轉(zhuǎn)換專門作為一個亮點來介紹的,那么數(shù)據(jù)轉(zhuǎn)換到底能在使用axios發(fā)揮什么功效呢?
數(shù)據(jù)轉(zhuǎn)換器-轉(zhuǎn)換請求與響應(yīng)數(shù)據(jù) 如何使用修改全局的轉(zhuǎn)換器
import axios from "axios" // 往現(xiàn)有的請求轉(zhuǎn)換器里增加轉(zhuǎn)換方法 axios.defaults.transformRequest.push((data, headers) => { // ...處理data return data; }); // 重寫請求轉(zhuǎn)換器 axios.defaults.transformRequest = [(data, headers) => { // ...處理data return data; }]; // 往現(xiàn)有的響應(yīng)轉(zhuǎn)換器里增加轉(zhuǎn)換方法 axios.defaults.transformResponse.push((data, headers) => { // ...處理data return data; }); // 重寫響應(yīng)轉(zhuǎn)換器 axios.defaults.transformResponse = [(data, headers) => { // ...處理data return data; }];
修改某次axios請求的轉(zhuǎn)換器
import axios from "axios" // 往已經(jīng)存在的轉(zhuǎn)換器里增加轉(zhuǎn)換方法 axios.get(url, { // ... transformRequest: [ ...axios.defaults.transformRequest, // 去掉這行代碼就等于重寫請求轉(zhuǎn)換器了 (data, headers) => { // ...處理data return data; } ], transformResponse: [ ...axios.defaults.transformResponse, // 去掉這行代碼就等于重寫響應(yīng)轉(zhuǎn)換器了 (data, headers) => { // ...處理data return data; } ], })源碼分析
默認的defaults配置項里已經(jīng)自定義了一個請求轉(zhuǎn)換器和一個響應(yīng)轉(zhuǎn)換器,
看下源碼:
// /lib/defaults.js var defaults = { transformRequest: [function transformRequest(data, headers) { normalizeHeaderName(headers, "Content-Type"); // ... if (utils.isArrayBufferView(data)) { return data.buffer; } if (utils.isURLSearchParams(data)) { setContentTypeIfUnset(headers, "application/x-www-form-urlencoded;charset=utf-8"); return data.toString(); } if (utils.isObject(data)) { setContentTypeIfUnset(headers, "application/json;charset=utf-8"); return JSON.stringify(data); } return data; }], transformResponse: [function transformResponse(data) { if (typeof data === "string") { try { data = JSON.parse(data); } catch (e) { /* Ignore */ } } return data; }], };
那么在axios項目里,是在什么地方使用了轉(zhuǎn)換器呢?
請求轉(zhuǎn)換器的使用地方是http請求前,使用請求轉(zhuǎn)換器對請求數(shù)據(jù)做處理,
然后傳給http請求適配器使用。
// /lib/core/dispatchRequest.js function dispatchRequest(config) { config.data = transformData( config.data, config.headers, config.transformRequest ); return adapter(config).then(/* ... */); };
看下transformData方法的代碼,
主要遍歷轉(zhuǎn)換器數(shù)組,分別執(zhí)行每一個轉(zhuǎn)換器,根據(jù)data和headers參數(shù),返回新的data。
// /lib/core/transformData.js function transformData(data, headers, fns) { utils.forEach(fns, function transform(fn) { data = fn(data, headers); }); return data; };
響應(yīng)轉(zhuǎn)換器的使用地方是在http請求完成后,根據(jù)http請求適配器的返回值做數(shù)據(jù)轉(zhuǎn)換處理:
// /lib/core/dispatchRequest.js return adapter(config).then(function onAdapterResolution(response) { // ... response.data = transformData( response.data, response.headers, config.transformResponse ); return response; }, function onAdapterRejection(reason) { if (!isCancel(reason)) { // ... if (reason && reason.response) { reason.response.data = transformData( reason.response.data, reason.response.headers, config.transformResponse ); } } return Promise.reject(reason); });轉(zhuǎn)換器和攔截器的關(guān)系?
攔截器同樣可以實現(xiàn)轉(zhuǎn)換請求和響應(yīng)數(shù)據(jù)的需求,但根據(jù)作者的設(shè)計和綜合代碼可以看出,
在請求時,攔截器主要負責(zé)修改config配置項,數(shù)據(jù)轉(zhuǎn)換器主要負責(zé)轉(zhuǎn)換請求體,比如轉(zhuǎn)換對象為字符串
在請求響應(yīng)后,攔截器可以拿到response,數(shù)據(jù)轉(zhuǎn)換器主要負責(zé)處理響應(yīng)體,比如轉(zhuǎn)換字符串為對象。
axios官方是將"自動轉(zhuǎn)換為JSON數(shù)據(jù)"作為一個獨立的亮點來介紹的,那么數(shù)據(jù)轉(zhuǎn)換器是如何完成這個功能的呢?
其實非常簡單,我們一起看下吧。
在默認情況下,axios將會自動的將傳入的data對象序列化為JSON字符串,將響應(yīng)數(shù)據(jù)中的JSON字符串轉(zhuǎn)換為JavaScript對象
源碼分析// 請求時,將data數(shù)據(jù)轉(zhuǎn)換為JSON 字符串 // /lib/defaults.js transformRequest: [function transformRequest(data, headers) { // ... if (utils.isObject(data)) { setContentTypeIfUnset(headers, "application/json;charset=utf-8"); return JSON.stringify(data); } return data; }] // 得到響應(yīng)后,將請求到的數(shù)據(jù)轉(zhuǎn)換為JSON對象 // /lib/defaults.js transformResponse: [function transformResponse(data) { if (typeof data === "string") { try { data = JSON.parse(data); } catch (e) { /* Ignore */ } } return data; }]
至此,axios項目的運作流程已經(jīng)介紹完畢,是不是已經(jīng)打通了任督二脈了呢
接下來我們一起看下axios還帶給了我們哪些好用的技能點吧。
import axios from "axios" // 設(shè)置通用header axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; // xhr標識 // 設(shè)置某種請求的header axios.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded;charset=utf-8"; // 設(shè)置某次請求的header axios.get(url, { headers: { "Authorization": "whr1", }, })源碼分析
// /lib/core/dispatchRequest.js - 44行 config.headers = utils.merge( config.headers.common || {}, config.headers[config.method] || {}, config.headers || {} );如何取消已經(jīng)發(fā)送的請求 如何使用
import axios from "axios" // 第一種取消方法 axios.get(url, { cancelToken: new axios.CancelToken(cancel => { if (/* 取消條件 */) { cancel("取消日志"); } }) }); // 第二種取消方法 const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios.get(url, { cancelToken: source.token }); source.cancel("取消日志");源碼分析
// /cancel/CancelToken.js - 11行 function CancelToken(executor) { var resolvePromise; this.promise = new Promise(function promiseExecutor(resolve) { resolvePromise = resolve; }); var token = this; executor(function cancel(message) { if (token.reason) { return; } token.reason = new Cancel(message); resolvePromise(token.reason); }); } // /lib/adapters/xhr.js - 159行 if (config.cancelToken) { config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } request.abort(); reject(cancel); request = null; }); }
取消功能的核心是通過CancelToken內(nèi)的this.promise = new Promise(resolve => resolvePromise = resolve),
得到實例屬性promise,此時該promise的狀態(tài)為pending
通過這個屬性,在/lib/adapters/xhr.js文件中繼續(xù)給這個promise實例添加.then方法
(xhr.js文件的159行config.cancelToken.promise.then(message => request.abort()));
在CancelToken外界,通過executor參數(shù)拿到對cancel方法的控制權(quán),
這樣當執(zhí)行cancel方法時就可以改變實例的promise屬性的狀態(tài)為rejected,
從而執(zhí)行request.abort()方法達到取消請求的目的。
上面第二種寫法可以看作是對第一種寫法的完善,
因為很多是時候我們?nèi)∠埱蟮姆椒ㄊ怯迷诒敬握埱蠓椒ㄍ猓?br>例如,發(fā)送A、B兩個請求,當B請求成功后,取消A請求。
// 第1種寫法: let source; axios.get(Aurl, { cancelToken: new axios.CancelToken(cancel => { source = cancel; }) }); axios.get(Burl) .then(() => source("B請求成功了")); // 第2種寫法: const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios.get(Aurl, { cancelToken: source.token }); axios.get(Burl) .then(() => source.cancel("B請求成功了"));
相對來說,我更推崇第1種寫法,因為第2種寫法太隱蔽了,不如第一種直觀好理解。
/lib/adapters/xhr.js文件中,onCanceled方法的參數(shù)不應(yīng)該叫message么,為什么叫cancel?
/lib/adapters/xhr.js文件中,onCanceled方法里,reject里應(yīng)該將config信息也傳出來
跨域攜帶cookie 如何使用import axios from "axios" axios.defaults.withCredentials = true;源碼分析
我們在用戶配置的config是怎么起作用的一節(jié)已經(jīng)介紹了config在axios項目里的傳遞過程,
由此得出,我們通過axios.defaults.withCredentials = true做的配置,
在/lib/adapters/xhr.js里是可以取到的,然后通過以下代碼配置到xhr對象項。
var request = new XMLHttpRequest(); // /lib/adapters/xhr.js if (config.withCredentials) { request.withCredentials = true; }超時配置及處理 如何使用
import axios from "axios" axios.defaults.timeout = 3000;源碼分析
// /adapters/xhr.js request.timeout = config.timeout; // /adapters/xhr.js // 通過createError方法,將錯誤信息合為一個字符串 request.ontimeout = function handleTimeout() { reject(createError("timeout of " + config.timeout + "ms exceeded", config, "ECONNABORTED", request)); };
axios庫外如何添加超時后的處理
axios().catch(error => { const { message } = error; if (message.indexOf("timeout") > -1){ // 超時處理 } })改寫驗證成功或失敗的規(guī)則validatestatus
自定義http狀態(tài)碼的成功、失敗范圍
如何使用import axios from "axios" axios.defaults.validateStatus = status => status >= 200 && status < 300;源碼分析
在默認配置中,定義了默認的http狀態(tài)碼驗證規(guī)則,
所以自定義validateStatus其實是對此處方法的重寫
// `/lib/defaults.js` var defaults = { // ... validateStatus: function validateStatus(status) { return status >= 200 && status < 300; }, // ... }
axios是何時開始驗證http狀態(tài)碼的?
// /lib/adapters/xhr.js var request = new XMLHttpRequest(); var loadEvent = "onreadystatechange"; // /lib/adapters/xhr.js // 每當 readyState 改變時,就會觸發(fā) onreadystatechange 事件 request[loadEvent] = function handleLoad() { if (!request || (request.readyState !== 4 && !xDomain)) { return; } // ...省略代碼 var response = { // ... // IE sends 1223 instead of 204 (https://github.com/axios/axios/issues/201) status: request.status === 1223 ? 204 : request.status, config: config, }; settle(resolve, reject, response); // ...省略代碼 }
// /lib/core/settle.js function settle(resolve, reject, response) { // 如果我們往上搗一搗就會發(fā)現(xiàn),config對象的validateStatus就是我們自定義的validateStatus方法或默認的validateStatus方法 var validateStatus = response.config.validateStatus; // validateStatus驗證通過,就會觸發(fā)resolve方法 if (!response.status || !validateStatus || validateStatus(response.status)) { resolve(response); } else { reject(createError( "Request failed with status code " + response.status, response.config, null, response.request, response )); } };總結(jié)
axios這個項目里,有很多對JS使用很巧妙的地方,比如對promise的串聯(lián)操作(當然你也可以說這塊是借鑒很多異步中間件的處理方式),讓我們可以很方便對請求前后的各種處理方法的流程進行控制;很多實用的小優(yōu)化,比如請求前后的數(shù)據(jù)處理,省了程序員一遍一遍去寫JSON.xxx了;同時支持了瀏覽器和node兩種環(huán)境,對使用node的項目來說無疑是極好的。
總之,這個能夠在github斬獲42K+(截止2018.05.27)的star,實力絕不是蓋的,值得好好交交心!
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/95292.html
摘要:讓你收獲滿滿碼個蛋從年月日推送第篇文章一年過去了已累積推文近篇文章,本文為年度精選,共計篇,按照類別整理便于讀者主題閱讀。本篇文章是今年的最后一篇技術(shù)文章,為了讓大家在家也能好好學(xué)習(xí),特此花了幾個小時整理了這些文章。 showImg(https://segmentfault.com/img/remote/1460000013241596); 讓你收獲滿滿! 碼個蛋從2017年02月20...
寫在前面 當大多數(shù)人Vue理解的爐火純青的時候,你應(yīng)該思考怎么讓vue頁面騷氣起來,下面就我個人在接觸Vue兩年的時間里,在實際工作中門戶網(wǎng)站在前端頁面交互應(yīng)用和技巧,炒幾道小菜給大家分享一哈,我把它封裝成一個項目vue-portal-webUI(github源碼),不敢說是UI,但也是各種常見常遇到的情景吧,看懂代碼需要一些vue、axios、es6、scss基礎(chǔ)、數(shù)據(jù)基本上是mock,功能和場...
摘要:網(wǎng)頁可訪問性似乎是一項艱巨的任務(wù),但它確實比聽起來要容易很多,這十條網(wǎng)頁可訪問性準則旨在確保所有網(wǎng)站都是通用的。 推薦 1. 阿里電商架構(gòu)演變之路 https://yq.aliyun.com/article... 首屆阿里巴巴中間件技術(shù)峰會上,阿里巴巴中間件技術(shù)部專家唐三帶來阿里電商架構(gòu)演變之路的演講,本文從阿里業(yè)務(wù)和技術(shù)架構(gòu)開始引入,分別分享了阿里電商從1.0到4.0架構(gòu)的演變之路,...
摘要:寫在前面本人自從事前端工作以來,每每遇到技術(shù)問題后,都是向百度谷歌求教,久而久之便養(yǎng)成了伸手即來的本領(lǐng)。。??赡苁浅鲇趹械脑驔]錯,就是因為懶,從未發(fā)表過技術(shù)文章或開發(fā)經(jīng)驗,深感慚愧,故洗心革面,決心分享一些東西。 寫在前面 本人自從事前端工作以來,每每遇到技術(shù)問題后,都是向百度/谷歌求教,久而久之便養(yǎng)成了伸手即來的‘本領(lǐng)’。。??赡苁浅鲇凇畱小脑颍]錯,就是因為懶),從未發(fā)表過技...
閱讀 1019·2021-09-30 09:58
閱讀 2852·2021-09-09 11:55
閱讀 2016·2021-09-01 11:41
閱讀 1005·2019-08-30 15:55
閱讀 3366·2019-08-30 12:50
閱讀 3511·2019-08-29 18:37
閱讀 3313·2019-08-29 16:37
閱讀 2024·2019-08-29 13:00