【源码阅读】| 从零到一实现简易版axios (五)

151 阅读3分钟

我正在参加「掘金·启航计划」
通过前几个章节,我们实现了axios的基础功能。在此基础上,对异常捕获的信息进行了增强。为了进一步提升用户体验,我们引入了混合类型,给axios拓展了多个语法糖。利用了重载的特性,实现了参数传参的不同类型。接下来,我们优化响应数据的类型推断和实现拦截器的功能。

需求分析

响应数据和请求支持泛型

  • 我们希望接口返回的数据是我们所期望的数据类型,因此,一开始发送请求的时候,就传入期望的数据格式,以便更好地处理数据。
  • 我们希望有以下格式的生成:
import axios, { AxiosResponse } from 'axios';

async function fetch<T>(url: string): Promise<AxiosResponse<T>> {
  const response = await axios.get<T>(url);
  return response;
}

interface User {
  name: string;
  age: number;
}

const fetchUser = async (): Promise<User> => {
  const response = await fetch<User>('/api/user/1');
  return response.data;
}

(async () => {
  const user = await fetchUser();
  console.log(user.name, user.age);
})();

这样声明后,我们可以很方便的处理响应数据,借助TS能够推断出数据的类型。
首先,我们看下源码中是怎么实现这部分的:
github.com/axios/axios…
image.png
我们修改之前定义的类型,增加泛型

// types/index
export interface AxiosResponse<T = any> {
  data: T
  status: number
  statusText: string
  headers: any
  config: AxiosRequestConfig
  request: any
}

export interface AxiosPromise<T = any> extends Promise<AxiosResponse<T>> {
}

export interface Axios {
  request<T = any>(config: AxiosRequestConfig): AxiosPromise<T>

  get<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>

  delete<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>

  head<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>

  options<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>

  post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>

  put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>

  patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>
}

export interface AxiosInstance extends Axios {
  <T = any>(config: AxiosRequestConfig): AxiosPromise<T>

  <T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>
}

通过这一步,当我们发送请求的时候,得到的是AxiosPromise<T>,然后可以在响应的数据中获取到T

响应拦截器和请求拦截器

对应的文档:axios-http.com/docs/interc…

通过阅读文档,我们大概知道需要的功能

  • 基于Promise调用,支持then,catch
  • 有一个变量存储interceptors
  • Interceptorsrequestresponse中有两个函数
    • use(添加)
    • eject (删除)

我们先来拆分问题,先定义Interceptors.request的接口类型

// types/index.ts
export interface AxiosInterceptorManager<T> {
  // 创建成功后返回id作为删除的索引值
  use(resolved: ResolvedFn<T>, rejected?: RejectedFn): number

  eject(id: number): void
}

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

export interface RejectedFn {
  (error: any): any
}

具体实现:

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

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

在原有的基础加入interceptors

// core/axios.ts
interface Interceptors {
  request: InterceptorManager<AxiosRequestConfig>
  response: InterceptorManager<AxiosResponse>
}

export default class Axios {
  interceptors: Interceptors

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

  // 省略...
}

实现链式调用,我们利用的是Promise的特性。
根据原有的axios库,我们知道,请求拦截器是先进后出,响应拦截是先进先出。
我们使用一个队列来管理。

// core/axios.ts
interface PromiseChain<T> {
  resolved: ResolvedFn<T> | ((config: AxiosRequestConfig) => AxiosPromise)
  rejected?: RejectedFn
}
export default class Axios {
  // 省略...
  request(url: any, config?: any): AxiosPromise {
    if (typeof url === 'string') {
      if (!config) {
        config = {}
      }
      config.url = url
    } else {
      config = url
    }

    const chain: PromiseChain<any>[] = [
      {
        resolved: dispatchRequest,
        rejected: undefined
      }
    ]

    // 请求拦截器后添加的先执行
    this.interceptors.request.forEach(interceptor => {
      // 后添加的先执行
      chain.unshift(interceptor)
    })

    this.interceptors.response.forEach(interceptor => {
      // 先添加的先执行
      chain.push(interceptor)
    })

    // 通过 Promise 链式调用的方式,依次执行请求拦截器和请求方法
    let promise = Promise.resolve(config)

    
    while (chain.length) {
      const { resolved, rejected } = chain.shift()!
      promise = promise.then(resolved, rejected)
    }

    return promise
  }

}

测试用例

import axios from '../../src/index'

axios.interceptors.request.use(config => {
  console.log('first request interceptor')
  config.headers.test += '1'
  return config
})
axios.interceptors.request.use(config => {
  console.log('second request interceptor')
  config.headers.test += '2'
  return config
})
axios.interceptors.request.use(config => {
  console.log('third request interceptor')
  config.headers.test += '3'
  return config
})

axios.interceptors.response.use(res => {
  res.data += '1'
  return res
})
let interceptor = axios.interceptors.response.use(res => {
  res.data += '2'
  return res
})
axios.interceptors.response.use(res => {
  res.data += '3'
  return res
})

axios.interceptors.response.eject(interceptor)

axios({
  url: '/interceptor/get',
  method: 'get',
  headers: {
    axiosRequestInterceptor: ''
  }
}).then(res => {
  console.log(res.data)
})

对应的输出结果:
image.png
可以看到,请求拦截器是先进后出的,响应拦截器是先进先出,经典的洋葱圈模型。通过执行eject,我们也成功的验证了删除函数。

总结

通过上述的章节,我们增加了响应数据的泛型支持,并实现了请求拦截器和响应拦截器,方便用户可以更好的对请求做处理(权限校验等功能)。通过实现这些功能,我们了解了洋葱圈模型是怎么样实现的,怎么将每一层链接起来,依次执行。