面試官不要再問我 axios 了?我能手寫簡易版的 axios

作者:FE_FLY

juejin.cn/post/6973257605367988260

axios 作爲我們工作中的常用的 ajax 請求庫,作爲前端工程師的我們當然是想一探究竟,axios 究竟是如何去架構整個框架,中間的攔截器、適配器、 取消請求這些都是我們經常使用的。

前言

由於 axios 源碼中有很多不是很重要的方法,而且很多方法爲了考慮兼容性,並沒有考慮到用 es6 的語法去寫。本篇主要是帶你去梳理 axios 的主要流程,並用 es6 重寫簡易版 axios

攔截器

一個 axios 實例上有兩個攔截器,一個是請求攔截器, 然後響應攔截器。我們下看下官網的用法:

添加攔截器

// 添加請求攔截器
axios.interceptors.request.use(function (config) {
    // 在發送請求之前做些什麼
    return config;
  }, function (error) {
    // 對請求錯誤做些什麼
    return Promise.reject(error);
  });

移除攔截器

const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);

其實源碼中就是,所有攔截器的執行 所以說肯定有一個 forEach 方法。

思路理清楚了,現在我們就開始去寫吧。代碼我就直接發出來,然後我在下面註解。

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
    }
  }

  // 執行棧中所有的hanlder
  forEach(fn) {
    this.handlers.forEach((item) => {
      // 這裏爲了過濾已經被取消的攔截器,因爲已經取消的攔截器被置null
      if (item) {
        fn(item)
      }
    })
  }
}

攔截器這個類我們已經初步實現了,現在我們去實現 axios 這個類,還是先看下官方文檔,先看用法,再去分析。

axios(config)

// 發送 POST 請求
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});
axios(url[, config])
// 發送 GET 請求(默認的方法) 
axios('/user/12345');

Axios 這個類最核心的方法其實還是 request 這個方法。我們先看下實現吧

class Axios {
  constructor(config) {
    this.defaults = config
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager(),
    }
  }
  // 發送一個請求
  request(config) {
    // 這裏呢其實就是去處理了 axios(url[,config])
    if (typeof config == 'string') {
      config = arguments[1] || {}
      config.url = arguments[0]
    } else {
      config = config || {}
    }

    // 默認get請求,並且都轉成小寫
    if (config.method) {
      config.method = config.method.toLowerCase()
    } else {
      config.method = 'get'
    }

    // dispatchRequest 就是發送ajax請求
    const chain = [dispatchRequest, undefined]
    //  發生請求之前加入攔截的 fulfille 和reject 函數
    this.interceptors.request.forEach((item) => {
      chain.unshift(item.fulfilled, item.rejected)
    })
    // 在請求之後增加 fulfilled 和reject 函數
    this.interceptors.response.forEach((item) => {
      chain.push(item.fulfilled, item.rejected)
    })

    // 利用promise的鏈式調用,將參數一層一層傳下去
    let promise = Promise.resolve(config)

    //然後我去遍歷 chain
    while (chain.length) {
      // 這裏不斷出棧 直到結束爲止
      promise = promise.then(chain.shift(), chain.shift())
    }
    return promise
  }
}

這裏其實就是體現了 axios 設計的巧妙, 維護一個棧結構 + promise 的鏈式調用 實現了 攔截器的功能, 可能有的小夥伴到這裏還是不是很能理解,我還是給大家畫一個草圖去模擬下這個過程。

假設我有 1 個請求攔截器 handler 和 1 個響應攔截器 handler

一開始我們棧中的數據就兩個

這個沒什麼問題,由於有攔截器的存在,如果存在的話,那麼我們就要往這個棧中加數據,請求攔截器顧名思義要在請求之前所以是 unshift。加完請求攔截器我們的棧變成了這樣

沒什麼問題,然後請求結束後,我們又想對請求之後的數據做處理,所以響應攔截的數據自然是 push 了。這時候棧結構變成了這樣:

然後遍歷整個棧結構,每次出棧都是一對出棧, 因爲 promise 的 then 就是 一個成功,一個失敗嘛。遍歷結束後,返回經過所有處理的 promise,然後你就可以拿到最終的值了。

adapter

Adapter: 英文解釋是適配器的意思。這裏我就不實現了,我帶大家看一下源碼。adapter 做了一件事非常簡單,就是根據不同的環境 使用不同的請求。如果用戶自定義了 adapter,就用 config.adapter。否則就是默認是 default.adpter.

 var adapter = config.adapter || defaults.adapter;

 return adapter(config).then() ...

繼續往下看 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;
}

其實就是做個選擇:如果是瀏覽器環境:就是用 xhr 否則就是 node 環境。判斷 process 是否存在。從寫代碼的角度來說,axios 源碼的這裏的設計可擴展性非常好。有點像設計模式中的適配器模式, 因爲瀏覽器端和 node 端 發送請求其實並不一樣, 但是我們不重要,我們不去管他的內部實現,用 promise 包一層做到對外統一。所以 我們用 axios 自定義 adapter 器的時候, 一定是返回一個 promise。ok 請求的方法我在下面模擬寫出。

cancleToken

我首先問大家一個問題,取消請求原生瀏覽器是怎麼做到的?有一個 abort 方法。可以取消請求。那麼 axios 源碼肯定也是運用了這一點去取消請求。現在瀏覽器其實也支持 fetch 請求, fetch 可以取消請求?很多同學說是不可以的,其實不是?fetch 結合 abortController 可以實現取消 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);

但是這是個實驗性功能,可惡的 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 參數是可選的)
source.cancel('Operation canceled by the user.');

還可以通過傳遞一個 executor 函數到 CancelToken 的構造函數來創建 cancel token:

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函數接收一個 cancel 函數作爲參數
    cancel = c;
  })
});

// cancel the request
cancel();

看了官方用法 和結合 axios 源碼:我給出以下實現:

export class cancelToken {
    constructor(exactor) {
        if (typeof executor !== 'function') {
        throw new TypeError('executor must be a function.')
        }
        // 這裏其實將promise的控制權 交給 cancel 函數
        // 同時做了防止多次重複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 其實本質上是一個語法糖 裏面做了封裝
    static source() {
        const cancel;
        const token = new cancelToken(function executor(c) {
            cancel = c;
        });
        return {
            token: token,
            cancel: cancel
        };
    }

}

截止到這裏大體 axios 大體功能已經給出。

接下來我就測試下我的手寫 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>

打開瀏覽器看一下結果:

成功了 ok, 然後我來測試一下攔截器的功能:代碼更新成下面這樣:

import Axios from './axios.js';
const config = { url:'http://101.132.113.6:3030/api/mock' }
const axios =  new Axios();
// 在axios 實例上掛載屬性
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('我是響應攔截器1',data )
    data += 1;
    return data;
},err)
axios.interceptors.response.use((data)=> {
    console.log('我是響應攔截器2',data )
    return  data
},err)
axios.request(config).then(res => {
    // console.log(res,'0000')
    // return res;
}).catch(err => {
    console.log(err)
})

ajax 請求的結果 我是 resolve(1) ,所以我們看下輸出路徑:

沒什麼問題, 響應後的數據我加了 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)

結果都是 OK 的, 至此 axios 簡單源碼終於搞定了。

反思

本篇文章只是把 axios 源碼的大體流程走了一遍, axios 源碼內部還是做了很多兼容比如:配置優先級:他有一個 mergeConfig 方法, 還有數據轉換器。不過這些不影響我們對 axios 源碼的整體梳理, 源碼中其實有一個 createInstance,至於爲什麼有?我覺得就是爲了可擴展性更好, 將來有啥新功能,直接在原有 axios 的實例的原型鏈上去增加,代碼可維護性強, axios.all spread 都是實例 new 出來再去掛的,不過都很簡單,沒啥的。有興趣大家自行閱讀。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/kALHt6mWupqY_n6t0Cmw-g