Axios
仓库地址
axios 源码
ts-axios 文章参考源码
本次调试源码仓库
读前说明
- 此仓库是根据
ts-axios
系列文章的复现以及进一步优化。最大的特点是用更详细以及更容易理解的方式去实现部分功能模块包括interceptor
|tranformRequest
|tranformResponse
|CancelToken
等,以及新功能支持文件的上传与下载
等
优化内容
原文章项目的构建,使用方式比较复杂。此仓库采用轻量化 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
查看效果。
- 切换到此仓库的
原文章的实现大概为 2020 年,此仓库实现为 2022~2023 年。
axios
,TypeScript
已经迭代多个版本。 此仓库同步 TypeScript@4, axios@1 进行实现。在已废弃功能以及新特性上进行了对应 Debug 补充。client
端优化详见mini-axios-ts#Debug。server
端完善了请求错误处理机制以及跨域处理方式。此仓库服务端实现了
Typescirpt
方案,详见分支main-ts
和init-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
方法 (类型声明上仍然不够严谨)
注意事项
- 处理 get 请求 url 参数: params[key]对应值为 undefined 时,不直接删除,而是保留 key=''
- 处理 get 请求 url 参数: 测试用例中
url中已存在的参数
应该为url已存在部分参数
- 处理 get 请求 url 参数:hash 情况下改为:去除哈希标志#后的参数,并且拼接 config.params 参数
- 处理请求的 header: 优化
helpers/headers.ts
中的normalizeHeaderName
函数,已知默认传入 headers 为空对象 helpers/error.ts
中的Object.setPrototypeOf(this, AxiosError.prototype)
这只是一个巧妙的解决方式用来应对 Error 类的原型问题, 具体原因参照 为什么扩展内置函数(如 Error、Array 和 )Map 不起作用?- 异常处理:增强版中
xhr.ts
应该引入工厂函数createError
而非AxiosError
, 否则不应该使用createError()
而是在对应位置使用new AxiosError
- 接口扩展时,
helpers/data.ts
的transformResponse
函数应该先判断是否存在 data 再进行类型判断, 因为响应可能为空, 此时 如果 JSON.parse()入参为空会报错 - 增加参数:
core/Axios.ts
的request
方法第一个参数url
的类型修改为string | AxiosRequestConfig
- 增加参数: 新增一个请求时不需要 config 参数的测试用例
axios('/api/addParameters')
以及server/app.ts
增加app.get('/api/addParameters', (req, res) => {res.json(req.query)})
- 让响应数据支持泛型: 修改了测试用例的写法, 在未启用拦截器解包之前,AxiosResponse.data 应该为返回数据整体即
{data: {name: 'NLRX', age: 18}, msg: 'hello world'}
需要二次解包 res.data.data 才是类型User
- 拦截器执行顺序:请求拦截器遵循栈的顺序
先入后出
,响应拦截器遵循队列的顺序先入先出
- 拦截器这章推荐按照我这边的代码结构进行书写,会比原文章清晰的多。
- 拦截器这章重点关注这五个接口的定义:
Interceptor
: 单组成功/失败拦截器InterceptorManager
:axios.interceptors.request 实例以及 axios.interceptors.response 的构造类ResolvedFn
:成功拦截器,包括入参为AxiosRequestConfig
以及AxiosResponse
这两种RejectedFn
: 失败拦截器,就是一个常规的错误函数 (err: any): anyPromiseArr
: 请求时链式调用数组,包括了拦截器{ResolvedFn, RejectedFn}
与常规请求{dispatchRequest}
- 拦截器类
interceptorManager
新增stack
和queue
方法,用于请求/响应拦截器的添加。原先是直接调用this.interceptors.request.interceptors
以及this.interceptors.response.interceptors
破坏了interceptorManager
类的private
特性。 - axios 默认配置:
defaults.ts
中methodsNoData
修改为requestWithoutDataMethods
,methodsWithData
修改为requestWithDataMethods
- axios 默认配置:
core/mergeConfig.ts
中defaultToUserConfig
修改为routineProperties
;valueFromUserConfig
修改为valueFromUserProperties
- axios 默认配置:
core/mergeConfig.ts
中mergeConfig
函数对深度合并的判断条件进行了相关注释。 - axios 默认配置:
helpers.ts
中flattenHeaders
函数入参 method 改为可选,因为支持axios(url)
的默认GET
调用。 - 请求和响应数据配置化:优化了
ransformRequest
以及transformResponse
在默认配置和用户配置的合并,在实际测试调用时不需要采用[userTransformRequst, ...axios.defaults.transformRequest]这种重复引入的方式,会自行合并。具体查看core/mergeConfig.ts
的代码实现 - axios.CancelToken 原理解析,具体查看mini-axios-ts#CancelToken
CancelToken.ts
的resolePromise
接口进行了简化,type/index.ts
的Canceler
同样进行了简化,调用cancel(message)
取消网络请求时一定要传入对应的 message- 封装
CancelToken.source
的好处在于实际调用该方法时,就已经返回了对应的token
以及cancel
。可以在axios.get
之余方法调用后直接调用cancel(message)
。 否则在ts静态检测阶段
,如果在 config.cancelToken 再去 new CancelToken 传入一个 executor 去给 cancel 赋值,会出现在给cancel赋值前使用了cancel
的报错,这种时候需要异步执行 cancel。详见测试用例16:通过方式二主动取消网络请求
- 防止 XSRF 攻击:优化了
helpers/isURLSameOrigin.ts
, 采用更严谨的方式来处理protocol
,host
,port
- 防止 XSRF 攻击:优化了 server 端
app.js
的cors
,app.options
的处理方式。 - 文件上传下载进度监控:添加了实际上传下载功能的实现,具体查看对应代码
- 文件下载功能实现:查看
client/axios/helpers/data.ts/transformResponse
函数实现以及对应的测试用例,server/app.js
对应的22
接口 - 文件上传功能实现:通过
multer
实现 - 添加 HTTP 授权 auth 属性:具体好处可参考此文章Http auth 认证的两种方式 Basic 方式和 Digest 认证, 这里补充一下:
- digest认证在服务端是通过客户端发送过来的
username
在服务端查找其对应的password
,解决了密码明文传输的问题。 - 这两种认证方式都是在没有办法使用
HTTPS
认证下的备选,都存在安全问题。
- digest认证在服务端是通过客户端发送过来的
- 自定义序列化请求参数:这章在
client
文件夹安装qs
时,需要安装对应的*.d.ts
支持, 因此应该在client
文件夹下执行npm install @types/qs --save -dev
- axios.all 和 axios.spread:目前
axios
官方已弃用这两个方法,axios.all 类型约束可参考Promise.all
,因此这个库就不实现了,而是以Promise.all
的类型取代axios.all
。删除了axios.spread
功能。原文章实现的 axios.all 以及 axios.spread 可以理解为语法糖做法,且在响应数据泛型设置上是存在无法同时约束多个返回值
的问题。 axios.all
的类型定义难度在于all
方法入参泛型 T 为[AxiosPromise<resultA>, AxiosPromise<resultB>]
, 但返回值却要是Promise<[AxiosResponse<resultA>, AxiosResponse<resultB>]>
。 如果你的TypeScript
理解够深,不妨实现一下上述描述然后提个pull request
,感谢!- 对于
axios.spread
, 的类型定义,泛型T
应该为数组类型
, 对应[AxiosResponse<resultA>, AxiosResponse<resultB>]
。 同上,如果可以实现对应的类型注解,同样欢迎pull request
。
示例代码
- CancelToken 这个类初始化的时候需要传递一个方法 executor,并且它的内部新建了一个 promise,最关键的是,它把 promise 的 resolve 方法的执行放在了 executor 方法入参
c
里面(重复理解这句话,对理解 CancelToken 设定非常重要)
// 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 实例,cancel
是token
即CancelToken
实例构建时,对应的用于触发 Promiseresolve
控制权的resolveHandle
。- 因此,一旦调用了
cancel
方法,即将token
(也就是新创建的 CancelToken 实例)构建的 promise 设为resolved
, 从而可以在这个 promise 的then
方法里触发实际的网络请求取消。 (伪代码:promise.then(res => {xml.abort(), reject(res)}))
综上所述,因此在 axios()调用,传入的 config 需要传入
token
(即创建的 CancelToken 实例), 此外,在需要主动取消时需要调用cancel
方法(即将创建的 CancelToken 实例里的 promise 设为 resolved,从而触发对应的 then 方法,取消网络请求。)因为每个 axios()调用如果都要携带主动取消功能,就需要对应的
token
以及cancel
。两者的关系绑定通过CancelToken
的静态方法source
调用返回值实现。于是就能看到以下调用。
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('取消第二个请求')
此除之所以要在 axios()调用时,传入 config.cancelToken 为对应的
token
是因为要通过 promise.then 触发网络请求取消,因此通过获取当前 axios.get, axios.post 调用时传入的 config.CancelToken, 从而获取到对应的 promise.then。new CancelToken 传入的
executor
是为了获取到可在外部调用的cancel
方法,CancelToken.constructor 内部executor(fn)
执行时这里的fn
为实际的 promise.resolve 方法。当你调用cancel(message)
时,即调用fn(message)
。所以在具体的 CancelToken.source 源码为:javascriptCancelToken.source = function source() { var cancel var token = new CancelToken(function executor(c) => { cancel = c }) return { cancel, token } }
而 CancelToken 类的实现如下:
javascriptclass CancelToken { constructor(executor) { let resolveHandle this.promise = new Promise((resolve) => { resolveHandle = resolve }) executor((message) => { if (this.reason) { return } this.reason = message resolveHandle(message) }) } }
从而在外部调用
cancel('message')
触发以下入参函数javascriptcancel函数 等价于 c 等价于executor的入参函数:(message) => { if(this.reason) { return } this.reason = message resolveHandle(message) }