axios 是一個基於Promise 的http請求函式庫,可以用在瀏覽器和node.js中,目前在github上有42K 的star數
├── /dist/ # 项目输出目录
├── /lib/ # 项目源码目录
│ ├── /cancel/ # 定义取消功能
│ ├── /core/ # 一些核心功能
│ │ ├── Axios.js # axios的核心主类
│ │ ├── dispatchRequest.js # 用来调用http请求适配器方法发送请求
│ │ ├── InterceptorManager.js # 拦截器构造函数
│ │ └── settle.js # 根据http响应状态,改变Promise的状态
│ ├── /helpers/ # 一些辅助方法
│ ├── /adapters/ # 定义请求的适配器 xhr、http
│ │ ├── http.js # 实现http适配器
│ │ └── xhr.js # 实现xhr适配器
│ ├── axios.js # 对外暴露接口
│ ├── defaults.js # 默认配置
│ └── utils.js # 公用工具
├── package.json # 项目信息
├── index.d.ts # 配置TypeScript的声明文件
└── index.js # 入口文件
註:因為我們需要要看的程式碼都是/lib/
目錄下的文件,所以以下所有涉及到文件路徑的地方, 我們都會在/lib/
下進行查找
攔截器interceptors
(如果你熟悉中間件,那麼就很好理解了,因為它起到的就是基於promise的中間件的作用)
攔截器分為請求攔截器和回應攔截器,顧名思義: 請求攔截器( interceptors.request
)是指可以攔截住每次或指定http請求,並可修改配置項回應攔截器( interceptors.response
)可以在每次http請求後攔截住每次或指定http請求,並可修改回傳結果項目。
這裡先簡單說明,後面會做詳細的介紹如何攔截請求回應並修改請求參數修改回應資料。
資料轉換器(其實就是對資料進行轉換,例如將物件轉換為JSON字串)
資料轉換器分為請求轉換器和回應轉換器,顧名思義: 請求轉換器( transformRequest
)是指在請求前對資料進行轉換, 回應轉換器( transformResponse
)主要對請求回應後的回應體做資料轉換。
http請求適配器(其實就是一個方法)
在axios專案裡,http請求適配器主要指兩種:XHR、http。 XHR的核心是瀏覽器端的XMLHttpRequest對象, http核心是node的http[s].request方法
當然,axios也留給了用戶透過config自行配置適配器的介面的, 不過,一般情況下,這兩種適配器就能夠滿足從瀏覽器端向服務端發請求或者從node的http客戶端向服務端發請求的需求。
本次分享主要圍繞XHR。
config配置項目(其實就是一個物件)
這裡我們說的config,專案內不是真的都叫config這個變數名,這個名字是我根據它的用途取的一個名字,方便大家理解。
在axios專案中的,設定讀取config時, 有的地方叫它defaults
( /lib/defaults.js
),這兒是預設配置項, 有的地方叫它config
,如Axios.prototype.request
的參數,再如xhrAdapter
http請求適配器方法的參數。
config在axios專案裡的是非常重要的一條鏈,是使用者跟axios專案內部「通訊」的主要橋樑。
(註:本節可先跳過,後面用到了再過來查看)
有一些方法在專案中多處使用,簡單介紹下這些方法
bind ( fn , context ) ;
實作效果同Function.prototype.bind
方法: fn.bind(context)
var utils = require ( './utils' ) ;
var forEach = utils . forEach ;
// 数组
utils . forEach ( [ ] , ( value , index , array ) => { } )
// 对象
utils . forEach ( { } , ( value , key , object ) => { } )
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
}
}
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 ) ,
}
執行extendObj.fn();
, 列印3
// 首先将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
的源碼能夠實現axios的多種使用方式的核心是createInstance
方法:
// /lib/axios.js
function createInstance ( defaultConfig ) {
// 创建一个Axios实例
var context = new Axios ( defaultConfig ) ;
// 以下代码也可以这样实现:var instance = Axios.prototype.request.bind(context);
// 这样instance就指向了request方法,且上下文指向context,所以可以直接以 instance(option) 方式调用
// Axios.prototype.request 内对第一个参数的数据类型判断,使我们能够以 instance(url, option) 方式调用
var instance = bind ( Axios . prototype . request , context ) ;
// 把Axios.prototype上的方法扩展到instance对象上,
// 这样 instance 就有了 get、post、put等方法
// 并指定上下文为context,这样执行Axios原型链上的方法时,this会指向context
utils . extend ( instance , Axios . prototype , context ) ;
// 把context对象上的自身属性和方法扩展到instance上
// 注:因为extend内部使用的forEach方法对对象做for in 遍历时,只遍历对象本身的属性,而不会遍历原型链上的属性
// 这样,instance 就有了 defaults、interceptors 属性。(这两个属性后面我们会介绍)
utils . extend ( instance , context ) ;
return instance ;
}
// 接收默认配置项作为参数(后面会介绍配置项),创建一个Axios实例,最终会被作为对象导出
var axios = createInstance ( defaults ) ;
以上程式碼看起來很繞,其實createInstance
最後是希望拿到一個Function,這個Function指向Axios.prototype.request
,這個Function還會有Axios.prototype
上的每個方法作為靜態方法,而這些方法的上下文都是指向同一個物件。
那再來看看Axios、Axios.prototype.request
的源碼是怎麼樣的呢?
Axios
是axios套件的核心,一個Axios
實例就是一個axios應用,其他方法都是對Axios
內容的擴充而Axios
建構子的核心方法是request
方法,各種axios的呼叫方式最終都是透過request
方法發請求的
// /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
} ) ) ;
} ;
} ) ;
透過上述程式碼,我們就可以用多種方式發起http請求了: axios()、axios.get()、axios.post()
一般情況,專案使用預設導出的axios實例就可以滿足需求了, 如果不滿足需求需要創建新的axios實例,axios套件也預留了接口, 看下面的程式碼:
// /lib/axios.js - 31行
axios . Axios = Axios ;
axios . create = function create ( instanceConfig ) {
return createInstance ( utils . merge ( defaults , instanceConfig ) ) ;
} ;
說完axios為什麼會有這麼多使用方式,可能你心中會有一個疑問: 使用axios時,無論get
方法還是post
方法,最終都是調用的Axios.prototype.request
方法,那麼這個方法是怎麼根據我們的config配置發請求的呢?
在開始說Axios.prototype.request
之前,我們先來捋一捋在axios專案中,使用者設定的config是怎麼運作的?
這裡說的config
,指的是貫穿整個專案的配置項對象, 透過這個對象,可以設定:
http请求适配器、请求地址、请求方法、请求头header、 请求数据、请求或响应数据的转换、请求进度、http状态码验证规则、超时、取消请求等
可以發現,幾乎axios
所有的功能都是透過這個物件進行配置和傳遞的, 既是axios
專案內部的溝通橋樑,也是用戶跟axios
進行溝通的橋樑。
首先我們來看看,用戶能以什麼方式定義配置項:
import axios from 'axios'
// 第1种:直接修改Axios实例上defaults属性,主要用来设置通用配置
axios . defaults [ configName ] = value ;
// 第2种:发起请求时最终会调用Axios.prototype.request方法,然后传入配置项,主要用来设置“个例”配置
axios ( {
url ,
method ,
headers ,
} )
// 第3种:新建一个Axios实例,传入配置项,此处设置的是通用配置
let newAxiosInstance = axios . create ( {
[ configName ] : value ,
} )
看下Axios.prototype.request
方法裡的一行程式碼: ( /lib/core/Axios.js
- 第35行)
config = utils . merge ( defaults , { method : 'get' } , this . defaults , config ) ;
可以發現此處將預設配置物件defaults
( /lib/defaults.js
)、Axios實例屬性this.defaults
、 request
請求的參數config
進行了合併。
由此得出,多處配置的優先權由低到高是: —> 預設配置物件defaults
( /lib/defaults.js
)
—> { method: 'get' }
—> Axios實例屬性this.defaults
—> request
請求的參數config
留給大家思考一個問題: defaults
和this.defaults
什麼時候配置是相同的,什麼時候是不同的?
至此,我們已經得到了將多處merge
後的config
對象,那麼這個對像在專案中又是怎麼傳遞的呢?
Axios . prototype . request = function request ( config ) {
// ...
config = utils . merge ( defaults , { method : 'get' } , this . defaults , config ) ;
var chain = [ dispatchRequest , undefined ] ;
// 将config对象当作参数传给Primise.resolve方法
var promise = Promise . resolve ( config ) ;
// ...省略代码
while ( chain . length ) {
// config会按序通过 请求拦截器 - dispatchRequest方法 - 响应拦截器
// 关于拦截器 和 dispatchRequest方法,下面会作为一个专门的小节来介绍。
promise = promise . then ( chain . shift ( ) , chain . shift ( ) ) ;
}
return promise ;
} ;
至此, config
走完了它傳奇的一生-_-
下一節就要說到重頭戲了: Axios.prototype.request
這裡面的程式碼比較複雜,有些方法需要追根溯源才能搞清楚, 所以只要對chain數組有個簡單的了解就好,涉及到的攔截器、[ dispatchRequest
]後面都會詳細介紹
chain
數組是用來盛放攔截器方法和dispatchRequest
方法的, 透過promise從chain
數組裡按序取出回調函數逐一執行,最後將處理後的新的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 ;
} ;
此時,你一定對攔截器充滿了好奇,這個攔截器到底是個什麼傢伙,下一節就讓我們一探究竟吧
// 添加请求拦截器
const myRequestInterceptor = axios . interceptors . request . use ( config => {
// 在发送http请求之前做些什么
return config ; // 有且必须有一个config对象被返回
} , error => {
// 对请求错误做些什么
return Promise . reject ( error ) ;
} ) ;
// 添加响应拦截器
axios . interceptors . response . use ( response => {
// 对响应数据做点什么
return response ; // 有且必须有一个response对象被返回
} , error => {
// 对响应错误做点什么
return Promise . reject ( error ) ;
} ) ;
// 移除某次拦截器
axios . interceptors . request . eject ( myRequestInterceptor ) ;
axios . interceptors . request . use ( config => config , error => {
// 是否可以直接 return error ?
return Promise . reject ( error ) ;
} ) ;
new People ( 'whr' ) . sleep ( 3000 ) . eat ( 'apple' ) . sleep ( 5000 ) . eat ( 'durian' ) ;
// 打印结果
// (等待3s)--> 'whr eat apple' -(等待5s)--> 'whr eat durian'
關於攔截器,名詞解釋一節已經做過簡單說明。
每個axios實例都有一個interceptors
實例屬性, interceptors
物件上有兩個屬性request
、 response
。
function Axios ( instanceConfig ) {
// ...
this . interceptors = {
request : new InterceptorManager ( ) ,
response : new InterceptorManager ( )
} ;
}
這兩個屬性都是一個InterceptorManager
實例,而這個InterceptorManager
建構子就是用來管理攔截器的。
我們先來看看InterceptorManager
建構子:
InterceptorManager
建構子是用來實作攔截器的,這個建構子原型上有3個方法:use、eject、forEach。 關於原始碼,其實是比較簡單的,都是用來操作該建構子的handlers實例屬性的。
// /lib/core/InterceptorManager.js
function InterceptorManager ( ) {
this . handlers = [ ] ; // 存放拦截器方法,数组内每一项都是有两个属性的对象,两个属性分别对应成功和失败后执行的函数。
}
// 往拦截器里添加拦截方法
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里的每一项作为参数传给fn执行
InterceptorManager . prototype . forEach = function forEach ( fn ) {
utils . forEach ( this . handlers , function forEachHandler ( h ) {
if ( h !== null ) {
fn ( h ) ;
}
} ) ;
} ;
那麼當我們透過axios.interceptors.request.use
新增攔截器後, axios內部又是怎麼讓這些攔截器能夠在請求前、請求後拿到我們想要的資料的呢?
先看下程式碼:
// /lib/core/Axios.js
Axios . prototype . request = function request ( config ) {
// ...
var chain = [ dispatchRequest , undefined ] ;
// 初始化一个promise对象,状态为resolved,接收到的参数为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数组大概会是这样的:
// [
// requestFulfilledFn, requestRejectedFn, ...,
// dispatchRequest, undefined,
// responseFulfilledFn, responseRejectedFn, ....,
// ]
// 只要chain数组长度不为0,就一直执行while循环
while ( chain . length ) {
// 数组的 shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
// 每次执行while循环,从chain数组里按序取出两项,并分别作为promise.then方法的第一个和第二个参数
// 按照我们使用InterceptorManager.prototype.use添加拦截器的规则,
// 正好每次添加的就是我们通过InterceptorManager.prototype.use方法添加的成功和失败回调
// 通过InterceptorManager.prototype.use往拦截器数组里添加拦截器时使用的数组的push方法,
// 对于请求拦截器:
// 从拦截器数组按序读到后是通过unshift方法往chain数组数里添加的,又通过shift方法从chain数组里取出的,
// 所以得出结论:对于请求拦截器,先添加的拦截器会后执行
// 对于响应拦截器:
// 从拦截器数组按序读到后是通过push方法往chain数组里添加的,又通过shift方法从chain数组里取出的,
// 所以得出结论:对于响应拦截器,添加的拦截器先执行
// 第一个请求拦截器的fulfilled函数会接收到promise对象初始化时传入的config对象,
// 而请求拦截器又规定用户写的fulfilled函数必须返回一个config对象,
// 所以通过promise实现链式调用时,每个请求拦截器的fulfilled函数都会接收到一个config对象
// 第一个响应拦截器的fulfilled函数会接受到dispatchRequest(也就是我们的请求方法)请求到的数据(也就是response对象),
// 而响应拦截器又规定用户写的fulfilled函数必须返回一个response对象,
// 所以通过promise实现链式调用时,每个响应拦截器的fulfilled函数都会接收到一个response对象
// 任何一个拦截器的抛出的错误,都会被下一个拦截器的rejected函数收到,
// 所以dispatchRequest抛出的错误才会被响应拦截器接收到。
// 因为axios是通过promise实现的链式调用,所以我们可以在拦截器里进行异步操作,
// 而拦截器的执行顺序还是会按照我们上面说的顺序执行,
// 也就是 dispatchRequest 方法一定会等待所有的请求拦截器执行完后再开始执行,
// 响应拦截器一定会等待 dispatchRequest 执行完后再开始执行。
promise = promise . then ( chain . shift ( ) , chain . shift ( ) ) ;
}
return promise ;
} ;
現在,你應該已經清楚了攔截器是怎麼回事,以及攔截器是如何在Axios.prototype.request
方法裡發揮作用的了, 那麼處於"中游位置"的dispatchRequest
是如何發送http請求的呢?
dispatchRequest主要做了3件事: 1,拿到config對象,對config進行傳給http請求適配器前的最後處理;2,http請求適配器根據config配置,發起請求3,http請求適配器請求完成後,如果成功則依照header、data、和config.transformResponse(關於transformResponse,下面的資料轉換器會進行解說)拿到資料轉換後的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进行转换
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请求适配器会优先使用config上自定义的适配器,没有配置时才会使用默认的XHR或http适配器,
// 不过大部分时候,axios提供的默认适配器是能够满足我们的
var adapter = config . adapter || defaults . adapter ;
return adapter ( config ) . then ( /**/ ) ;
} ;
好了,看到這裡,我們是時候梳理一下:axios是如何用promise搭起基於xhr的非同步橋樑的?
axios是如何透過Promise進行非同步處理的?
import axios from 'axios'
axios . get ( /**/ )
. then ( data => {
// 此处可以拿到向服务端请求回的数据
} )
. catch ( error => {
// 此处可以拿到请求失败或取消或其他处理失败的错误对象
} )
先來一個圖簡單的了解下axios專案裡,http請求完成後到達使用者的順序流程:
透過axios為何會有多種使用方式我們知道, 使用者無論以何種方式呼叫axios,最終都是呼叫的Axios.prototype.request
方法, 這個方法最終回傳的是一個Promise物件。
Axios . prototype . request = function request ( config ) {
// ...
var chain = [ dispatchRequest , undefined ] ;
// 将config对象当作参数传给Primise.resolve方法
var promise = Promise . resolve ( config ) ;
while ( chain . length ) {
promise = promise . then ( chain . shift ( ) , chain . shift ( ) ) ;
}
return promise ;
} ;
Axios.prototype.request
方法會呼叫dispatchRequest
方法,而dispatchRequest
方法會呼叫xhrAdapter
方法, xhrAdapter
方法回傳的是還一個Promise對象
// /lib/adapters/xhr.js
function xhrAdapter ( config ) {
return new Promise ( function dispatchXhrRequest ( resolve , reject ) {
// ... 省略代码
} ) ;
} ;
xhrAdapter
內的XHR發送請求成功後會執行這個Promise物件的resolve
方法,並將請求的資料傳出去, 反之則執行reject
方法,並將錯誤訊息作為參數傳出去。
// /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 ;
} ;
驗證服務端的回傳結果是否經過驗證:
// /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
方法內,首先得到xhrAdapter
方法返回的Promise對象, 然後通過.then
方法,對xhrAdapter
返回的Promise對象的成功或失敗結果再次加工, 成功的話,則將處理後的response
返回, 失敗的話,則傳回一個狀態為rejected
的Promise對象,
return adapter ( config ) . then ( function onAdapterResolution ( response ) {
// ...
return response ;
} , function onAdapterRejection ( reason ) {
// ...
return Promise . reject ( reason ) ;
} ) ;
} ;
那麼至此,使用者呼叫axios()
方法時,就可以直接呼叫Promise的.then
或.catch
進行業務處理了。
回過頭來,我們在介紹dispatchRequest
一節時說到的資料轉換,而axios官方也將資料轉換專門作為一個亮點來介紹的,那麼資料轉換到底能在使用axios發揮什麼功效呢?
import axios from 'axios'
// 往现有的请求转换器里增加转换方法
axios . defaults . transformRequest . push ( ( data , headers ) => {
// ...处理data
return data ;
} ) ;
// 重写请求转换器
axios . defaults . transformRequest = [ ( data , headers ) => {
// ...处理data
return data ;
} ] ;
// 往现有的响应转换器里增加转换方法
axios . defaults . transformResponse . push ( ( data , headers ) => {
// ...处理data
return data ;
} ) ;
// 重写响应转换器
axios . defaults . transformResponse = [ ( data , headers ) => {
// ...处理data
return data ;
} ] ;
import axios from 'axios'
// 往已经存在的转换器里增加转换方法
axios . get ( url , {
// ...
transformRequest : [
... axios . defaults . transformRequest , // 去掉这行代码就等于重写请求转换器了
( data , headers ) => {
// ...处理data
return data ;
}
] ,
transformResponse : [
... axios . defaults . transformResponse , // 去掉这行代码就等于重写响应转换器了
( data , headers ) => {
// ...处理data
return data ;
}
] ,
} )
預設的defaults
配置項目裡已經自訂了一個請求轉換器和一個回應轉換器, 看下原始碼:
// /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專案裡,是在什麼地方使用了轉換器呢?
請求轉換器的使用地方是http請求前,使用請求轉換器對請求資料做處理, 然後傳給http請求適配器使用。
// /lib/core/dispatchRequest.js
function dispatchRequest ( config ) {
config . data = transformData (
config . data ,
config . headers ,
config . transformRequest
) ;
return adapter ( config ) . then ( /* ... */ ) ;
} ;
看下transformData
方法的程式碼, 主要遍歷轉換器數組,分別執行每一個轉換器,根據data和headers參數,傳回新的data。
// /lib/core/transformData.js
function transformData ( data , headers , fns ) {
utils . forEach ( fns , function transform ( fn ) {
data = fn ( data , headers ) ;
} ) ;
return data ;
} ;
回應轉換器的使用地方是在http請求完成後,依照http請求適配器的回傳值做資料轉換處理:
// /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 ) ;
} ) ;
攔截器同樣可以實現轉換請求和回應資料的需求,但根據作者的設計和綜合程式碼可以看出, 在請求時,攔截器主要負責修改config配置項,資料轉換器主要負責轉換請求體,例如轉換對象為字串在請求回應後,攔截器可以拿到response
,資料轉換器主要負責處理回應體,例如轉換字串為物件。
axios官方是將"自動轉換為JSON資料"作為一個獨立的亮點來介紹的,那麼資料轉換器是如何完成這個功能的呢? 其實非常簡單,我們一起看吧。
在預設情況下,axios將會自動的將傳入的data物件序列化為JSON字串,將回應資料中的JSON字串轉換為JavaScript對象
// 请求时,将data数据转换为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 ;
} ]
// 得到响应后,将请求到的数据转换为JSON对象
// /lib/defaults.js
transformResponse: [ function transformResponse ( data ) {
if ( typeof data === 'string' ) {
try {
data = JSON . parse ( data ) ;
} catch ( e ) { /* Ignore */ }
}
return data ;
} ]
至此,axios計畫的運作流程已經介紹完畢,是不是已經打通了任督二脈了呢接下來我們一起看下axios還帶給了我們哪些好用的技能點吧。
import axios from 'axios'
// 设置通用header
axios . defaults . headers . common [ 'X-Requested-With' ] = 'XMLHttpRequest' ; // xhr标识
// 设置某种请求的header
axios . defaults . headers . post [ 'Content-Type' ] = 'application/x-www-form-urlencoded;charset=utf-8' ;
// 设置某次请求的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 || { }
) ;
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內的this.promise = new Promise(resolve => resolvePromise = resolve)
, 得到實例屬性promise
,此時該promise
的狀態為pending
通過這個屬性,在/lib/adapters/xhr.js
文件中繼續為這個promise
實例添加.then
方法( xhr.js
檔案的159行config.cancelToken.promise.then(message => request.abort())
);
在CancelToken
外界,透過executor
參數拿到對cancel
方法的控制權, 這樣當執行cancel
方法時就可以改變實例的promise
屬性的狀態為rejected
, 從而執行request.abort()
方法達到取消請求的目的。
上面第二種寫法可以看作是第一種寫法的完善, 因為很多是時候我們取消請求的方法是用在本次請求方法外, 例如,發送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方法的參數不應該叫message麼,為什麼叫cancel?
/lib/adapters/xhr.js檔案中,onCanceled方法裡,reject裡應該將config訊息也傳出來
import axios from 'axios'
axios . defaults . withCredentials = true ;
我們在使用者配置的config是怎麼運作的一節已經介紹了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 ( ) . catch ( error => {
const { message } = error ;
if ( message . indexOf ( 'timeout' ) > - 1 ) {
// 超时处理
}
} )
自訂http狀態碼的成功、失敗範圍
import axios from 'axios'
axios . defaults . validateStatus = status => status >= 200 && status < 300 ;
在預設配置中,定義了預設的http狀態碼驗證規則, 所以自訂validateStatus
其實是對此處方法的重寫
// `/lib/defaults.js`
var defaults = {
// ...
validateStatus : function validateStatus ( status ) {
return status >= 200 && status < 300 ;
} ,
// ...
}
axios是何時開始驗證http狀態碼的?
// /lib/adapters/xhr.js
var request = new XMLHttpRequest ( ) ;
var loadEvent = 'onreadystatechange' ;
// /lib/adapters/xhr.js
// 每当 readyState 改变时,就会触发 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 ) {
// 如果我们往上捣一捣就会发现,config对象的validateStatus就是我们自定义的validateStatus方法或默认的validateStatus方法
var validateStatus = response . config . validateStatus ;
// validateStatus验证通过,就会触发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
) ) ;
}
} ;
axios這個專案裡,有很多對JS使用很巧妙的地方,例如對promise的串聯操作(當然你也可以說這塊是藉鑑很多非同步中間件的處理方式),讓我們可以很方便對請求前後的各種處理方法的流程進行控制;很多實用的小優化,例如請求前後的資料處理,省了程式設計師一遍一遍去寫JSON.xxx了;同時支援了瀏覽器和node兩種環境,對使用node的項目來說無疑是極好的。
總之,這個能夠在github斬獲42K+(截止2018.05.27)的star,實力絕不是概的,值得好好交心!