面試官不要再問我 axios 了?我能手寫簡易版的 axios
點(diǎn)擊上方?前端Q,關(guān)注公眾號
回復(fù)加群,加入前端Q技術(shù)交流群
作為我們工作中的常用的ajax請求庫,作為前端工程師的我們當(dāng)然是想一探究竟,axios究竟是如何去架構(gòu)整個框架,中間的攔截器、適配器、 取消請求這些都是我們經(jīng)常使用的。
前言
由于axios源碼中有很多不是很重要的方法,而且很多方法為了考慮兼容性,并沒有考慮到用es6 的語法去寫。本篇主要是帶你去梳理axios的主要流程,并用es6重寫簡易版axios
攔截器
適配器
取消請求
攔截器
一個axios實(shí)例上有兩個攔截器,一個是請求攔截器, 然后響應(yīng)攔截器。我們下看下官網(wǎng)的用法:添加攔截器
//?添加請求攔截器
axios.interceptors.request.use(function?(config)?{
????//?在發(fā)送請求之前做些什么
????return?config;
??},?function?(error)?{
????//?對請求錯誤做些什么
????return?Promise.reject(error);
??});
移除攔截器
const?myInterceptor?=?axios.interceptors.request.use(function?()?{/*...*/});
axios.interceptors.request.eject(myInterceptor);
其實(shí)源碼中就是,所有攔截器的執(zhí)行 所以說肯定有一個forEach方法。
思路理清楚了,現(xiàn)在我們就開始去寫吧。代碼我就直接發(fā)出來,然后我在下面注解。
export?class?InterceptorManager?{
??constructor()?{
????//?存放所有攔截器的棧
????this.handlers?=?[]
??}
??use(fulfilled,?rejected)?{
????this.handlers.push({
??????fulfilled,
??????rejected,
????})
????//返回id?便于取消
????return?this.handlers.length?-?1
??}
??//?取消一個攔截器
??eject(id)?{
????if?(this.handlers[id])?{
??????this.handlers[id]?=?null
????}
??}
??//?執(zhí)行棧中所有的hanlder
??forEach(fn)?{
????this.handlers.forEach((item)?=>?{
??????//?這里為了過濾已經(jīng)被取消的攔截器,因為已經(jīng)取消的攔截器被置null
??????if?(item)?{
????????fn(item)
??????}
????})
??}
}
攔截器這個類我們已經(jīng)初步實(shí)現(xiàn)了,現(xiàn)在我們?nèi)?shí)現(xiàn)axios 這個類,還是先看下官方文檔,先看用法,再去分析。
axios(config)
//?發(fā)送?POST?請求
axios({
??method:?'post',
??url:?'/user/12345',
??data:?{
????firstName:?'Fred',
????lastName:?'Flintstone'
??}
});
axios(url[, config])
//?發(fā)送?GET?請求(默認(rèn)的方法)?
axios('/user/12345');
Axios 這個類最核心的方法其實(shí)還是 request 這個方法。我們先看下實(shí)現(xiàn)吧
class?Axios?{
??constructor(config)?{
????this.defaults?=?config
????this.interceptors?=?{
??????request:?new?InterceptorManager(),
??????response:?new?InterceptorManager(),
????}
??}
??//?發(fā)送一個請求
??request(config)?{
????//?這里呢其實(shí)就是去處理了?axios(url[,config])
????if?(typeof?config?==?'string')?{
??????config?=?arguments[1]?||?{}
??????config.url?=?arguments[0]
????}?else?{
??????config?=?config?||?{}
????}
????//?默認(rèn)get請求,并且都轉(zhuǎn)成小寫
????if?(config.method)?{
??????config.method?=?config.method.toLowerCase()
????}?else?{
??????config.method?=?'get'
????}
????//?dispatchRequest?就是發(fā)送ajax請求
????const?chain?=?[dispatchRequest,?undefined]
????//??發(fā)生請求之前加入攔截的?fulfille?和reject?函數(shù)
????this.interceptors.request.forEach((item)?=>?{
??????chain.unshift(item.fulfilled,?item.rejected)
????})
????//?在請求之后增加?fulfilled?和reject?函數(shù)
????this.interceptors.response.forEach((item)?=>?{
??????chain.push(item.fulfilled,?item.rejected)
????})
????//?利用promise的鏈?zhǔn)秸{(diào)用,將參數(shù)一層一層傳下去
????let?promise?=?Promise.resolve(config)
????//然后我去遍歷?chain
????while?(chain.length)?{
??????//?這里不斷出棧?直到結(jié)束為止
??????promise?=?promise.then(chain.shift(),?chain.shift())
????}
????return?promise
??}
}
這里其實(shí)就是體現(xiàn)了axios設(shè)計的巧妙, 維護(hù)一個棧結(jié)構(gòu) + promise 的鏈?zhǔn)秸{(diào)用 實(shí)現(xiàn)了 攔截器的功能, 可能有的小伙伴到這里還是不是很能理解,我還是給大家畫一個草圖去模擬下這個過程。
假設(shè)我有1個請求攔截器handler和1個響應(yīng)攔截器handler
一開始我們棧中的數(shù)據(jù)就兩個
這個沒什么問題,由于有攔截器的存在,如果存在的話,那么我們就要往這個棧中加數(shù)據(jù),請求攔截器顧名思義要在請求之前所以是unshift。加完請求攔截器我們的棧變成了這樣
沒什么問題,然后請求結(jié)束后,我們又想對請求之后的數(shù)據(jù)做處理,所以響應(yīng)攔截的數(shù)據(jù)自然是push了。這時候棧結(jié)構(gòu)變成了這樣:
然后遍歷整個棧結(jié)構(gòu),每次出棧都是一對出棧, 因為promise 的then 就是 一個成功,一個失敗嘛。遍歷結(jié)束后,返回經(jīng)過所有處理的promise,然后你就可以拿到最終的值了。
adapter
Adapter: 英文解釋是適配器的意思。這里我就不實(shí)現(xiàn)了,我?guī)Т蠹铱匆幌略创a。adapter 做了一件事非常簡單,就是根據(jù)不同的環(huán)境 使用不同的請求。如果用戶自定義了adapter,就用config.adapter。否則就是默認(rèn)是default.adpter.
?var?adapter?=?config.adapter?||?defaults.adapter;
?return?adapter(config).then()?...
繼續(xù)往下看deafults.adapter做了什么事情:
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;
}
其實(shí)就是做個選擇:如果是瀏覽器環(huán)境:就是用xhr 否則就是node 環(huán)境。判斷process是否存在。從寫代碼的角度來說,axios源碼的這里的設(shè)計可擴(kuò)展性非常好。有點(diǎn)像設(shè)計模式中的適配器模式, 因為瀏覽器端和node 端 發(fā)送請求其實(shí)并不一樣, 但是我們不重要,我們不去管他的內(nèi)部實(shí)現(xiàn),用promise包一層做到對外統(tǒng)一。所以 我們用axios 自定義adapter 器的時候, 一定是返回一個promise。ok請求的方法我在下面模擬寫出。
cancleToken
我首先問大家一個問題,取消請求原生瀏覽器是怎么做到的?有一個abort 方法??梢匀∠埱?。那么axios源碼肯定也是運(yùn)用了這一點(diǎn)去取消請求?,F(xiàn)在瀏覽器其實(shí)也支持fetch請求, fetch可以取消請求?很多同學(xué)說是不可以的,其實(shí)不是?fetch 結(jié)合 abortController 可以實(shí)現(xiàn)取消fetch請求。我們看下例子:
const?controller?=?new?AbortController();
const?{?signal?}?=?controller;
fetch("http://localhost:8000",?{?signal?}).then(response?=>?{
????console.log(`Request?1?is?complete!`);
}).catch(e?=>?{
????console.warn(`Fetch?1?error:?${e.message}`);
});
//?Wait?2?seconds?to?abort?both?requests
setTimeout(()?=>?controller.abort(),?2000);
但是這是個實(shí)驗性功能,可惡的ie。所以我們這次還是用原生的瀏覽器xhr基于promise簡單的封裝一下。代碼如下:
export?function?dispatchRequest(config)?{
??return?new?Promise((resolve,?reject)?=>?{
????const?xhr?=?new?XMLHttpRequest()
????xhr.open(config.method,?config.url)
????xhr.onreadystatechange?=?function?()?{
??????if?(xhr.status?>=?200?&&?xhr.status?<=?300?&&?xhr.readyState?===?4)?{
????????resolve(xhr.responseText)
??????}?else?{
????????reject('失敗了')
??????}
????}
????if?(config.cancelToken)?{
??????//?Handle?cancellation
??????config.cancelToken.promise.then(function?onCanceled(cancel)?{
????????if?(!xhr)?{
??????????return
????????}
????????xhr.abort()
????????reject(cancel)
????????//?Clean?up?request
????????xhr?=?null
??????})
????}
????xhr.send()
??})
}
Axios 源碼里面做了很多處理, 這里我只做了get處理,我主要的目的就是為了axios是如何取消請求的。先看下官方用法:
主要是兩種用法:
使用?cancel token?取消請求
const?CancelToken?=?axios.CancelToken;
const?source?=?CancelToken.source();
axios.get('/user/12345',?{
??cancelToken:?source.token
}).catch(function(thrown)?{
??if?(axios.isCancel(thrown))?{
????console.log('Request?canceled',?thrown.message);
??}?else?{
?????//?處理錯誤
??}
});
axios.post('/user/12345',?{
??name:?'new?name'
},?{
??cancelToken:?source.token
})
//?取消請求(message?參數(shù)是可選的)
source.cancel('Operation?canceled?by?the?user.');
還可以通過傳遞一個 executor 函數(shù)到?CancelToken?的構(gòu)造函數(shù)來創(chuàng)建 cancel token:
const?CancelToken?=?axios.CancelToken;
let?cancel;
axios.get('/user/12345',?{
??cancelToken:?new?CancelToken(function?executor(c)?{
????//?executor?函數(shù)接收一個?cancel?函數(shù)作為參數(shù)
????cancel?=?c;
??})
});
//?cancel?the?request
cancel();
看了官方用法 和結(jié)合axios源碼:我給出以下實(shí)現(xiàn):
export?class?cancelToken?{
????constructor(exactor)?{
????????if?(typeof?executor?!==?'function')?{
????????throw?new?TypeError('executor?must?be?a?function.')
????????}
????????//?這里其實(shí)將promise的控制權(quán)?交給?cancel?函數(shù)
????????//?同時做了防止多次重復(fù)cancel?之前?Redux?還有React?源碼中也有類似的案列
????????const?resolvePromise;
????????this.promise?=??new?Promise(resolve?=>?{
????????????resolvePromise?=?resolve;
????????})
????????this.reason?=?undefined;
????????
????????const?cancel??=?(message)?=>?{
????????????if(this.reason)?{
????????????????return;
????????????}
????????????this.reason?=?'cancel'?+?message;
????????????resolvePromise(this.reason);
????????}
????????exactor(cancel)
????}
????throwIfRequested()?{
????????if(this.reason)?{
????????????throw?this.reason
????????}
????}
????
????//?source?其實(shí)本質(zhì)上是一個語法糖?里面做了封裝
????static?source()?{
????????const?cancel;
????????const?token?=?new?cancelToken(function?executor(c)?{
????????????cancel?=?c;
????????});
????????return?{
????????????token:?token,
????????????cancel:?cancel
????????};
????}
}
截止到這里大體axios 大體功能已經(jīng)給出。
接下來我就測試下我的手寫axios 有沒有什么問題?
?
打開瀏覽器看一下結(jié)果:
成功了ok, 然后我來測試一下攔截器的功能:代碼更新成下面這樣:
import?Axios?from?'./axios.js';
const?config?=?{?url:'http://101.132.113.6:3030/api/mock'?}
const?axios?=??new?Axios();
//?在axios?實(shí)例上掛載屬性
const?err?=?()?=>?{}
axios.interceptors.request.use((config)=>?{
????console.log('我是請求攔截器1')
????config.id?=?1;
????return??config
},err?)
axios.interceptors.request.use((config)=>?{
????config.id?=?2
????console.log('我是請求攔截器2')
????return?config
},err)
axios.interceptors.response.use((data)=>?{
????console.log('我是響應(yīng)攔截器1',data?)
????data?+=?1;
????return?data;
},err)
axios.interceptors.response.use((data)=>?{
????console.log('我是響應(yīng)攔截器2',data?)
????return??data
},err)
axios.request(config).then(res?=>?{
????//?console.log(res,'0000')
????//?return?res;
}).catch(err?=>?{
????console.log(err)
})??console.log(err)})
ajax 請求的結(jié)果 我是resolve(1) ,所以我們看下輸出路徑:
沒什么問題, 響應(yīng)后的數(shù)據(jù)我加了1。
接下來我來是取消請求的兩種方式
//?第一種方式
let??cancelFun?=?undefined;
const?cancelInstance?=?new?cancelToken((c)=>{
????cancelFun?=?c;
});
config.cancelToken?=?cancelInstance;
//?50?ms?就取消請求
setTimeout(()=>{
????cancelFun('取消成功')
},50)
第二種方式:
const?{?token,?cancel?}??=?cancelToken.source();
config.cancelToken?=?token;
setTimeout(()=>{
????cancel()
},50)

結(jié)果都是OK的,至此axios簡單源碼終于搞定了。
反思
本篇文章只是把a(bǔ)xios源碼的大體流程走了一遍, axios源碼內(nèi)部還是做了很多兼容比如:配置優(yōu)先級:他有一個mergeConfig 方法, 還有數(shù)據(jù)轉(zhuǎn)換器。不過這些不影響我們對axios源碼的整體梳理, 源碼中其實(shí)有一個createInstance,至于為什么有?我覺得就是為了可擴(kuò)展性更好, 將來有啥新功能,直接在原有axios的實(shí)例的原型鏈上去增加,代碼可維護(hù)性強(qiáng), axios.all spread 都是實(shí)例new出來再去掛的,不過都很簡單,沒啥的。有興趣大家自行閱讀。
作者:FE_FLY
juejin.cn/post/6973257605367988260

往期推薦



最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個專業(yè)的技術(shù)人...


