面試官不要再問我 axios 了?我能手寫簡(jiǎn)易版的 axios
作為我們工作中的常用的ajax請(qǐng)求庫,作為前端工程師的我們當(dāng)然是想一探究竟,axios究竟是如何去架構(gòu)整個(gè)框架,中間的攔截器、適配器、 取消請(qǐng)求這些都是我們經(jīng)常使用的。
前言
由于axios源碼中有很多不是很重要的方法,而且很多方法為了考慮兼容性,并沒有考慮到用es6 的語法去寫。本篇主要是帶你去梳理axios的主要流程,并用es6重寫簡(jiǎn)易版axios
- 攔截器
 - 適配器
 - 取消請(qǐng)求
 
攔截器
一個(gè)axios實(shí)例上有兩個(gè)攔截器,一個(gè)是請(qǐng)求攔截器, 然后響應(yīng)攔截器。我們下看下官網(wǎng)的用法:添加攔截器
- // 添加請(qǐng)求攔截器
 - axios.interceptors.request.use(function (config) {
 - // 在發(fā)送請(qǐng)求之前做些什么
 - return config;
 - }, function (error) {
 - // 對(duì)請(qǐng)求錯(cuò)誤做些什么
 - return Promise.reject(error);
 - });
 
移除攔截器
- const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
 - axios.interceptors.request.eject(myInterceptor);
 
其實(shí)源碼中就是,所有攔截器的執(zhí)行 所以說肯定有一個(gè)forEach方法。
思路理清楚了,現(xiàn)在我們就開始去寫吧。代碼我就直接發(fā)出來,然后我在下面注解。
- export class InterceptorManager {
 - constructor() {
 - // 存放所有攔截器的棧
 - this.handlers = []
 - }
 - use(fulfilled, rejected) {
 - this.handlers.push({
 - fulfilled,
 - rejected,
 - })
 - //返回id 便于取消
 - return this.handlers.length - 1
 - }
 - // 取消一個(gè)攔截器
 - eject(id) {
 - if (this.handlers[id]) {
 - this.handlers[id] = null
 - }
 - }
 - // 執(zhí)行棧中所有的hanlder
 - forEach(fn) {
 - this.handlers.forEach((item) => {
 - // 這里為了過濾已經(jīng)被取消的攔截器,因?yàn)橐呀?jīng)取消的攔截器被置null
 - if (item) {
 - fn(item)
 - }
 - })
 - }
 - }
 
攔截器這個(gè)類我們已經(jīng)初步實(shí)現(xiàn)了,現(xiàn)在我們?nèi)?shí)現(xiàn)axios 這個(gè)類,還是先看下官方文檔,先看用法,再去分析。
axios(config)
- // 發(fā)送 POST 請(qǐng)求
 - axios({
 - method: 'post',
 - url: '/user/12345',
 - data: {
 - firstName: 'Fred',
 - lastName: 'Flintstone'
 - }
 - });
 
axios(url[, config])
- // 發(fā)送 GET 請(qǐng)求(默認(rèn)的方法)
 - axios('/user/12345');
 
Axios 這個(gè)類最核心的方法其實(shí)還是 request 這個(gè)方法。我們先看下實(shí)現(xiàn)吧!
- class Axios {
 - constructor(config) {
 - this.defaults = config
 - this.interceptors = {
 - request: new InterceptorManager(),
 - response: new InterceptorManager(),
 - }
 - }
 - // 發(fā)送一個(gè)請(qǐng)求
 - request(config) {
 - // 這里呢其實(shí)就是去處理了 axios(url[,config])
 - if (typeof config == 'string') {
 - config = arguments[1] || {}
 - config.url = arguments[0]
 - } else {
 - configconfig = config || {}
 - }
 - // 默認(rèn)get請(qǐng)求,并且都轉(zhuǎn)成小寫
 - if (config.method) {
 - configconfig.method = config.method.toLowerCase()
 - } else {
 - config.method = 'get'
 - }
 - // dispatchRequest 就是發(fā)送ajax請(qǐng)求
 - const chain = [dispatchRequest, undefined]
 - // 發(fā)生請(qǐng)求之前加入攔截的 fulfille 和reject 函數(shù)
 - this.interceptors.request.forEach((item) => {
 - chain.unshift(item.fulfilled, item.rejected)
 - })
 - // 在請(qǐng)求之后增加 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é)束為止
 - promisepromise = promise.then(chain.shift(), chain.shift())
 - }
 - return promise
 - }
 - }
 
這里其實(shí)就是體現(xiàn)了axios設(shè)計(jì)的巧妙, 維護(hù)一個(gè)棧結(jié)構(gòu) + promise 的鏈?zhǔn)秸{(diào)用 實(shí)現(xiàn)了 攔截器的功能, 可能有的小伙伴到這里還是不是很能理解,我還是給大家畫一個(gè)草圖去模擬下這個(gè)過程。
假設(shè)我有1個(gè)請(qǐng)求攔截器handler和1個(gè)響應(yīng)攔截器handler
一開始我們棧中的數(shù)據(jù)就兩個(gè)
這個(gè)沒什么問題,由于有攔截器的存在,如果存在的話,那么我們就要往這個(gè)棧中加數(shù)據(jù),請(qǐng)求攔截器顧名思義要在請(qǐng)求之前所以是unshift。加完請(qǐng)求攔截器我們的棧變成了這樣。
沒什么問題,然后請(qǐng)求結(jié)束后,我們又想對(duì)請(qǐng)求之后的數(shù)據(jù)做處理,所以響應(yīng)攔截的數(shù)據(jù)自然是push了。這時(shí)候棧結(jié)構(gòu)變成了這樣:
然后遍歷整個(gè)棧結(jié)構(gòu),每次出棧都是一對(duì)出棧, 因?yàn)閜romise 的then 就是 一個(gè)成功,一個(gè)失敗嘛。遍歷結(jié)束后,返回經(jīng)過所有處理的promise,然后你就可以拿到最終的值了。
adapter
Adapter: 英文解釋是適配器的意思。這里我就不實(shí)現(xiàn)了,我?guī)Т蠹铱匆幌略创a。adapter 做了一件事非常簡(jiǎn)單,就是根據(jù)不同的環(huán)境 使用不同的請(qǐng)求。如果用戶自定義了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í)就是做個(gè)選擇:如果是瀏覽器環(huán)境:就是用xhr 否則就是node 環(huán)境。判斷process是否存在。從寫代碼的角度來說,axios源碼的這里的設(shè)計(jì)可擴(kuò)展性非常好。有點(diǎn)像設(shè)計(jì)模式中的適配器模式, 因?yàn)闉g覽器端和node 端 發(fā)送請(qǐng)求其實(shí)并不一樣, 但是我們不重要,我們不去管他的內(nèi)部實(shí)現(xiàn),用promise包一層做到對(duì)外統(tǒng)一。所以 我們用axios 自定義adapter 器的時(shí)候, 一定是返回一個(gè)promise。ok請(qǐng)求的方法我在下面模擬寫出。
cancleToken
我首先問大家一個(gè)問題,取消請(qǐng)求原生瀏覽器是怎么做到的?有一個(gè)abort 方法??梢匀∠?qǐng)求。那么axios源碼肯定也是運(yùn)用了這一點(diǎn)去取消請(qǐng)求?,F(xiàn)在瀏覽器其實(shí)也支持fetch請(qǐng)求, fetch可以取消請(qǐng)求?很多同學(xué)說是不可以的,其實(shí)不是?fetch 結(jié)合 abortController 可以實(shí)現(xiàn)取消fetch請(qǐng)求。我們看下例子:
- 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);
 
但是這是個(gè)實(shí)驗(yàn)性功能,可惡的ie。所以我們這次還是用原生的瀏覽器xhr基于promise簡(jiǎn)單的封裝一下。代碼如下:
- 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是如何取消請(qǐng)求的。先看下官方用法:
主要是兩種用法:
使用 cancel token 取消請(qǐng)求
- 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 {
 - // 處理錯(cuò)誤
 - }
 - });
 - axios.post('/user/12345', {
 - name: 'new name'
 - }, {
 - cancelToken: source.token
 - })
 - // 取消請(qǐng)求(message 參數(shù)是可選的)
 - source.cancel('Operation canceled by the user.');
 
還可以通過傳遞一個(gè) 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ù)接收一個(gè) cancel 函數(shù)作為參數(shù)
 - ccancel = 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ù)
 - // 同時(shí)做了防止多次重復(fù)cancel 之前 Redux 還有React 源碼中也有類似的案列
 - const resolvePromise;
 - this.promise = new Promise(resolve => {
 - resolveresolvePromise = 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ì)上是一個(gè)語法糖 里面做了封裝
 - static source() {
 - const cancel;
 - const token = new cancelToken(function executor(c) {
 - ccancel = c;
 - });
 - return {
 - token: token,
 - cancel: cancel
 - };
 - }
 - }
 
截止到這里大體axios 大體功能已經(jīng)給出。
接下來我就測(cè)試下我的手寫axios,有沒有什么問題?
- <script type="module" >
 - import Axios from './axios.js';
 - const config = { url:'http://101.132.113.6:3030/api/mock' }
 - const axios = new Axios();
 - axios.request(config).then(res => {
 - console.log(res,'0000')
 - }).catch(err => {
 - console.log(err)
 - })
 - /script>
 
打開瀏覽器看一下結(jié)果:
成功了ok, 然后我來測(cè)試一下攔截器的功能,代碼更新成下面這樣:
- 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('我是請(qǐng)求攔截器1')
 - config.id = 1;
 - return config
 - },err )
 - axios.interceptors.request.use((config)=> {
 - config.id = 2
 - console.log('我是請(qǐng)求攔截器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 請(qǐng)求的結(jié)果 我是resolve(1) ,所以我們看下輸出路徑:
沒什么問題, 響應(yīng)后的數(shù)據(jù)我加了1。
接下來我來是取消請(qǐng)求的兩種方式 :
- // 第一種方式
 - let cancelFun = undefined;
 - const cancelInstance = new cancelToken((c)=>{
 - ccancelFun = c;
 - });
 - config.cancelToken = cancelInstance;
 - // 50 ms 就取消請(qǐng)求
 - setTimeout(()=>{
 - cancelFun('取消成功')
 - },50)
 - 第二種方式:
 - const { token, cancel } = cancelToken.source();
 - config.cancelToken = token;
 - setTimeout(()=>{
 - cancel()
 - },50)
 
結(jié)果都是OK的,至此axios簡(jiǎn)單源碼終于搞定了。
反思
本篇文章只是把a(bǔ)xios源碼的大體流程走了一遍, axios源碼內(nèi)部還是做了很多兼容比如:配置優(yōu)先級(jí):他有一個(gè)mergeConfig 方法, 還有數(shù)據(jù)轉(zhuǎn)換器。不過這些不影響我們對(duì)axios源碼的整體梳理, 源碼中其實(shí)有一個(gè)createInstance,至于為什么有?我覺得就是為了可擴(kuò)展性更好, 將來有啥新功能,直接在原有axios的實(shí)例的原型鏈上去增加,代碼可維護(hù)性強(qiáng), axios.all spread 都是實(shí)例new出來再去掛的,不過都很簡(jiǎn)單,沒啥的。有興趣大家自行閱讀。





















 
 
 







 
 
 
 