面試官不要再問我 axios 了?我能手寫簡易版的 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 {
- configconfig = config || {}
- }
- // 默認get請求,并且都轉成小寫
- if (config.method) {
- configconfig.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) {
- // 這里不斷出棧 直到結束為止
- promisepromise = 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 函數作為參數
- ccancel = 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 => {
- 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 其實本質上是一個語法糖 里面做了封裝
- static source() {
- const cancel;
- const token = new cancelToken(function executor(c) {
- ccancel = 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)
- }) console.log(err)})
ajax 請求的結果 我是resolve(1) ,所以我們看下輸出路徑:
沒什么問題, 響應后的數據我加了1。
接下來我來是取消請求的兩種方式 :
- // 第一種方式
- let cancelFun = undefined;
- const cancelInstance = new cancelToken((c)=>{
- ccancelFun = 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出來再去掛的,不過都很簡單,沒啥的。有興趣大家自行閱讀。