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

400 阅读3分钟

我正在参加「掘金·启航计划」
大家好,最近在看axios库的源码时,突发奇想:有没有可能,自己去实现一个类似axios的库呢? 说干就干,我们先来看看axios库实现了哪些功能 github.com/axios/axios

明确需求

通过阅读ReadMe,我们发现以下几点:

image-20230426154438368

  • 浏览器端基于XMLHttpRequest实现的
  • nodejs端基于http实现
  • 支持Promise
  • 支持请求拦截和响应拦截
  • 支持数据转换
  • 支持取消请求
  • 自动推断JSON格式数据
  • 自动将数据对象序列化为multipart/form-datax-www-form-urlencoded主体编码
  • 防止XSRF攻击

制定实现的功能点

​ 因为我们大部分使用的场景是基于浏览器端,基于此,我们实现基于XMLHttpRequest部分的功能。通过对上述的功能点我们小步走

  • 创建仓库
    • 我们使用ts 基础库的开发脚手架 TypeScript library starter,不过这个库很久没更新了,不过影响功能的实现
    • github.com/alexjoverm/…
    • 测试用例使用了nodejs做后端服务,文中没有对这一部分进行详细的描述,可自行实现。

基础功能实现

axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});

我们来实现基础的功能点,定义一个axios方法,最终是通过xhr来实现请求的,具体实现如下:

// types  index
export type Method = 'get' | 'GET'
  | 'delete' | 'Delete'
  | 'head' | 'HEAD'
  | 'options' | 'OPTIONS'
  | 'post' | 'POST'
  | 'put' | 'PUT'
  | 'patch' | 'PATCH'
export interface AxiosRequestConfig {
  url: string
  method?: Method
  data?: any
  params?: any
}

// index
import {AxiosRequestConfig} from './types'
import xhr from './xhr'

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

export default axios
// xhr
export default function xhr(config: AxiosRequestConfig): void {
  const { data = null, url, method = 'get' } = config
  const request = new XMLHttpRequest()
  request.open(method.toUpperCase(), url, true)
  request.send(data)
}

至此,我们就实现了简易版的axios请求。

测试

测试用例如下

// test
import axios from './axios'
// should send a GET request to the specified URL
axios({ 
  url: 'https://jsonplaceholder.typicode.com/posts',
  method: 'get'
})

我们打开浏览器,可以看到有xhr请求,并且返回了数据,说明我们的函数是正确的

处理url带参逻辑

在文档中,我们可以看到如下描述

image-20230426165731966

需要处理功能点:

  • 普通对象
  • 拼接在url后面
  • URLSearchParams对象,相当于encodeURL
  • 非空处理
// plain object
 function isPlainObject(obj) {
  if (typeof obj !== 'object' || obj === null) return false

  let proto = obj
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}
// valid Date
const toString = Object.prototype.toString

export function isDate (val: any): val is Date {
  return toString.call(val) === '[object Date]'
}
// encode url
// 将不需要处理的字符转换回来  如:@ ? : [] $
function encode (val: string): string {
  return encodeURIComponent(val)
    .replace(/%40/g, '@')
    .replace(/%3A/gi, ':')
    .replace(/%24/g, '$')
    .replace(/%2C/gi, ',')
    .replace(/%20/g, '+')
    .replace(/%5B/gi, '[')
    .replace(/%5D/gi, ']')
}
// 拼接url
export function bulidURL (url: string, params?: any) {
  if (!params) {
    return url
  }

  const parts: string[] = []

  Object.keys(params).forEach((key) => {
    let val = params[key]
    // 非空处理
    if (val === null || typeof val === 'undefined') {
      return
    }
    let values: string[]
    // 处理数组
    if (Array.isArray(val)) {
      values = val
      key += '[]'
    } else {
      values = [val]
    }
    values.forEach((val) => {
      if (isDate(val)) {
        val = val.toISOString()
      } else if (isObject(val)) {
        val = JSON.stringify(val)
      }
      parts.push(`${encode(key)}=${encode(val)}`)
    })
  })

  let serializedParams = parts.join('&')

  if (serializedParams) {
     // 移除Hash
    const markIndex = url.indexOf('#')
    if (markIndex !== -1) {
      url = url.slice(0, markIndex)
    }

    url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
  }

  return url
}
  • 上述实现的最小的功能点,现在将其接入axios
function axios (config: AxiosRequestConfig): void {
  processConfig(config)
  xhr(config)
}

function processConfig (config: AxiosRequestConfig): void {
  config.url = transformUrl(config)
}

function transformUrl (config: AxiosRequestConfig): string {
  const { url, params } = config
  return bulidURL(url, params)
}

测试

axios({
  url: 'https://jsonplaceholder.typicode.com/posts',
  method: 'get',
  params: {
    id: 1
  }
})
axios({
  url: 'https://jsonplaceholder.typicode.com/posts',
  method: 'get',
  params: {
    id: 1,
    userId: 1
  }
})
axios({
  method: 'get',
  url: 'https://jsonplaceholder.typicode.com/posts',
  params: {
    foo: '@:$, '
  }
})
axios({
  method: 'get',
  url: 'https://jsonplaceholder.typicode.com/posts',
  params: {
    foo: 'bar',
    baz: null
  }
})

我们打开浏览器控制台,可以看到请求url是正确的。

总结

​ 我们根据axios文档提示,实现了部分简易的逻辑,还有很多的功能点未完成,下一章节中,我们继续去实现基础功能的处理。通过阅读源码和文档的形式,并实现MVP。可以极大的加深我们对源码的认识和提高自己的编程技术,当然,也可以借助GPT等工具,更加容易理解源码。