Skip to content

Axios

仓库地址

axios 源码
ts-axios 文章参考源码
本次调试源码仓库

读前说明

  1. 此仓库是根据ts-axios系列文章的复现以及进一步优化。最大的特点是用更详细以及更容易理解的方式去实现部分功能模块包括interceptor|tranformRequest|tranformResponse|CancelToken等,以及新功能支持文件的上传与下载

优化内容

  1. 原文章项目的构建,使用方式比较复杂。此仓库采用轻量化 Webpack 配置,实现了一个极其简洁的项目构建,测试方案。 可以通过以下步骤得到一个最简单的起始项目

    • 切换到此仓库的init分支进行本地git clone --branch init 对应方式地址 。得到基础项目
    • cd server -> npm run server 得到运行在 http://127.0.0.1:3000 的 API 服务端
    • cd client -> npm run client 得到运行在 http://127.0.0.1:8080 的客户端测试网页
    • 根据原系列文章ts-axios以及此仓库调试注意事项进行对应代码书写。
    • 打开浏览器,输入 http://127.0.0.1:8080 -> F12 切换到Network查看效果。

    也可通过以下步骤获得一个已实现便于调试的项目

    • 切换到此仓库的main分支进行本地git clone --branch main 对应方式地址。得到基础项目
    • cd server -> npm run server 得到运行在 http://127.0.0.1:3000 的 API 服务端
    • cd client -> npm run client 得到运行在 http://127.0.0.1:8080 的客户端测试网页
    • 打开浏览器,输入 http://127.0.0.1:8080 -> F12 打开开发者工具切换到Network查看效果。
    • 通过点击页面上对应按钮触发网络请求,再通过 F12 打开开发者工具切换到Network查看效果。
  2. 原文章的实现大概为 2020 年,此仓库实现为 2022~2023 年。 axios, TypeScript已经迭代多个版本。 此仓库同步 TypeScript@4, axios@1 进行实现。在已废弃功能以及新特性上进行了对应 Debug 补充。

  3. client端优化详见mini-axios-ts#Debug

  4. server端完善了请求错误处理机制以及跨域处理方式。

  5. 此仓库服务端实现了Typescirpt方案,详见分支main-tsinit-ts

功能特性

  • 在浏览器端使用 XMLHttpRequest 对象通讯
  • 支持 PromiseAPI
  • 处理 get 请求 url 参数
  • 处理 post 请求参数
  • 处理请求的 header
  • 获取正确的响应数据
  • 转换响应头部
  • 转换响应数据类型
  • 异常处理
  • 接口扩展,支持 axios.(request| get | delete | head | options | put | patch | put)以及axios(config)
  • 允许 axios(url, config?)以及 axios(config)调用
  • 响应数据添加泛型约束
  • 支持请求和响应的拦截器
  • axios 自身的默认配置包括timeout, headers, transformRequest, transformResponse
  • 支持请求数据和响应数据的转换
  • 支持 axios.create 方法
  • 支持请求的主动取消
  • 支持通过 axios.isCancel(reason)判断报错是否是主动取消网络请求
  • 支持相同 CancelToken 实例的取消完成后的防抖操作
  • 客户端支持通过配置withCredentials实现跨域请求携带 cookie
  • 客户端防止 XSRF
  • 支持上传下载进度监控
  • 支持上传下载功能
  • 支持配置HTTPAuthorization
  • 支持配置状态码校验函数validateStatus, 优化了该项类型定义,允许用户配置时传入 null 取消状态码校验
  • 支持自定义序列化请求参数函数paramsSerializer
  • 支持baseURL的添加
  • 支持getUri方法
  • 支持all以及spread方法 (类型声明上仍然不够严谨)

注意事项

  1. 处理 get 请求 url 参数: params[key]对应值为 undefined 时,不直接删除,而是保留 key=''
  2. 处理 get 请求 url 参数: 测试用例中url中已存在的参数应该为url已存在部分参数
  3. 处理 get 请求 url 参数:hash 情况下改为:去除哈希标志#后的参数,并且拼接 config.params 参数
  4. 处理请求的 header: 优化helpers/headers.ts中的normalizeHeaderName函数,已知默认传入 headers 为空对象
  5. helpers/error.ts中的Object.setPrototypeOf(this, AxiosError.prototype) 这只是一个巧妙的解决方式用来应对 Error 类的原型问题, 具体原因参照 为什么扩展内置函数(如 Error、Array 和 )Map 不起作用?
  6. 异常处理:增强版中xhr.ts应该引入工厂函数createError而非AxiosError, 否则不应该使用createError()而是在对应位置使用new AxiosError
  7. 接口扩展时,helpers/data.tstransformResponse函数应该先判断是否存在 data 再进行类型判断, 因为响应可能为空, 此时 如果 JSON.parse()入参为空会报错
  8. 增加参数:core/Axios.tsrequest方法第一个参数url的类型修改为string | AxiosRequestConfig
  9. 增加参数: 新增一个请求时不需要 config 参数的测试用例 axios('/api/addParameters') 以及server/app.ts增加 app.get('/api/addParameters', (req, res) => {res.json(req.query)})
  10. 让响应数据支持泛型: 修改了测试用例的写法, 在未启用拦截器解包之前,AxiosResponse.data 应该为返回数据整体即{data: {name: 'NLRX', age: 18}, msg: 'hello world'}需要二次解包 res.data.data 才是类型User
  11. 拦截器执行顺序:请求拦截器遵循栈的顺序先入后出,响应拦截器遵循队列的顺序先入先出
  12. 拦截器这章推荐按照我这边的代码结构进行书写,会比原文章清晰的多。
  13. 拦截器这章重点关注这五个接口的定义:
    • Interceptor: 单组成功/失败拦截器
    • InterceptorManager:axios.interceptors.request 实例以及 axios.interceptors.response 的构造类
    • ResolvedFn :成功拦截器,包括入参为AxiosRequestConfig以及AxiosResponse这两种
    • RejectedFn: 失败拦截器,就是一个常规的错误函数 (err: any): any
    • PromiseArr: 请求时链式调用数组,包括了拦截器{ResolvedFn, RejectedFn}与常规请求{dispatchRequest}
  14. 拦截器类interceptorManager新增stackqueue方法,用于请求/响应拦截器的添加。原先是直接调用this.interceptors.request.interceptors以及this.interceptors.response.interceptors破坏了interceptorManager类的private特性。
  15. axios 默认配置:defaults.tsmethodsNoData修改为requestWithoutDataMethodsmethodsWithData修改为requestWithDataMethods
  16. axios 默认配置:core/mergeConfig.tsdefaultToUserConfig修改为routineProperties;valueFromUserConfig修改为valueFromUserProperties
  17. axios 默认配置:core/mergeConfig.tsmergeConfig函数对深度合并的判断条件进行了相关注释。
  18. axios 默认配置:helpers.tsflattenHeaders函数入参 method 改为可选,因为支持axios(url)的默认GET调用。
  19. 请求和响应数据配置化:优化了ransformRequest以及transformResponse在默认配置和用户配置的合并,在实际测试调用时不需要采用[userTransformRequst, ...axios.defaults.transformRequest]这种重复引入的方式,会自行合并。具体查看core/mergeConfig.ts的代码实现
  20. axios.CancelToken 原理解析,具体查看mini-axios-ts#CancelToken
  21. CancelToken.tsresolePromise接口进行了简化,type/index.tsCanceler同样进行了简化,调用cancel(message)取消网络请求时一定要传入对应的 message
  22. 封装CancelToken.source的好处在于实际调用该方法时,就已经返回了对应的token以及cancel。可以在axios.get之余方法调用后直接调用cancel(message)。 否则在ts静态检测阶段,如果在 config.cancelToken 再去 new CancelToken 传入一个 executor 去给 cancel 赋值,会出现在给cancel赋值前使用了cancel的报错,这种时候需要异步执行 cancel。详见测试用例16:通过方式二主动取消网络请求
  23. 防止 XSRF 攻击:优化了helpers/isURLSameOrigin.ts, 采用更严谨的方式来处理protocol, host, port
  24. 防止 XSRF 攻击:优化了 server 端app.jscors, app.options的处理方式。
  25. 文件上传下载进度监控:添加了实际上传下载功能的实现,具体查看对应代码
  26. 文件下载功能实现:查看client/axios/helpers/data.ts/transformResponse函数实现以及对应的测试用例,server/app.js对应的22接口
  27. 文件上传功能实现:通过multer实现
  28. 添加 HTTP 授权 auth 属性:具体好处可参考此文章Http auth 认证的两种方式 Basic 方式和 Digest 认证, 这里补充一下:
    • digest认证在服务端是通过客户端发送过来的username在服务端查找其对应的password,解决了密码明文传输的问题。
    • 这两种认证方式都是在没有办法使用HTTPS认证下的备选,都存在安全问题。
  29. 自定义序列化请求参数:这章在client文件夹安装qs时,需要安装对应的*.d.ts支持, 因此应该在client文件夹下执行npm install @types/qs --save -dev
  30. axios.all 和 axios.spread:目前axios官方已弃用这两个方法,axios.all 类型约束可参考Promise.all,因此这个库就不实现了,而是以Promise.all的类型取代axios.all。删除了axios.spread功能。原文章实现的 axios.all 以及 axios.spread 可以理解为语法糖做法,且在响应数据泛型设置上是存在无法同时约束多个返回值的问题。
  31. axios.all的类型定义难度在于all方法入参泛型 T 为[AxiosPromise<resultA>, AxiosPromise<resultB>], 但返回值却要是Promise<[AxiosResponse<resultA>, AxiosResponse<resultB>]>。 如果你的TypeScript理解够深,不妨实现一下上述描述然后提个pull request,感谢!
  32. 对于axios.spread, 的类型定义,泛型T应该为数组类型, 对应[AxiosResponse<resultA>, AxiosResponse<resultB>]。 同上,如果可以实现对应的类型注解,同样欢迎pull request

示例代码

  1. CancelToken 这个类初始化的时候需要传递一个方法 executor,并且它的内部新建了一个 promise,最关键的是,它把 promise 的 resolve 方法的执行放在了 executor 方法入参c里面(重复理解这句话,对理解 CancelToken 设定非常重要)
javascript
// CancelToken类的实现
// (如果理解这个,就不需要再看下面其他2,3, 4, ......)
class CancelToken {
  constructor(executor) {
    let resolveHandle
    this.promise = new Promise((resolve) => {
      resolveHandle = resolve
    })
    executor(function (message) {
      if (this.reason) {
        return
      }
      this.reason = message
      resolveHandle(this.reason)
    })
  }

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

// 调用
const cancelProof = CancelToken.source()
cancelProof.cancel('this is cancel Message')
cancelProof.token.promise.then((res) => {
  console.log(res) // this.is cancel Message
})
  • axios.CancelToken 是一个类
  • axiosCancelToken.source()是静态方法,调用的是 CancelToken 类的静态 source 方法
  • axiosCancelToken.source()会返回token以及cancel
  • token是新创建的 CancelToken 实例, canceltokenCancelToken实例构建时,对应的用于触发 Promiseresolve控制权的resolveHandle
  • 因此,一旦调用了cancel方法,即将token(也就是新创建的 CancelToken 实例)构建的 promise 设为resolved, 从而可以在这个 promise 的then方法里触发实际的网络请求取消。 (伪代码:promise.then(res => {xml.abort(), reject(res)}))
  1. 综上所述,因此在 axios()调用,传入的 config 需要传入token(即创建的 CancelToken 实例), 此外,在需要主动取消时需要调用cancel方法(即将创建的 CancelToken 实例里的 promise 设为 resolved,从而触发对应的 then 方法,取消网络请求。)

  2. 因为每个 axios()调用如果都要携带主动取消功能,就需要对应的token以及cancel。两者的关系绑定通过CancelToken的静态方法source调用返回值实现。于是就能看到以下调用。

javascript
const cancelProof1 = axios.CancelToken.source()
const cancelProof2 = axios.CancelToken.source()


axios.get(url, {
    cancelToken: cancelProof1.token
})

axios.post(url, data {
	cancelToken: cancelProof2.token
})

cancelProof1.cancel('取消第一个请求')
cancelProof2.cancel('取消第二个请求')
  1. 此除之所以要在 axios()调用时,传入 config.cancelToken 为对应的token是因为要通过 promise.then 触发网络请求取消,因此通过获取当前 axios.get, axios.post 调用时传入的 config.CancelToken, 从而获取到对应的 promise.then。

  2. new CancelToken 传入的executor是为了获取到可在外部调用的cancel方法,CancelToken.constructor 内部executor(fn)执行时这里的fn为实际的 promise.resolve 方法。当你调用cancel(message)时,即调用fn(message)。所以在具体的 CancelToken.source 源码为:

    javascript
    CancelToken.source = function source() {
    	var cancel
    	var token = new CancelToken(function executor(c) => {
    		cancel = c
    	})
    	return {
    		cancel,
    		token
    	}
    }
  3. 而 CancelToken 类的实现如下:

    javascript
    class CancelToken {
      constructor(executor) {
        let resolveHandle
        this.promise = new Promise((resolve) => {
          resolveHandle = resolve
        })
        executor((message) => {
          if (this.reason) {
            return
          }
          this.reason = message
          resolveHandle(message)
        })
      }
    }
  4. 从而在外部调用cancel('message')触发以下入参函数

    javascript
    cancel函数 等价于 c  等价于executor的入参函数:(message) => {
    	if(this.reason) {
    		return
    	}
    	this.reason = message
    	resolveHandle(message)
    }