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

319 阅读4分钟

我正在参加「掘金·启航计划」
接前面的章节,我们完成了axios的基本功能实现,并在最后章节中,我们对一些常见的xhr错误进行了捕获处理。
接下来,我们来对捕获的错误信息进行增强,建立AxiosError

  • 为什么需要AxiosError
    • 可以获取到更详细的请求和响应信息,帮助我们更快速地排查和解决问题。

需求分析

处理AxiosError

我们可以参考源码中的定义
github.com/axios/axios…

  • 错误信息 mssage
  • 错误代码 code
  • 当前的 request config
  • request
  • response

首先,我们定义一个接口

export interface AxiosError extends Error {
	config: AxiosRequestConfig
	code?: string
	request?: any
	response?: AxiosResponse
	isAxiosError: boolean
}

实现AxiosError

import { AxiosRequestConfig, AxiosResponse } from './../types/index'
export class AxiosError extends Error {
  config: AxiosRequestConfig
  code?: string
  request?: any
  response?: AxiosResponse
  isAxiosError: boolean

  constructor(
    message: string,
    config: AxiosRequestConfig,
    code?: string | undefined,
    request?: any,
    response?: AxiosResponse
  ) {
    super(message)

    this.config = config
    this.code = code
    this.request = request
    this.response = response
    this.isAxiosError = true

    // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf
    Object.setPrototypeOf(this, AxiosError.prototype)
  }
}

// 对外暴露接口
export function  createError(
  message: string,
  config: AxiosRequestConfig,
  code?: string | undefined,
  request?: any,
  response?: AxiosResponse
): AxiosError {
  const error = new AxiosError(message, config, code, request, response)

  return error
}

在原有的axios中导出类型定义

// axios.ts
import {AxiosRequestConfig} from './types'
import xhr from './xhr'

function axios(config:AxiosRequestConfig){
    xhr(config)
}

export default axios
// index.ts
import axios from './axios'
export * from './types'

export default axios

对原有的error修改成axiosError

// xhr.ts
export default function xhr(config: AxiosRequestConfig): AxiosPromise {
  return new Promise((resolve, reject) => {
    // 省略代码...

    request.ontimeout = function handleTimeout() {
      reject(
        createError(
          `Timeout of ${timeout} ms exceeded`,
          config,
          'ECONNABORTED',
          request
        )
      )
    }

    request.open(method.toUpperCase(), url!, true)

    request.onreadystatechange = function handleLoad() {
      // 省略代码...

    request.onerror = function handleError() {
      reject(createError('Network Error', config, undefined, request))
    }

    // ...
      
    function handleResponse(response: AxiosResponse): void {
      if (response.status >= 200 && response.status < 300) {
        resolve(response)
      } else {
        reject(
          createError(
            `Request fail with status code ${response.status}`,
            config,
            undefined,
            request,
            response
          )
        )
      }
    }
  })
}

通过实现AxiosError类,我们对原有的error做了增强,更方便去排查问题。

Axios API的实现

接下来,我们来看下Axios API的实现,我们从文档中得知,发送axios请求可以有几种形式:

// Send a POST request
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});
// Send a GET request (default method)
axios('/user/12345');

// The Axios Instance
const instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

image.png
我们可以知道,axios库提供了多种接口方便用户发送请求,并且axios是一个混合类型
我们先来定义axios的类型

export interface Axios {
  request(config: AxiosRequestConfig): AxiosPromise

  get(url: string, config?: AxiosRequestConfig): AxiosPromise

  delete(url: string, config?: AxiosRequestConfig): AxiosPromise

  head(url: string, config?: AxiosRequestConfig): AxiosPromise

  options(url: string, config?: AxiosRequestConfig): AxiosPromise

  post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise

  put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise

  patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise
}

// 定义了axios instance对象,可以直接使用
// 重载,兼容第一个传 url 的情况
export interface AxiosInstance extends Axios {
  (config: AxiosRequestConfig): AxiosPromise
  (url: string, config?: AxiosRequestConfig): AxiosPromise
}

// 因为要适配上述第二种情况,Url改为可选
export interface AxiosRequestConfig {
  url?: string
  // ...
}

具体实现:
将原有的xhr逻辑移入core目录改命为dispatchRequest
对外暴露的多个接口相当于语法糖,本质还是调用request方法,我们将request分为传data和不传data两种形式,再对其进行一层封装。

// core/Axios.ts
export default class Axios {
  request(url: any, config?: any): AxiosPromise {
    // 如果传入的是字符串,就把它放到 config 的 url 属性上
    // 如果传入的是对象,就直接赋值给 config
    if (typeof url === 'string') {
      if (!config) {
        config = {}
      }
      config.url = url
    } else {
      config = url
    }
    return dispatchRequest(config)
  }

  get(url: string, config?: AxiosRequestConfig): AxiosPromise {
    return this._requestMethodWithoutData('get', url, config)
  }

  delete(url: string, config?: AxiosRequestConfig): AxiosPromise {
    return this._requestMethodWithoutData('delete', url, config)
  }

  head(url: string, config?: AxiosRequestConfig): AxiosPromise {
    return this._requestMethodWithoutData('head', url, config)
  }

  options(url: string, config?: AxiosRequestConfig): AxiosPromise {
    return this._requestMethodWithoutData('options', url, config)
  }

  post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
    return this._requestMethodWithData('post', url, data, config)
  }

  put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
    return this._requestMethodWithData('put', url, data, config)
  }

  patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
    return this._requestMethodWithData('patch', url, data, config)
  }

  _requestMethodWithoutData(method: Method, url: string, config?: AxiosRequestConfig) {
    return this.request(
      Object.assign(config || {}, {
        method,
        url
      })
    )
  }

  _requestMethodWithData(method: Method, url: string, data?: any, config?: AxiosRequestConfig) {
    return this.request(
      Object.assign(config || {}, {
        method,
        url,
        data
      })
    )
  }
}

至此,我们实现了Axios类,然后我们将其转成混合类型
对应库的源码:github.com/axios/axios…

// helpers/utils
export function extend<T, U>(to: T, from: U): T & U {
  for (const key in from) {
    ;(to as T & U)[key] = from[key] as any
  }
  return to as T & U
}
// src/axios.ts
import { AxiosInstance } from './types'
import Axios from './core/axios'
import { extend } from './helpers/utils'


function createInstance(): AxiosInstance {
  const context = new Axios()

  // 创建instance 指向 Axios.prototype.request 方法,并绑定了上下文 context
  const instance = Axios.prototype.request.bind(context)

  // 通过 extend 方法把 context 中的原型方法和实例方法全部拷贝到 instance 上
  extend(instance, context)

  return instance as AxiosInstance
}


const axios = createInstance()


export default axios

通过上述的操作,当我们调用axios对象时,内部生成Axios实例,然后将request方法绑定到实例中,当直接调用axios方法,相当于调用request。

总结

通过上述章节,我们实现了axios的错误处理增强和Axios混合类型。接下来,我们将对response做更友好的优化处理和拦截器的实现。
image.png