源码学习 TS + NodeJS实现axios Breezli 2025-03-04 2025-03-08 TS + NodeJS 实现 axios 1 git clone https://github.com/alexjoverm/typescript-library-starter.git ts-axios
需求
在浏览器端使用 XMLHttpRequest 对象通讯
Promise API
请求响应拦截器
请求数据和响应数据转换
请求的取消
JSON 数据的自动转换
客户端防止 XSRF
框架工具
请求代码 axios 最基本的操作
1 2 3 4 5 6 7 8 axios ({ method : 'get' , url : '/simple/get' , params : { a : 1 , b : 2 , }, })
创建入口文件
index.ts
1 2 3 function axios (config ) {}export default axios
xhr.ts
xhr
函数用于发送 HTTP 请求的工具函数,基于 XMLHttpRequest API 实现,并提供了一个简单的接口来配置和发送请求。
1 2 3 4 5 6 7 8 import { AxiosRequestConfig } from './types' export default function xhr (config : AxiosRequestConfig ) { const { data = null , url, method = 'get' } = config const request = new XMLHttpRequest () request.open (method.toUpperCase (), url, true ) request.send (data) }
安装依赖
webpack 配置
webpack.config.js
整体导出一个对象,包含 mode(开发模式),entry,output,module,resolve,plugins 几个关键属性
entry 1 2 3 4 5 6 7 8 9 entry : fs.readdirSync (__dirname).reduce ((entries, dir ) => { const fullDir = path.join (__dirname, dir) const entry = path.join (fullDir, 'app.ts' ) if (fs.statSync (fullDir).isDirectory () && fs.existsSync (entry)) { entries[dir] = ['webpack-hot-middleware/client' , entry] } return entries }, {}),
entries 收集了多个目录的入口文件 (app.ts) ,并将每个入口引入一个用于热更新的文件
entries 是一个对象,key 为目录名
output
将 entries 的 key 打包成 filename
1 2 3 4 5 output : { path : path.join (__dirname, '__build__' ), filename : '[name].js' , publicPath : '/__build__/' },
module
rules 定义一系列处理不同类型文件的规则
.ts :tslint-loader
进行预处理 (包括类型检查)
.tsx :transpileOnly: true
只进行编译而不进行类型检查
.css :style-loader
将 CSS 注入到 DOM 中,css-loader
处理 CSS 文件中的 @import
和 url()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 module : { rules : [ { test : /\.ts$/ , enforce : 'pre' , use : [ { loader : 'tslint-loader' } ] }, { test : /\.tsx?$/ , use : [ { loader : 'ts-loader' , options : { transpileOnly : true } } ] }, { test : /\.css$/ , use : ['style-loader' , 'css-loader' ] } ] },
resolve
指定在导入模块时可以省略的文件扩展名,Webpack 会自动尝试这些扩展名。
1 2 3 resolve : { extensions : ['.ts' , '.tsx' , '.js' ] }
plugins
热更新 | 发生错误不打包文件
1 2 3 4 plugins : [ new webpack.HotModuleReplacementPlugin (), new webpack.NoEmitOnErrorsPlugin (), ]
核心功能 处理 url 参数(params)
核心思想:把 params 的 key&value 解构拼接 到 URL 上
helpers/url.ts
解构 URL -> params 1 2 3 4 export function bulidURL (url : string , params ?: any ): string { ... return url }
需处理逻辑
没有参数
1 2 3 4 if (!params) { return url }
解构 params -> key/value params 是一个包含请求参数的对象
1 2 3 4 5 6 const params = { name : 'John' , age : 30 , hobbies : ['reading' , 'coding' ], }
具体代码
1 2 3 4 5 6 const parts : string [] = [] Object .keys (params).forEach (key => { const val = params[key] ... })
value 为 null 或 undefined
1 2 3 if (val === null || typeof val === 'undefined' ) { return }
value 为 数组
1 2 3 4 5 6 7 8 9 let values = [] if (Array .isArray (val)) { values = val key += '[]' } else { values = [val] }
value 为 Data | 字符串 | 对象
helpers/util.ts
1 2 3 4 5 6 7 const toString = Object .prototype .toString export function isDate (val : any ): val is Date { return toString.call (val) === '[object Date]' } export function isObject (val : any ): val is Object { return val !== null && typeof val === 'object' }
再回来
1 2 3 4 5 6 7 8 9 10 11 import { isDate, isObject } from './util' 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 , ']' ) }
1 2 3 4 5 6 7 8 9 10 11 values.forEach ((val ) => { if (isDate (val)) { val = val.toISOString () } else if (isObject (val)) { val = JSON .stringify (val) } parts.push (`${encode(key)} =${encode(val)} ` ) })
拼装成 URL
parts.join(‘&’) 效果
1 ;['name=John' , 'age=30' , 'hobbies[]=reading' , 'hobbies[]=coding' ]
1 'name=John&age=30&hobbies[]=reading&hobbies[]=coding'
1 2 3 4 5 6 7 8 9 let serializedParams = parts.join ('&' ) if (serializedParams) { const markIndex = url.indexOf ('#' ) if (markIndex !== -1 ) { url = url.slice (0 , markIndex) } url += (url.indexOf ('?' ) === -1 ? '?' : '&' ) + serializedParams }
处理 body 数据 helpers/util.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const toString = Object .prototype .toString export function isDate (val : any ): val is Date { return toString.call (val) === '[object Date]' } export function isPlainObject (val : any ): val is Object { return toString.call (val) === '[object Object]' }
helpers/data.ts
1 2 3 4 5 6 7 8 import { isPlainObject } from './util' export function transformRequest (data : any ): any { if (isPlainObject (data)) { return JSON .stringify (data) } return data }
index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function processConfig (config : AxiosRequestConfig ): void { config.url = transformURL (config) config.data = transformRequestData (config) } function transformURL (config : AxiosRequestConfig ): string { const { url, params } = config return bulidURL (url, params) } function transformRequestData (config : AxiosRequestConfig ): void { config.data = transformRequest (config.data ) }
headers 一般传递诸如认证信息、内容类型等元数据给服务器
helpers/header.ts
处理 headers
1 2 3 4 5 6 7 8 9 10 11 export function processHeaders (headers : any , data : any ): any { normalizeHeaderName (headers, 'Content-Type' ) if (isPlainObject (data)) { if (headers && !headers['Content-Type' ]) { headers['Content-Type' ] = 'application/json;charset=utf-8' } } }
Content-Type
是 HTTP 请求头的一部分,用于指示实体主体(即请求或响应的正文部分)的数据类型。它告诉接收方如何解析接收到的数据。例如,当发送 JSON 格式的数据时,通常会将 Content-Type
设置为 application/json
,这样接收方就知道应该以 JSON 的形式来解析数据
辅助函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function normalizeHeaderName (headers : any , normalizedName : string ): void { if (!headers) { return } Object .keys (headers).forEach ((name ) => { if ( name !== normalizedName && name.toUpperCase () === normalizedName.toUpperCase () ) { headers[normalizedName] = headers[name] delete headers[name] } }) return headers }
types/index.ts 加入 headers
1 2 3 4 5 6 7 export interface AxiosRequestConfig { url : string method ?: Method data ?: any params ?: any headers ?: any }
index.ts 增添处理逻辑
1 2 3 4 5 6 7 8 9 10 11 12 function processConfig (config : AxiosRequestConfig ): void { config.url = transformURL (config) config.headers = transformHeaders (config) config.data = transformRequestData (config) } function transformHeaders (config : AxiosRequestConfig ): any { const { headers = {}, data } = config return processHeaders (headers, data) }
xhr.ts 添加 header 处理逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export default function xhr (config : AxiosRequestConfig ): void { const { data = null , url, method = 'get' , headers } = config const request = new XMLHttpRequest () request.open (method.toUpperCase (), url, true ) Object .keys (headers).forEach ((name ) => { if (data === null && name.toLowerCase () === 'content-type' ) { delete headers[name] } else { request.setRequestHeader (name, headers[name]) } }) request.send (data) }
总结
这部分处理实现了
为 Content-Type 自动添加 application/json;charset=UTF-8(告知服务端解析的是 JSON 数据)
获取处理 response types / index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 export interface AxiosRequestConfig { url : string method ?: Method data ?: any params ?: any headers ?: any responseType ?: XMLHttpRequestResponseType } export interface AxiosResponse { data : any status : number statusText : string headers : any config : AxiosRequestConfig request : any } export interface AxiosPromise extends Promise <AxiosResponse > { }
xhr.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import { AxiosRequestConfig , AxiosPromise , AxiosResponse } from './types' export default function xhr (config : AxiosRequestConfig ): AxiosPromise { return new Promise ((resolve, reject ) => const { data = null , url, method = 'get' , headers, responseType } = config const request = new XMLHttpRequest () if (responseType) { request.responseType = responseType } request.open (method.toUpperCase (), url, true ) request.onreadystatechange = function handleLoad ( ) { if (request.readyState !== 4 ) { return } const responseHeaders = request.getAllResponseHeaders () const responseData = responseType && responseType !== 'text' ? request.response : request.responseText const response : AxiosResponse = { data : responseData, status : request.status , statusText : request.statusText , headers : responseHeaders, config, request } resolve (response) } Object .keys (headers).forEach (name => { if (data === null && name.toLowerCase () === 'content-type' ) { delete headers[name] } else { request.setRequestHeader (name, headers[name]) } }) request.send (data) }) }
index.ts
1 2 3 4 5 6 7 import { AxiosRequestConfig , AxiosPromise } from './types' function axios (config : AxiosRequestConfig ): AxiosPromise { processConfig (config) return xhr (config) }
getAllResponseHeaders
方法处理
XMLHttpRequest 对象的 getAllResponseHeaders 方法获取到的值是如下字符串
1 2 3 4 5 6 date: Fri, 05 Apr 2019 12 : 40 : 49 GMT etag: W/"d-Ssxx4FRxEutDLwo2+xkkxKc4y0k'' connection: keep-alive x-powered-by: Express content-length:13 content-type:application/json;charset=utf-8
每一行都是以回车和换行符\r\n
结束,是每个 header 属性的分隔符
而我们希望解析成的对象结构
1 2 3 4 5 6 7 8 { date: Fri, 05 Apr 2019 12 : 40 : 49 GMT etag: W/"d-Ssxx4FRxEutDLwo2+xkkxKc4y0k'', connection: keep-alive, x-powered-by: Express, content-length:13 content-type:application/json;charset=utf-8 }
headers.ts 新增 parseHeaders 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 export function parseHeaders (headers : string ): any { let parsed = Object .create (null ) if (!headers) { return parsed } headers.split ('\r\n' ).forEach ((line ) => { let [key, val] = line.split (':' ) key = key.trim ().toLowerCase () if (!key) { return } if (val) { val = val.trim () } parsed[key] = val }) return parsed }
处理响应 data 不设置 responseType 情况下,服务端返回数据是字符串类型
我们要转成对象
data.ts 新增 transformResponse 转化函数
1 2 3 4 5 6 7 8 9 10 export function transformResponse (data : any ): any { if (typeof data === 'string' ) { try { data = JSON .parse (data) } catch (e) { } } return data }
index.ts 处理 axios
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { transformRequest, transformResponse } from './helpers/data' function axios (config : AxiosRequestConfig ): AxiosPromise { processConfig (config) return xhr (config).then ((res ) => { return transformResponseData (res) }) } function transformResponseData (res : AxiosResponse ): AxiosResponse { res.data = transformResponse (res.data ) return res }
错误处理(reject 拒回) 处理网络错误 网络不通时发送请求会触发 XMLHttpRequest 对象实例的 error 事件,所以我们可以在 onerror 事件回调函数中捕获此类错误
xhr.ts 中添加
1 2 3 4 request.onerror = function handleError ( ) { reject (new Error ('Network Error' )) }
处理超时错误 设置一个 timeout,当请求超过某个时间后没收到响应则终止,并触发 timeout 事件
默认超时时间为 0(永不超时,我们首先需要允许程序可配置超时时间
type / index.ts
1 2 3 4 5 6 7 8 9 10 export interface AxiosRequestConfig { url : string method ?: Method data ?: any params ?: any headers ?: any responseType ?: XMLHttpRequestResponseType timeout ?: number }
xhr.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const { data = null , url, method = 'get' , headers, responseType, timeout, } = config if (timeout) { request.timeout = timeout } request.ontimeout = function handleTimeout ( ) { reject (new Error (`Timeout of ${timeout} ms exceeded` )) }
状态码处理(!200 错误)
处理时机:接收到响应的时候
xhr.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 request.onreadystatechange = function handleLoad ( ) { if (request.readyState !== 4 ) { return } if (request.status === 0 ) { return } resolve (response) } function handleResponse (response : AxiosResponse ): void { if (response.status >= 200 && response.status < 300 ) { resolve (response) } else { reject (new Error (`Request failed with status code ${response.status} ` )) } }
错误信息增强
按照之前的操作,我们实现了对基础错误信息的处理,但我们对外提供的错误信息是十分有限的
我们希望对外提供的错误信息包括
错误文本信息(报错显示)
请求对象配置 config
错误代码 code
XMLHttpRequest 对象实例 request
自定义响应对象 response
创建 AxiosError 类 types / index.ts 定义接口
1 2 3 4 5 6 7 8 export interface AxiosError extends Error { isAxiosError : boolean config : AxiosRequestConfig code ?: string request ?: any response ?: AxiosResponse }
新建 helpers / error.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import { AxiosRequestConfig , AxiosResponse } from '../types' export class AxiosError extends Error { isAxiosError : boolean config : AxiosRequestConfig code ?: string | null request ?: any response ?: AxiosResponse constructor ( message : string , config : AxiosRequestConfig , code ?: string | null , request ?: any , response ?: AxiosResponse ) { super (message) this .config = config this .code = code this .request = request this .response = response this .isAxiosError = true Object .setPrototypeOf (this , AxiosError .prototype ) } } export function createError ( message : string , config : AxiosRequestConfig , code ?: string | null , request ?: any , response ?: AxiosResponse ) { const error = new AxiosError (message, config, code, request, response) return error }
createError 方法应用
对错误对象创建的逻辑进行修改
xhr.ts
onerror 修改
1 2 3 request.onerror = function handleError ( ) { reject (new Error ('Network Error' )) }
替换为
1 2 3 request.onerror = function handleError ( ) { reject (createError ('Network Error' , config, null , request)) }
ontimeout 修改
1 2 3 4 request.ontimeout = function handleTimeout ( ) { reject (new Error (`Timeout of ${timeout} ms exceeded` )) }
替换为
1 2 3 4 5 6 7 8 9 10 11 request.ontimeout = function handleTimeout ( ) { reject ( createError ( `Timeout of ${timeout} ms exceeded` , config, 'ECONNABORTED' , request ) ) }
handleResponse 修改
1 2 3 4 5 6 7 8 9 function handleResponse (response : AxiosResponse ): void { if (response.status >= 200 && response.status < 300 ) { resolve (response) } else { reject (new Error (`Request failed with status code ${response.status} ` )) } }
替换为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function handleResponse (response : AxiosResponse ): void { if (response.status >= 200 && response.status < 300 ) { resolve (response) } else { reject ( createError ( `Request failed with status code ${response.status} ` , config, null , request, response ) ) } }
接口扩展 需求分析
为了用户更加方便地使用 axios 发送请求,我们可以为所有支持的请求方法扩展一些接口
1 2 3 4 5 6 7 8 9 axios.request (config) axios.get (url[,config]) axios.delete (url[,config]) axios.head (url[,config]) axios.options (url[,config]) axios.post (url[,datal,config]) axios.put (url[,datal,config]) axios.patch (url[,datal,config])
types / index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 } export interface AxiosInstance extends Axios { (config : AxiosRequestConfig ): AxiosPromise }
创建 Axios.ts
存放一些发送请求核心代码
core / Axios.ts //大写表示是一个类
1 2 3 4 5 6 7 import { AxiosRequestConfig , AxiosPromise } from '../types' export default class Axios { request (config : AxiosRequestConfig ): AxiosPromise { } }
core / dispatchRequest.ts
封装 axios.ts 核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import { AxiosRequestConfig , AxiosPromise , AxiosResponse } from '../types' import xhr from '../xhr' import { bulidURL } from '../helpers/url' import { transformRequest, transformResponse } from '../helpers/data' import { processHeaders } from '../helpers/header' export default function dispatchRequest ( config : AxiosRequestConfig ): AxiosPromise { processConfig (config) return xhr (config).then ((res ) => { return transformResponseData (res) }) } function processConfig (config : AxiosRequestConfig ): void { config.url = transformURL (config) config.headers = transformHeaders (config) config.data = transformRequestData (config) } function transformURL (config : AxiosRequestConfig ): string { const { url, params } = config return bulidURL (url, params) } function transformRequestData (config : AxiosRequestConfig ): void { config.data = transformRequest (config.data ) } function transformHeaders (config : AxiosRequestConfig ): any { const { headers = {}, data } = config return processHeaders (headers, data) } function transformResponseData (res : AxiosResponse ): AxiosResponse { res.data = transformResponse (res.data ) return res }
然后将 xhr.ts 移入 core 文件夹(xhr 也作为核心模块)
部分报错修改 (严格模式)
1 2 3 4 5 return bulidURL (url!, params)request.open (method.toUpperCase (), url!, true )
Tips: 断言( ! )
TS 中,当你保证某个变量不会为 null
或者 undefined
,但编译器却无法推断出来的时候,就可以使用非空断言操作符 ( ! ),不会在运行时进行检查,但要是你做出了错误的保证,在运行时就可能会出现错误。
实现 Axios 类中接口 Axios.ts
1 2 3 4 5 6 7 import { AxiosRequestConfig , AxiosPromise } from '../types' import dispatchRequest from './dispatchRequest' export default class Axios { request (...) get (...) }
实现 request 接口
1 2 3 request (config : AxiosRequestConfig ): AxiosPromise { return dispatchRequest (config) }
逻辑封装
1 2 3 4 5 6 7 8 _requestMethodWithoutData ( method : Method , url : string , config ?: AxiosRequestConfig ): AxiosPromise { return this .request (Object .assign (config || {}, { method, url })) }
实现 get 接口
1 2 3 get (url : string , config ?: AxiosRequestConfig ): AxiosPromise { return this ._requestMethodWithoutData ('get' , url, config) }
实现 delete 接口
1 2 3 delete (url : string , config ?: AxiosRequestConfig ): AxiosPromise { return this ._requestMethodWithoutData ('delete' , url, config) }
实现 head 接口
1 2 3 head (url : string , config ?: AxiosRequestConfig ): AxiosPromise { return this ._requestMethodWithoutData ('head' , url, config) }
实现 options 接口
1 2 3 options (url : string , config ?: AxiosRequestConfig ): AxiosPromise { return this ._requestMethodWithoutData ('options' , url, config) }
逻辑封装
1 2 3 4 5 6 7 8 9 _requestMethodWithData ( method : Method , url : string , data ?: any , config ?: AxiosRequestConfig ): AxiosPromise { return this .request (Object .assign (config || {}, { method, url, data })) }
实现 post 接口
1 2 3 head (url : string , config ?: AxiosRequestConfig ): AxiosPromise { return this ._requestMethodWithData ('post' , url, data, config) }
实现 put 接口
1 2 3 put (url : string , config ?: AxiosRequestConfig ): AxiosPromise { return this ._requestMethodWithData ('put' , url, data, config) }
实现 patch 接口
1 2 3 patch (url : string , config ?: AxiosRequestConfig ): AxiosPromise { return this ._requestMethodWithData ('patch' , url, data, config) }
混合对象实现
首先这个对象是函数,其次这个对象包括 Axios 类内所有原型属性和实例属性
helpers / utils
extend
合成函数
1 2 3 4 5 const target = { a : 1 }const source = { b : 2 }const result = extend (target, source)console .log (result)
这里我们使用**泛型 ** T
和 U
T
代表目标对象 to
的类型,U
代表源对象 from
的类型
函数的返回值类型是 T & U
,也就是 to
和 from
两个对象类型的交集。
1 2 3 4 5 6 7 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 }
工厂函数实现 处理 axios.ts
删掉内容,只保留 export default axios
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { AxiosInstance } from './types' import Axios from './core/Axios' import { extend } from './helpers/util' function createInstance ( ): AxiosInstance { const context = new Axios () const instance = Axios .prototype .request .bind (context) extend (instance, context) return instance as AxiosInstance } const axios = createInstance () export default axios
axios 函数重载
目前我们函数只能存放一个对象
1 2 3 4 5 6 7 axios({ url: '/extend/post', method: 'post', data:{ msg:'hi' } })
那么如果 axios 同时含有两个参数
1 2 3 4 5 6 axios('/extend/post',{ method: 'post', data:{ msg:'hi' } })
type / index.ts
1 2 3 4 export interface AxiosInstance extends Axios { (config : AxiosRequestConfig ): AxiosPromis ) (url : string , config ?: AxiosRequestConfig ): AxiosPromise }
core / Axios.ts
修改 request 方法
1 2 3 request (config : AxiosRequestConfig ): AxiosPromise { return dispatchRequest (config) }
改为 👇
1 2 3 4 5 6 7 8 9 10 11 request (url : any , config : any ): AxiosPromise { if (typeof url === 'string' ) { if (!config) { config = {} } config.url = url } else { config = url } return dispatchRequest (config) }
响应数据支持泛型
通常情况,我们会将后端返回数据格式单独放入一个接口中
设置泛型,提高类型灵活性
type / index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export interface AxiosResponse <T = any > { data : T status : number statusText : string headers : any config : AxiosRequestConfig request : any } export interface AxiosPromise <T = any > extends Promise <AxiosResponse <T>> { } export interface Axios { request<T = any >(config : AxiosRequestConfig ): AxiosPromise <T> get<T = any >(url : string , config ?: AxiosRequestConfig ): AxiosPromise <T> ...... } export interface AxiosInstance extends Axios { <T = any >(config : AxiosRequestConfig ): AxiosPromise <T> <T = any >(url : string , config ?: AxiosRequestConfig ): AxiosPromise <T> }
拦截器实现
也就是 axios 二次封装中常见的请求拦截器 & 响应拦截器
我们先回顾一下用户 asios 拦截器是怎么写的
1 2 3 4 5 6 7 8 9 10 11 12 13 import axios from 'axios' const instance = axios.create ({ baseURL :'/api }) instance.interceptors.request.use(res =>{//请求拦截器 console.log(' res',res) }) instance.interceptors.response.use(res =>{//响应拦截器 console.log(' res',res) }) export default instance
Tip:request 是后添加的先执行,response 是先添加的先执行
拦截器管理类 types / index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export interface AxiosInterceptorManager <T> { use (resolved : ResolvedFn <T>, rejected ?: RejectedFn ): number eject (id : number ): void } export interface ResolvedFn <T = any > { (val : T): T | Promise <T> } export interface RejectedFn { (error : any ): any }
core / interceptorManager.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import { ResolvedFn , RejectedFn } from '../types' interface Interceptor <T> { resolved : ResolvedFn <T> rejected ?: RejectedFn } export default class InterceptorManager <T> { private interceptors : Array <Interceptor <T>> constructor ( ) { this .interceptors = [] } use (resolved : ResolvedFn <T>, rejected ?: RejectedFn ): number { this .interceptors .push ({ resolved, rejected, }) return this .interceptors .length - 1 } forEach (fn : (interceptor : Interceptor <T> ) => void ): void { this .interceptors .forEach ((interceptor ) => { if (interceptor !== null ) { fn (interceptor) } }) } eject (id : number ): void { if (this .interceptors [id]) { this .interceptors [id] = null } } }
链式调用实现 core / Axios.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { AxiosRequestConfig , AxiosPromise ,AxiosResponse , Method } from '../types' import InterceptorManager from './interceptorManager' interface Interceptors { request : InterceptorManager <AxiosRequestConfig > response : InterceptorManager <AxiosResponse > } export default class Axios { interceptors : Interceptors constructor ( ) { this .interceptors = { request : new InterceptorManager <AxiosRequestConfig >(), response : new InterceptorManager <AxiosResponse >() } } request (...)... ... }
完善 request (实现调用链)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import { AxiosRequestConfig , AxiosPromise , AxiosResponse , Method ,ResolvedFn ,RejectedFn } from '../types' interface PromiseChain <T> { resolved : ResolvedFn <T> | ((config : AxiosRequestConfig ) => AxiosPromise ) rejected ?: RejectedFn } ... request (url : any , config : any ): AxiosPromise { if (typeof url === 'string' ) { if (!config) { config = {} } config.url = url } else { config = url } const chain :PromiseChain <any >[] = [ { resolved : dispatchRequest, 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 dispatchRequest (config) }
合并用户配置
用户在发送请求时可以传入一个配置来决定请求的不同行为
甚至可以直接修改一些默认配置 (defaults)
1 2 3 4 axios.defaults .headers .common ['test' ] = 123 axios.defaults .headers .post ['Content-Type' ] = 'application/x-www-form-urlencoded' axios.defaults .timeout = 2000
默认配置设置 defaults.ts 配置默认请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import { AxiosRequestConfig } from './types' const defaults : AxiosRequestConfig = { url : '' , method : 'get' , timeout : 0 , headers : { common : { Accept : 'application/json, text/plain, */*' , }, }, } const methodsNoData = ['delete' , 'get' , 'head' , 'options' ]methodsNoData.forEach ((method ) => { defaults.headers [method] = {} }) const methodsWithData = ['post' , 'put' , 'patch' ]methodsWithData.forEach ((method ) => { defaults.headers [method] = { 'Content-Type' : 'application/x-www-form-urlencoded' , } }) export default defaults
core / Axios.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export default class Axios { defaults : AxiosRequestConfig interceptors : Interceptors constructor (initConfig ?: AxiosRequestConfig ) { this .defaults = initConfig this .interceptors = { request : new InterceptorManager <AxiosRequestConfig >(), response : new InterceptorManager <AxiosResponse >() } } ... }
types / index.ts
1 2 3 4 5 6 7 8 export interface Axios { defaults : AxiosRequestConfig interceptors : { request : AxiosInterceptorManager <AxiosRequestConfig > response : AxiosInterceptorManager <AxiosResponse > } ... }
重写 axios.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { AxiosInstance , AxiosRequestConfig } from './types' import Axios from './core/Axios' import { extend } from './helpers/util' import defaults from './defaults' function createInstance (config : AxiosRequestConfig ): AxiosInstance { const context = new Axios (config) const instance = Axios .prototype .request .bind (context) extend (instance, context) return instance as AxiosInstance } const axios = createInstance (defaults) export default axios
合并策略实现 创建 core / mergeConfig.ts
config1
为默认配置
config2
代表用户传入的自定义配置
实现示例 (假设 config1
和 config2
如下:)
1 2 3 4 5 6 7 8 9 10 11 12 13 const config1 = { timeout : 1000 , headers : { 'Content-Type' : 'application/json' , }, } const config2 = { timeout : 2000 , headers : { Authorization : 'Bearer token' , }, }
合并 timeout
属性时,val1
就是 config1.timeout
的值 1000
,val2
就是 config2.timeout
的值 2000
。
timeout 逻辑 mergeConfig.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import { AxiosRequestConfig } from '../types' const strats = Object .create (null ) function defaultStrat (val1 : any , val2 : any ): any { return typeof val2 !== 'undefined' ? val2 : val1 } function fromVal2Strat (val1 : any , val2 : any ): any { if (typeof val2 !== 'undefined' ) { return val2 } } const stratKeysFromVal2 = ['url' , 'params' , 'data' ]stratKeysFromVal2.forEach ((key ) => { strats[key] = fromVal2Strat }) export default function mergeConfig ( config1 : AxiosRequestConfig , config2 ?: AxiosRequestConfig ): AxiosRequestConfig { if (!config2) { config2 = {} } const config = Object .create (null ) for (let key in config2) { mergeField (key) } for (let key in config1) { if (!config2[key]) { mergeField (key) } } function mergeField (key : string ): void { const strat = strats[key] || defaultStrat config[key] = strat (config1[key], config2![key]) } return config }
types / index.ts 添加自动索引
1 2 3 4 export interface AxiosRequestConfig { ... [propName : string ]: any }
合并 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function deepMergeStrat (val1 : any , val2 : any ): any { if (isPlainObject (val2)) { return deepMerge (val1, val2) } else if (typeof val2 !== 'undefined' ) { return val2 } else if (isPlainObject (val1)) { return deepMerge (val1) } else if (typeof val1 !== 'undefined' ) { return val1 } }
deepMerge 工具函数实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 export function deepMerge (...objs : any [] ): any { const result = Object .create (null ) objs.forEach ((obj ) => { if (obj) { Object .keys (obj).forEach ((key ) => { const val = obj[key] if (isPlainObject (val)) { if (isPlainObject (result[key])) { result[key] = deepMerge (result[key], val) } else { result[key] = deepMerge (val) } } else { result[key] = val } }) } }) return result }
在请求中应用函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 request (url : any , config : any ): AxiosPromise { 。。。 if (typeof url === 'string' ) { if (!config) { config = {} } config.url = url } else { config = url } config = this .mergeConfig (this .defaults ,config) const chain :PromiseChain <any >[] = [ { resolved : dispatchRequest, rejected : undefined } ] 。。。 }
压缩
合并后的 headers 是一个复杂对象,多了 common、post、get 等属性
1 2 3 4 5 6 7 8 headers:{ common:{ Accept:'application/json,text/plain,*/*' }, post: { 'Content-Type':'application/x-www-form-urlencoded' } }
我们需要压缩成一级
1 2 3 4 headers:{ Accept:'application/json,text/plain,*/*', 'Content-Type':'application/x-www-form-urlencoded' }
Tip:对于 comman 中定义的 header 字段,我们都要提取,而对于 post、get 这类提取,需要和该次请求的方法对应。
headers.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 export function flattenHeaders (headers : any , method : Method ): any { if (!headers) { return headers } headers = deepMerge (headers, null ) const methodsToDelete = [ 'delete' , 'get' , 'head' , 'options' , 'post' , 'put' , 'patch' , 'common' , ] methodsToDelete.forEach ((method ) => { delete headers[method] }) return headers }
dispatchRequest.ts 应用函数
1 2 3 4 5 6 7 function processConfig (config : AxiosRequestConfig ): void { config.url = transformURL (config) config.headers = transformHeaders (config) config.data = transformRequestData (config) config.headers = flattenHeaders (config.headers , config.method !) }
更多配置 transfromRequest & transfromResponse
官方 axios 中提供transfromRequest
&transfromResponse
,值为**一个数组或一个函数 **
这两个属性允许用户在 数据发送到服务器之前 (只适用于 put、post、patch,而且可以修改 headers 对象) / 把响应数据传递给 then 或 catch 之前 对其修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 axios({ transformRequest: [ (function(data) { return qs.stringify(data); // 自定义的 transformRequest }), ...axios.defaults.transformRequest // 保留默认的 transformRequest ], transformResponse: [ ...axios.defaults.transformResponse, // 展开默认的 transformResponse function(data) { if (typeof data === 'object') { // 自定义的 transformResponse data.b = 2; // 修改响应数据 } return data; } ], url: '/config/post', // 请求的 URL method: 'post', // 请求方法 data: { a: 1 // 请求的数据 } });
定义接口 types / index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export interface AxiosRequestConfig { url : string method ?: Method data ?: any params ?: any headers ?: any responseType ?: XMLHttpRequestResponseType timeout ?: number transformRequest ?: AxiosTransformer | AxiosTransformer [] transformResponse ?: AxiosTransformer | AxiosTransformer [] [propName : string ]: any } export interface AxiosTransformer { (data : any , headers ?: any ): any }
defaults.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { processHeaders } from './helpers/header' import { transformRequest, transformResponse } from './helpers/data' const defaults : AxiosRequestConfig = { url : '' , method : 'get' , timeout : 0 , headers : { common : { Accept : 'application/json, text/plain, */*' , }, }, transformRequest : [ function (data : any , headers : any ): any { processHeaders (headers, data) return transformRequest (data) }, ], transformResponse : [ function (data : any ): any { return transformResponse (data) }, ], }
core / transform.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { AxiosTransformer } from '../types' export default function transform ( data : any , headers : any , fns ?: AxiosTransformer | AxiosTransformer [] ): any { if (!fns) { return data } if (!Array .isArray (fns)) { fns = [fns] } fns.forEach ((fn ) => { data = fn (data, headers) }) return data }
core / dispatchRequest.ts
1 import transform from './transform'
1 2 3 4 5 6 7 function processConfig (config : AxiosRequestConfig ): void { config.url = transformURL (config) config.headers = transformHeaders (config) config.data = transformRequestData (config) config.headers = flattenHeaders (config.headers , config.method !) }
更改为 👇
1 2 3 4 5 6 function processConfig (config : AxiosRequestConfig ): void { config.url = transformURL (config) config.data = transform (config.data , config.headers , config.transformRequest ) config.headers = flattenHeaders (config.headers , config.method !) }
删掉不用的东西
1 2 3 4 5 6 7 8 9 10 11 12 import { transformRequest, transformResponse } from '../helpers/data' function transformRequestData (config : AxiosRequestConfig ): void { config.data = transformRequest (config.data ) } function transformHeaders (config : AxiosRequestConfig ): any { const { headers = {}, data } = config return processHeaders (headers, data) }
更改逻辑
1 2 3 4 5 function transformResponseData (res : AxiosResponse ): AxiosResponse { res.data = transform (res.data , res.headers , res.config .transformResponse ) return res }
create
目前为止,我们的 axios 都是一个单例,一旦我们修改了 axios 的默认配置,会影响所有的请求。我们希望提供了一个axios.create
的静态接口允许我们创建一个新的 axios 实例,同时允许我们传入新的配置和默认配置合并,并做为新的默认配置。
扩展新的接口 types / index.ts
1 2 3 4 export interface AxiosStatic extends AxiosInstance { create (config ?: AxiosRequestConfig ): AxiosInstance }
axios.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { AxiosRequestConfig , AxiosStatic } from './types' import Axios from './core/Axios' import { extend } from './helpers/util' import defaults from './defaults' import mergeConfig from './core/mergeConfig' function createInstance (config : AxiosRequestConfig ): AxiosStatic { const context = new Axios (config) const instance = Axios .prototype .request .bind (context) extend (instance, context) return instance as AxiosStatic } const axios = createInstance (defaults) axios.create = function create (config ) { return createInstance (mergeConfig (defaults, config)) } export default axios