axios核心模塊原理剖析

axios是一個基于promise調(diào)用邏輯的http請求庫,是一個優(yōu)秀的開源項目。了解其實現(xiàn)邏輯有助于深化我們對接口往返的理解,提高Promise的應用能力等样勃。本文會挑幾個axios重點且常用的功能模塊進行原理剖析并使用ts實現(xiàn)嘱函,完整代碼在這里蓉驹。本文對每個模塊將按照功能描述,原理剖析搂橙,ts實現(xiàn)的順序進行歉提。

請求參數(shù)與url的處理

axios會對我們傳入的請求參數(shù)做統(tǒng)一處理,當發(fā)送的是get請求時會根據(jù)不同數(shù)據(jù)類型做不同的拼接處理,總結(jié)有以下幾點

  • 基本類型 按基礎規(guī)則拼接

    axios({
      method: 'get',
      url: '/test/get',
      params: {
        a: 1,
        b: 2,
      },
    });
    // http://localhost:8080/simple/get?a=1&b=2
    
  • 數(shù)組 屬性名拼接數(shù)組符號并依次拼接各元素

    axios({
      method: 'get',
      url: '/test/get',
      params: {
        arr: [1, 2],
      },
    });
    // http://localhost:3000/test/get?arr[]=1&arr[]=2
    
  • 對象 encode后拼接

    axios({
      method: 'get',
      url: '/test/get',
      params: {
        obj: {
          name: 'foo',
        },
      },
    });
    // http://localhost:3000/test/get?obj=%7B%22name%22:%22foo%22%7D
    
  • Date toString后拼接

    axios({
      method: 'get',
      url: '/test/get',
      params: {
        date: new Date(),
      },
    });
    // http://localhost:3000/test/get?date=2021-11-02T07:58:49.323Z
    
  • 特殊字符不被encode @苔巨、:版扩、$,侄泽、[礁芦、],允許他們存在url中悼尾。

  • 忽略空值 類型為null或者undefined的值不會被添加到url中柿扣。

  • 忽略哈希標記#.

  • 保存已存在的url參數(shù),傳入的參數(shù)會繼續(xù)拼接在已存在的參數(shù)后闺魏。

接下來具體實現(xiàn)未状,命名為buildUrl

//  聲明兩個工具函數(shù) 用于判斷Date類型和object類型
function isDate(val: any) {
  return toString.call(val) === '[object Date]'
}

function isPlainObject(val: any) {
  return toString.call(val) === '[object Object]'
}
function buildURL(url: string, params?: any): string {
  if (!params) {
    return url;
  }
  // 要拼接的參數(shù)數(shù)組
  const parts: string[] = [];

  Object.keys(params).forEach(key => {
    let val = params[key];
    if (val === null || typeof val === 'undefined') {
      return;
    }
    let values = [];
    if (Array.isArray(val)) {
      values = val;
      // 屬性名加'[]'標記
      key += '[]';
    } else {
      values = [val];
    }
    values.forEach(val => {
      if (isDate(val)) {
        val = val.toISOString();
      } else if (isPlainObject(val)) {
        val = JSON.stringify(val);
      }
      parts.push(`${encode(key)}=${encode(val)}`);
    });
  });

  let serializedParams = parts.join('&');

  if (serializedParams) {
    // 忽略哈希標記
    const markIndex = url.indexOf('#');
    if (markIndex !== -1) {
      url = url.slice(0, markIndex);
    }
    // 保留原有的參數(shù)
    url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams;
  }

  return url;
}

默認配置與配置合并策略

axios有默認配置(axios.defaults),我們可以修改默認配置且其能與我們傳入axios的配置合并舷胜,下面分析其合并策略娩践。

由于axios的配置的是個復雜對象,因此默認配置和自定義配置的合并也不是簡單的對象合并烹骨。合并的總體原則是翻伺,對于基本類型屬性(method,timeout沮焕,withCredentials吨岭。。峦树。)的合并優(yōu)先使用自定義屬性辣辫,沒有自定義屬性則用默認屬性,無默認屬性則為null(有些屬性沒有默認值如url魁巩、params急灭、data,因為這些屬性與當次請求強相關谷遂。設置默認值無意義)葬馋,對于object類型(如headers)屬性則需要深度合并,即要進行遞歸判斷肾扰。

因此畴嘶,我們要為每一個屬性定制其合并策略,接下來按以下步驟具體實現(xiàn):

  • 聲明一個對象strats用來存儲各個屬性的合并策略
  const strats = Object.create(null);
  • 分別定義默認策略和深度合并策略

    默認策略

    優(yōu)先用自定義屬性 沒有則用默認屬性 否則返會null

    const defMerge = (target: any, source: any) => {
      return typeof source !== 'undefined' ? source : typeof target !== 'undefined' ? target : null;
    };
    

    深度合并策略
    基本類型直接合并集晚,對象類型值則判斷原屬性是否是對象類型窗悯,如果是,則遞歸合并偷拔。不是蒋院,則用一個空對象與之合并亏钩。

    const deepMerge = (...objs: any[]): any => {
      const result = Object.create(null)
      objs.forEach(obj => {
        if (obj) {
          Object.keys(obj).forEach(key => {
            const val = obj[key]
            if (isPlainObject(val)) {
              if (isPlainObject(result[key])) {
                result[key] = deepMerge(result[key], val)
              } else {
                result[key] = deepMerge({}, val)
              }
            } else {
              result[key] = val
            }
          })
        }
      })
      return result
    }
    
  • 為每個屬性指定合并策略

由于只有少數(shù)屬性('headers', 'auth')需要深度合并,因此我們只需將需要深度合并的屬性及其合并策略注冊到strats中悦污,在執(zhí)行合并時判斷當前屬性是否在strats中存在即可铸屉,存在則執(zhí)行其專屬的合并策略,不存在則執(zhí)行默認合并策略切端。

// 需要深度合并的屬性
const stratKeysDeepMerge = ['headers', 'auth'];
// 注冊合并策略
stratKeysDeepMerge.forEach(key => {
  strats[key] = deepMergeStrat;
});
  • 執(zhí)行合并

我們約定config1代表默認配置 config2代表自定義配置彻坛。
首先聲明一個空對象存儲合并結(jié)果,遍歷自定義配置并執(zhí)行對應合并策略踏枣,會優(yōu)先使用strats中的合并策略昌屉,沒有則用默認合并策略。之后遍歷默認配置茵瀑,只有合并結(jié)果中不存在該屬性時再執(zhí)行其合并策略间驮。

function mergeConfig(config1: AxiosRequestConfig, config2?: AxiosRequestConfig): AxiosRequestConfig {
  if (!config2) {
    config2 = {};
  }
  const config = Object.create(null);
  // 優(yōu)先合并自定義配置
  for (let key in config2) {
    mergeField(key);
  }
  // 合并默認配置 只有在沒有自定義配置時才使用默認配置
  for (let key in config1) {
    if (!config2[key]) {
      mergeField(key);
    }
  }
  function mergeField(key: string): void {
    // 優(yōu)先自定義配置合并策略 沒有則用默認策略
    const strat = strats[key] || defMerge;
    config[key] = strat(config1[key], config2![key]);
  }
  return config;
}

攔截器

axios的攔截器幾乎是項目中必用的一項配置,它可以在請求前/響應后對請求體/響應體做一些處理马昨,先來回顧一下基本用法竞帽。

  • 使用use方法注冊攔截器,使用類似promise.then鸿捧,接收兩個參數(shù)屹篓,第一個用來添加我們期望攔截器處理的邏輯,第二個參數(shù)用來處理錯誤匙奴。

  • 使用eject方法刪除某個攔截器堆巧。

  • 攔截器可以添加多個,執(zhí)行順序是:請求攔截器先添加的后執(zhí)行泼菌,響應攔截器先添加的先執(zhí)行谍肤。

我們知道axios是基于promise實現(xiàn)的,結(jié)合攔截器的執(zhí)行過程其實不難想到可以用promise的鏈式調(diào)用實現(xiàn)哗伯,先來回顧一下鏈式調(diào)用荒揣。簡單來講Promise.then方法會返回一個Promise,可以繼續(xù)調(diào)用then方法焊刹, 前一個then的回調(diào)返回的數(shù)據(jù)會作為參數(shù)傳入下一個then的回調(diào)乳附。因此我們可以將請求/響應攔截器與請求發(fā)送的調(diào)用使用promise.then方法串聯(lián)起來。

基于上述伴澄,首先實現(xiàn)一個攔截器管理類

interface ResolvedFn<T = any> {
  (val: T): T | Promise<T>
}

interface RejectedFn {
  (error: any): any
}
interface Interceptor<T> {
  resolved: ResolvedFn<T>
  rejected?: RejectedFn
}

class InterceptorManager<T> {
  private interceptors: Array<Interceptor<T> | null>

  constructor() {
    // 用于存放攔截器
    this.interceptors = []
  }
  // 注冊攔截器,返回其索引可用于刪除
  use(resolved: ResolvedFn<T>, rejected?: RejectedFn): number {
    this.interceptors.push({
      resolved,
      rejected
    })
    return this.interceptors.length - 1
  }
  // 遍歷所有攔截器阱缓,將每個攔截器作為傳入函數(shù)的參數(shù)并執(zhí)行
  // 將來用于將攔截器推入promise的調(diào)用鏈中
  forEach(fn: (interceptor: Interceptor<T>) => void): void {
    this.interceptors.forEach(interceptor => {
      if (interceptor !== null) {
        fn(interceptor)
      }
    })
  }
  // 刪除攔截器
  eject(id: number): void {
    if (this.interceptors[id]) {
      this.interceptors[id] = null
    }
  }
}

接下來為Axios類添加interceptors屬性非凌,他有兩個值分別是請求和響應攔截器

export default class Axios {
  constructor() {
    this.interceptors = {
      request: new InterceptorManager<AxiosRequestConfig>(),
      response: new InterceptorManager<AxiosResponse>()
    }
  }
  ......
}

攔截器攔截的是請求,因此最后需要對發(fā)送請求的方法進行處理荆针。具體如下:

  • 聲明一個chain數(shù)組用于存放promise調(diào)用鏈敞嗡,并首先將請求發(fā)送方法放入颁糟。

  • 分別調(diào)用請求/響應攔截器的foreach方法,將請求攔截器倒序插入數(shù)組前部喉悴,將響應攔截器順序插入數(shù)組尾部棱貌。

  • 聲明一個resolved狀態(tài)的Promise用于啟動鏈式調(diào)用,最后循環(huán)chain數(shù)組箕肃,取出每個攔截器婚脱,使用then方法調(diào)用即可。

request(url: any, config?: any): AxiosPromise {
    // 其他邏輯
    const chain: PromiseChain[] = [{
      resolved: dispatchRequest,
      rejected: undefined
    }]
    // 將請求攔截器倒序插入數(shù)組前部
    this.interceptors.request.forEach(interceptor => {
      chain.unshift(interceptor)
    })
    // 將響應攔截器插入數(shù)組尾部
    this.interceptors.response.forEach(interceptor => {
      chain.push(interceptor)
    })
    // 初始化一個reslove狀態(tài)的promise
    let promise = Promise.resolve(config)
    while (chain.length) {
      // 鏈式調(diào)用
      const { resolved, rejected } = chain.shift()!
      promise = promise.then(resolved, rejected)
    }
    return promise
  }

請求取消

請求取消也是axios在項目中的一個常用功能勺像,一個典型場景就是當接口響應慢且會多次觸發(fā)時(如點擊按鈕提交障贸,搜索輸入框等),由于每次響應時間不定吟宦,因此可能出現(xiàn)后發(fā)出的請求比先發(fā)出的請求的響應速度快的情況篮洁,此時就可以使用請求取消,即判斷前一次請求結(jié)果未返回時殃姓,取消當次請求袁波。

回顧下如何使用請求取消,有兩種使用方式:

  • 方式一 使用axios.CancelToken的source方法蜗侈,調(diào)用后返回token和cancel兩個屬性篷牌,token用于請求時傳給配置對象中的cancelToken屬性,在請求發(fā)出后宛篇,可以使用cancel方法取消請求娃磺。
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/test/get', {
  cancelToken: source.token
})
source.cancel('Operation canceled by the user.');
  • 方式二 直接new一個CancelToken的實例賦給配置對象的cancelToken屬性并傳入一個函數(shù),該函數(shù)接收一個處理取消邏輯的函數(shù)參數(shù)叫倍,在函數(shù)體內(nèi)將其賦給在外手動聲明的cancel變量偷卧,通過執(zhí)行cancel函數(shù)取消請求。
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/test/get', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c;
  })
});
cancel();

接下來分析其實現(xiàn)思路吆倦。
我們知道http請求取消是通過調(diào)用xhr對象的abort方法實現(xiàn)的√睿現(xiàn)在的問題是當想要取消請求時我們往往無法直接訪問xhr對象。要想操作xhr對象我們只有在請求發(fā)送的過程中蚕泽,也即在new XMLHttpRequest()后才能訪問xhr對象晌梨。而要實現(xiàn)請求取消,我們只能事先將請求取消的邏輯寫好但不執(zhí)行须妻,等將來需要取消時再執(zhí)行這段邏輯仔蝌。說到這里就又想到了promise,我們知道promise.then方法中指定的回調(diào)是會等promise的狀態(tài)改變后再執(zhí)行的荒吏,因此實現(xiàn)思路也就有啦敛惊。我們可以聲明一個pending狀態(tài)的promise,在then方法的成功回調(diào)中添加請求取消的邏輯绰更,即xhr.abort瞧挤。等需要取消時锡宋,將該promise的狀態(tài)改變即可。換句話說我們把請求取消的邏輯"寄托"在了一個promise上特恬。那么這個promise從何而來执俩?觀察攔截器的兩種使用方式,其實配置對象中的cancelToken屬性就是那個promise癌刽。

既然有兩種使用方式役首,相應的也就有兩種方式可以得到這個promise,分別是直接實例化axios.CancelToken類以及調(diào)用CancelToken類的source方法得到妒穴。axios得到這個promise后就可以將取消邏輯‘寄托’在其then方法上宋税,接下來看一下實現(xiàn):

export default (config: AxiosRequestConfig): AxiosPromise => {
  return new Promise((resolve,reject)=>{
    const {
   ......
   cancelToken,// 傳入的promise
    } = config
  const request = new XMLHttpRequest()
  // ...其他邏輯
  if (cancelToken) {
        cancelToken.promise.then(reason => {
          // 調(diào)用取消方法
          request.abort()
          // 改變Axiospromise狀態(tài)為失敗
          reject(reason)
        })
      }
  })
}

接下里分析如何改變該promise的狀態(tài)讼油。觀察第二種使用方式杰赛,executor函數(shù)接收的參數(shù)就是用來處理取消邏輯的函數(shù),即改變promise的狀態(tài)矮台。因此在實現(xiàn)CancelToken類時首先需要聲明一個pending狀態(tài)的promise乏屯,即暫不執(zhí)行resolve函數(shù),而是將其暫存起來(賦給外部變量)瘦赫。之后執(zhí)行executor函數(shù)并傳入一個函數(shù)辰晕,函數(shù)內(nèi)部執(zhí)行暫存的resolve函數(shù)即可改變上述promise的狀態(tài),接下來具體實現(xiàn)

interface ResolvePromise {
  (reason?: string): void
}
class CancelToken {
  promise: Promise<string>
  reason?: string
  constructor(executor) {
    let resolvePromise: ResolvePromise
    // 聲明一個pending狀態(tài)的promise
    this.promise = new Promise<string>(resolve => {
      resolvePromise = resolve
    })
    // executor函數(shù)傳入的參數(shù)會被賦值給外部變量cancel用于取消請求
    executor(message => {
      if (this.reason) {
        return
      }
      this.reason = message
      resolvePromise(this.reason)
    })
  }
}

至此第二種使用方式已經(jīng)實現(xiàn)确虱,再來看第一種含友。容易看出source函數(shù)返回的token就是一個pending狀態(tài)的promise,返回的cancel函數(shù)可直接調(diào)用校辩,無需我們手動聲明窘问,對比一下不難發(fā)現(xiàn)其實這就是相對于第二種方式做了一層封裝,將CancelToken的實例化和取消函數(shù)的處理邏輯放在source方法內(nèi)部實現(xiàn)宜咒,接下來實現(xiàn)source方法惠赫。

class CancelToken {
 ...
  //靜態(tài)方法source 實例化CancelToken 聲明cancel變量 接收取消函數(shù)并返回
  static source(): CancelTokenSource {
    let cancel
    const token = new CancelToken(c => {
      cancel = c
    })
    return {
      cancel,
      token
    }
  }
  constructor(executor) {...} 
}

至此請求取消的主體邏輯已經(jīng)實現(xiàn)。有一點需要特別區(qū)分故黑,即axios.CancelToken和config的cancelToken這兩個屬性儿咱,事實上他們是類與實例的關系。

源碼地址场晶,如有錯誤懇請指正混埠。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市诗轻,隨后出現(xiàn)的幾起案子钳宪,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件使套,死亡現(xiàn)場離奇詭異,居然都是意外死亡鞠柄,警方通過查閱死者的電腦和手機侦高,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來厌杜,“玉大人奉呛,你說我怎么就攤上這事『痪。” “怎么了瞧壮?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長匙握。 經(jīng)常有香客問我咆槽,道長,這世上最難降的妖魔是什么圈纺? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任秦忿,我火速辦了婚禮,結(jié)果婚禮上蛾娶,老公的妹妹穿的比我還像新娘灯谣。我一直安慰自己,他們只是感情好蛔琅,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布胎许。 她就那樣靜靜地躺著,像睡著了一般罗售。 火紅的嫁衣襯著肌膚如雪辜窑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天莽囤,我揣著相機與錄音谬擦,去河邊找鬼。 笑死朽缎,一個胖子當著我的面吹牛惨远,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播话肖,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼北秽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了最筒?” 一聲冷哼從身側(cè)響起贺氓,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎床蜘,沒想到半個月后辙培,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蔑水,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年扬蕊,在試婚紗的時候發(fā)現(xiàn)自己被綠了搀别。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡尾抑,死狀恐怖歇父,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情再愈,我是刑警寧澤榜苫,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站翎冲,受9級特大地震影響垂睬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜府适,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一羔飞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧檐春,春花似錦逻淌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至俐巴,卻和暖如春骨望,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背欣舵。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工擎鸠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缘圈。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓劣光,卻偏偏與公主長得像,于是被迫代替她去往敵國和親糟把。 傳聞我的和親對象是個殘疾皇子绢涡,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容