likes
comments
collection
share

20分钟,带你手写简易版Axios

作者站长头像
站长
· 阅读数 42

Hello, 各位勇敢的小伙伴, 大家好, 我是你们的嘴强王者小五, 身体健康, 脑子没病.

本人有丰富的脱发技巧, 能让你一跃成为资深大咖.

一看就会一写就废是本人的主旨, 菜到抠脚是本人的特点, 卑微中透着一丝丝刚强, 傻人有傻福是对我最大的安慰.

欢迎来到 小五随笔系列手写简易版Axios.

前言

作为一个前端er,很难不和 axios 打交道,那你有曾去探索 axios 的具体实现吗?如果答案是否的话,不妨跟随笔者的步伐,共同实现一个简易版的 axios 吧

简易版 axios 代码 -- Github地址(在项目中使用命令 npm install deeruby-axios 安装体验)

tips:本文仅对 axios 的浏览器部分做简易实现,对 node 部分感兴趣的小伙伴可自行探索,借助适配器即可抹平环境差异,使用户无感知

20分钟,带你手写简易版Axios

基础拾遗

此部分内容为本文所需知识点,如需扩充,请各位看官自行查阅相关资料

XMLHttpRequest

axios 的浏览器环境围绕 XMLHttpRequest 为核心进行封装

👉 创建

const xhr = new XMLHttpRequest()

👉 open

用于初始化一个请求: open(method, url, async)

xhr.open('get', '/user', true)

👉 send

用于发送HTTP请求: send(data),若无 data 需传入 null

xhr.send(null)

👉 setRequestHeader

用于设置头部信息:setRequestHeader(key, value)

setRequestHeader('Authorization', `Bearer ${localStorage.getItem('access_token')}`)

👉 onreadystatechange

用于检测 readyState 值的变化,当 readyState === 4 时,表明已完成请求

xhr.onreadystatechange = () => {
  if (xhr.readyState !== 4) return
  // ... 执行xxx ...
}

👉 withCredentials

Boolean 类型,表示跨域请求时是否携带凭证信息,可阅读 HTTP请求方法 -> OPTIONS -> CORS:跨域资源共享 了解更多相关知识

👉 abort

用于在接收到响应前取消请求

xhr.abort()

👉 get & post

get 请求需将查询参数追加至 url 末尾

xhr.open('get', '/user?name=deeruby&age=18', true)

post 请求传递 data

const data = { name: 'deeruby', age: 18 }
xhr.open('post', '/user', true)
xhr.send(data)

对象处理

将 obj2 合并至 obj1

const mergeDeep = (obj1, obj2) => {
  for (let key in obj2) {
    obj1[key] = obj1[key] && isObject(obj1[key])
      ? mergeDeep(obj1[key], obj2[key]) 
      : obj2[key]
  }
  return obj1
}

判断是否为对象

const isObject = (data) => {
  return Object.prototype.toString.call(data) === '[object Object]'
}

同源判断

协议域名均相同即为同源

const urlParsingNode = document.createElement('a')

const resolveURL = url => {
  urlParsingNode.setAttribute('href', url)
  const { protocol, host } = urlParsingNode
  return { protocol, host }
}

const isURLSameOrigin = url => {
  const currentOrigin = resolveURL(window.location.href)
  const parsedOrigin = resolveURL(url)
  return (
    parsedOrigin.protocol === currentOrigin.protocol && 
    parsedOrigin.host === currentOrigin.host
  )
}

响应头解析

const parseHeaders = (headers) => {
  if (!headers) return {}

  let parsed = {}
  headers.split('\r\n').forEach(line => {
    let [key, value] = line.split(':')
    key = key.trim().toLowerCase()
    if (!key) return
    parsed[key] = value
  })
  return parsed
}

分析实战

这里只对一些关键步骤进行讲解,若需查看全部代码,请跳转 【Github - ajun568】手写axios,或通过 npm install deeruby-axios 安装体验,注意 post 的请求方式稍有不同,data 需放在 config 中传递

创建 Axios 类

Q1: 如何实现以下两种调用方式

  1. axios({ method: 'get', ... })
  2. axiox.get(...)

分析:

  • 实例化 Axios 即可调用 get 方法
class Axios {
  constructor() {}
  request() {}
  get() { this.request('get') }
}

const axios = new Axios()
axios.get()
  • axios({ ... }) 可通过将方法暴露出来实现
const instance = axios.request
return instance

Q2: 如何对上述两种调用方式进行整合

分析:

我们通过 Axios 的原型找到 request 方法,并使用 bind 改变其 this 指向

const context = new Axios(initConfig)
const instance = Axios.prototype.request.bind(context)
return instance

发送真实请求

send 方法关键步骤如下

const send = (config) => {
  const xhr = new XMLHttpRequest()
  xhr.open(method.toLowerCase(), url, true)
  xhr.send(data)
}

接下来我们处理 get 请求的参数,该参数通过 params 传递,取值后将其拼接至 url 中即可

20分钟,带你手写简易版Axios

buildUrl

let query = qs.stringify(params)
if (query) query = `?${query}`
return `${url}${query}`

处理好 get 请求参数后,再来处理 post 请求参数,该参数通过 data 传递

tips:这里我们直接将其放在 config 的 data 中,不进行其他处理

20分钟,带你手写简易版Axios

transformData

return JSON.stringify(data)

头部的默认值适配 get 及 post 两个方法,若 post 请求没有 Content-Type,则需追加默认值,逻辑如下

initializeHeaders

if (method.toLowerCase() !== 'post') return headers
if (headers['Content-Type']) return headers

headers['Content-Type'] = 'application/json;charset=utf-8'
return headers

最后,在 Axios 中增加 _dealRequest 方法整合以上操作

const { url, params, data, method, headers = {} } = config
config.url = buildUrl(url, params)
config.data = transformData(data)
config.headers = initializeHeaders(method, headers)
return send(config)

Q: 如何实现链式结构

分析: 借助 promise 即可实现链式结构,改造 send 方法,为其包裹一层 promise

return new Promise((resolve, reject) => {
  // ... xxx ...
  xhr.onreadystatechange = () => {
    if (xhr.readyState !== 4) return
    // ... xxx ...
    resolve(response)
  }
  // ... xxx ...
})

拦截器

  • 请求拦截器:用于在请求发送前统一执行某些操作,如在请求头中添加 token 字段

20分钟,带你手写简易版Axios

  • 响应拦截器:用于在接收到服务器响应后统一执行某些操作,如统一处理 401 跳转登录逻辑

20分钟,带你手写简易版Axios

分析

  • 拦截器是一个链式调用的过程,借助 promise 即可实现链式调用

  • 请求拦截在请求前执行,响应拦截在请求后执行,故构造形式为 [...request.use..., request, ...response.use...] 的数组,依次执行即可

  • send 方法需传入 config,故以 Promise.resolve(config) 为 promise 起点

综上,Interceptor 类实现如下

class Interceptor {
  interceptors = []

  use(resolved, rejected) {
    this.interceptors.push({
      resolved,
      rejected,
    })
    return this.interceptors.length - 1
  }

  forEach(fn) {
    this.interceptors.forEach(interceptor => {
      if (interceptor) fn(interceptor)
    })
  }
}

Axios 类中追加相关处理

constructor() {
  this.interceptors = {
    request: new Interceptor(),
    response: new Interceptor(),
  }
}
request(config) {
  const chain = [
    {
      resolved: this._dealRequest,
      rejected: undefined,
    }
  ]

  this.interceptors.request.forEach(interceptor => {
    chain.unshift(interceptor)
  })
  this.interceptors.response.forEach(interceptor => {
    chain.push(interceptor)
  })

  let promise = Promise.resolve(config)
  while (chain.length) {
    const { resolved, rejected } = chain.shift()
    promise = promise.then(resolved, rejected)
  }
  return promise
}

取消请求

方式1:

20分钟,带你手写简易版Axios

方式2:

20分钟,带你手写简易版Axios

分析

  • 取消请求通过实例化 CancelToken 实现

  • 方式1中的 source.token 即为方式2中的 new CancelToken(c => { cancel = c })

  • 方式1中的 source.cancel 即为方式2中的 cancel

  • 综合第2条第3条,方式1中的 source 方法是对方式2的封装

Q: 如何实现方式2

分析

  • 借助 promise 实现,定义 resolvePromise 保存 promise 的 resolve

  • 调用 cancel 时触发 resolve

  • promise.then 中调用 xhr.abort() 实现取消请求

CancelToken 实现

class CancelToken {
  promise

  constructor(executor) {
    let resolvePromise
    this.promise = new Promise(resolve => {
      resolvePromise = resolve
    })
    const paramFn = () => { resolvePromise() }
    executor(paramFn)
  }

  static source() {
    let cancel
    const token = new CancelToken(c => { cancel = c })
    return { token, cancel }
  }
}

send 方法中追加逻辑

if (cancelToken) {
  cancelToken.promise.then(reason => {
      xhr.abort()
      reject(reason)
    })
    .catch(() => {})
}

参考链接

【Lion】手写 axios 库并发布至 npm 线上完整过程

Fetch:中止(Abort)

20分钟,带你手写简易版Axios

转载自:https://juejin.cn/post/7165653891818717221
评论
请登录