前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于TypeScript封装Axios笔记(七)

基于TypeScript封装Axios笔记(七)

作者头像
用户7572539
发布2020-08-26 10:31:46
1.7K0
发布2020-08-26 10:31:46
举报
文章被收录于专栏:二少爷的花间集
合并配置的设计与实现

需求分析

在发送请求的时候可以传入一个配置,来决定请求的不同行为。我们也希望 ts-axios 可以有默认配置,定义一些默认的行为。这样在发送每个请求,用户传递的配置可以和默认配置做一层合并。

和官网 axios 库保持一致,我们给 axios 对象添加一个 defaults 属性,表示默认配置,你甚至可以直接修改这些默认配置:‍

代码语言:javascript
复制
1axios.defaults.headers.common['test'] = 123
2axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
3axios.defaults.timeout = 2000

其中对于 headers 的默认配置支持 common 和一些请求 method 字段,common 表示对于任何类型的请求都要添加该属性,而 method 表示只有该类型请求方法才会添加对应的属性。

在上述例子中,我们会默认为所有请求的 header 添加 test 属性,会默认为 post 请求的 header 添加 Content-Type 属性。

默认配置

默认配置定义

接下来,我们先实现默认配置

defaults.ts:

代码语言:javascript
复制
 1import { AxiosRequestConfig } from './types'
 2
 3const defaults: AxiosRequestConfig = {
 4 method: 'get',
 5
 6 timeout: 0,
 7
 8 headers: {
 9 common: {
10 Accept: 'application/json, text/plain, */*'
11    }
12  }
13}
14
15const methodsNoData = ['delete', 'get', 'head', 'options']
16
17methodsNoData.forEach(method => {
18  defaults.headers[method] = {}
19})
20
21const methodsWithData = ['post', 'put', 'patch']
22
23methodsWithData.forEach(method => {
24  defaults.headers[method] = {
25 'Content-Type': 'application/x-www-form-urlencoded'
26  }
27})
28
29export default defaults

我们定义了 defaults 常量,它包含默认请求的方法、超时时间,以及 headers 配置。

未来我们会根据新的需求添加更多的默认配置。

添加到 axios 对象中

根据需求,我们要给 axios 对象添加一个 defaults 属性,表示默认配置:

代码语言:javascript
复制
 1export default class Axios {
 2  defaults: AxiosRequestConfig
 3  interceptors: Interceptors
 4
 5 constructor(initConfig: AxiosRequestConfig) {
 6 this.defaults = initConfig
 7 this.interceptors = {
 8 request: new InterceptorManager<AxiosRequestConfig>(),
 9 response: new InterceptorManager<AxiosResponse>()
10    }
11  }
12 // ...
13}  

我们给 Axios 类添加一个 defaults 成员属性,并且让 Axios 的构造函数接受一个 initConfig 对象,把 initConfig 赋值给 this.defaults。

接着修改 createInstance 方法,支持传入 config 对象。

代码语言:javascript
复制
 1import defaults from './defaults'
 2
 3function createInstance(config: AxiosRequestConfig): AxiosStatic {
 4 const context = new Axios(config)
 5 const instance = Axios.prototype.request.bind(context)
 6
 7 // extend(instance, Axios.prototype, context)
 8
 9  extend(instance, context)
10
11 return instance as AxiosStatic
12}
13
14const axios = createInstance(defaults)

这样我们就可以在执行 createInstance 创建 axios 对象的时候,把默认配置传入了。

配置合并及策略

定义了默认配置后,我们发送每个请求的时候需要把自定义配置和默认配置做合并,它并不是简单的 2 个普通对象的合并,对于不同的字段合并,会有不同的合并策略。举个例子:

代码语言:javascript
复制
 1config1 = {
 2  method: 'get',
 3
 4  timeout: 0,
 5
 6  headers: {
 7    common: {
 8      Accept: 'application/json, text/plain, */*'
 9    }
10  }
11}
12
13config2 = {
14  url: '/config/post',
15  method: 'post',
16  data: {
17    a: 1
18  },
19  headers: {
20 test: '321'
21  }
22}
23
24merged = {
25  url: '/config/post',
26  method: 'post',
27  data: {
28    a: 1
29  },
30  timeout: 0,
31  headers: {
32    common: {
33      Accept: 'application/json, text/plain, */*'
34    }
35 test: '321'
36  }
37}

我们在 core/mergeConfig.ts 中实现合并方法。

合并方法

代码语言:javascript
复制
 1export default function mergeConfig(
 2  config1: AxiosRequestConfig,
 3  config2?: AxiosRequestConfig
 4): AxiosRequestConfig {
 5 if (!config2) {
 6    config2 = {}
 7  }
 8
 9 const config = Object.create(null)
10
11 for (let key in config2) {
12    mergeField(key)
13  }
14
15 for (let key in config1) {
16 if (!config2[key]) {
17      mergeField(key)
18    }
19  }
20
21 function mergeField(key: string): void {
22 const strat = strats[key] || defaultStrat
23    config[key] = strat(config1[key], config2![key])
24  }
25
26 return config
27}

合并方法的整体思路就是对 config1 和 config2 中的属性遍历,执行 mergeField 方法做合并,这里 config1 代表默认配置,config2 代表自定义配置。

遍历过程中,我们会通过 config2[key] 这种索引的方式访问,所以需要给 AxiosRequestConfig 的接口定义添加一个字符串索引签名:‍

代码语言:javascript
复制
1export interface AxiosRequestConfig {
2 // ...
3
4  [propName: string]: any
5}

在 mergeField 方法中,我们会针对不同的属性使用不同的合并策略。

默认合并策略

这是大部分属性的合并策略,如下:

代码语言:javascript
复制
1function defaultStrat(val1: any, val2: any): any {
2 return typeof val2 !== 'undefined' ? val2 : val1
3}

它很简单,如果有 val2 则返回 val2,否则返回 val1,也就是如果自定义配置中定义了某个属性,就采用自定义的,否则就用默认配置。

只接受自定义配置合并策略

对于一些属性如 url、params、data,合并策略如下:

代码语言:javascript
复制
 1function fromVal2Strat(val1: any, val2: any): any {
 2 if (typeof val2 !== 'undefined') {
 3 return val2
 4  }
 5}
 6
 7const stratKeysFromVal2 = ['url', 'params', 'data']
 8
 9stratKeysFromVal2.forEach(key => {
10  strats[key] = fromVal2Strat
11})

因为对于 url、params、data 这些属性,默认配置显然是没有意义的,它们是和每个请求强相关的,所以我们只从自定义配置中获取。

复杂对象合并策略

对于一些属性如 headers,合并策略如下:

代码语言:javascript
复制
 1function deepMergeStrat(val1: any, val2: any): any {
 2 if (isPlainObject(val2)) {
 3 return deepMerge(val1, val2)
 4  } else if (typeof val2 !== 'undefined') {
 5 return val2
 6  } else if (isPlainObject(val1)) {
 7 return deepMerge(val1)
 8  } else if (typeof val1 !== 'undefined') {
 9 return val1
10  }
11}
12
13const stratKeysDeepMerge = ['headers']
14
15stratKeysDeepMerge.forEach(key => {
16  strats[key] = deepMergeStrat
17})

helpers/util.ts:

代码语言:javascript
复制
 1export function deepMerge(...objs: any[]): any {
 2 const result = Object.create(null)
 3
 4  objs.forEach(obj => {
 5 if (obj) {
 6 Object.keys(obj).forEach(key => {
 7 const val = obj[key]
 8 if (isPlainObject(val)) {
 9 if (isPlainObject(result[key])) {
10            result[key] = deepMerge(result[key], val)
11          } else {
12            result[key] = deepMerge({}, val)
13          }
14        } else {
15          result[key] = val
16        }
17      })
18    }
19  })
20
21 return result
22}

对于 headers 这类的复杂对象属性,我们需要使用深拷贝的方式,同时也处理了其它一些情况,因为它们也可能是一个非对象的普通值。未来我们讲到认证授权的时候,auth 属性也是这个合并策略。

最后我们在 request 方法里添加合并配置的逻辑:

1config = mergeConfig(this.defaults, config)

flatten headers

经过合并后的配置中的 headers 是一个复杂对象,多了 common、post、get 等属性,而这些属性中的值才是我们要真正添加到请求 header 中的。

举个例子:

代码语言:javascript
复制
1headers: {
2  common: {
3    Accept: 'application/json, text/plain, */*'
4  },
5  post: {
6 'Content-Type':'application/x-www-form-urlencoded'
7  }
8}

我们需要把它压成一级的,如下:

代码语言:javascript
复制
1headers: {
2 Accept: 'application/json, text/plain, */*',
3 'Content-Type':'application/x-www-form-urlencoded'
4}

这里要注意的是,对于 common 中定义的 header 字段,我们都要提取,而对于 post、get 这类提取,需要和该次请求的方法对应。

接下来我们实现 flattenHeaders 方法。

helpers/header.ts:

代码语言:javascript
复制
 1export function flattenHeaders(headers: any, method: Method): any {
 2 if (!headers) {
 3 return headers
 4  }
 5  headers = deepMerge(headers.common || {}, headers[method] || {}, headers)
 6
 7 const methodsToDelete = ['delete', 'get', 'head', 'options', 'post', 'put', 'patch', 'common']
 8
 9  methodsToDelete.forEach(method => {
10 delete headers[method]
11  })
12
13 return headers
14}

我们可以通过 deepMerge 的方式把 common、post 的属性拷贝到 headers 这一级,然后再把 common、post 这些属性删掉。

然后我们在真正发送请求前执行这个逻辑。

core/dispatchRequest.ts:

代码语言:javascript
复制
1function processConfig(config: AxiosRequestConfig): void {
2 config.url = transformURL(config)
3 config.headers = transformHeaders(config)
4 config.data = transformRequestData(config)
5 config.headers = flattenHeaders(config.headers, config.method!)
6}

这样确保我们了配置中的 headers 是可以正确添加到请求 header 中的

demo 编写

在 examples 目录下创建 config 目录,在 config 目录下创建 index.html:

代码语言:javascript
复制
 1<!DOCTYPE html>
 2<html lang="en">
 3 <head>
 4 <meta charset="utf-8">
 5 <title>Config example</title>
 6 </head>
 7 <body>
 8 <script src="/__build__/config.js"></script>
 9 </body>
10</html>

接着创建 app.ts 作为入口文件:

代码语言:javascript
复制
 1import axios from '../../src/index'
 2import qs from 'qs'
 3
 4axios.defaults.headers.common['test2'] = 123
 5
 6axios({
 7  url: '/config/post',
 8  method: 'post',
 9  data: qs.stringify({
10    a: 1
11  }),
12  headers: {
13    test: '321'
14  }
15}).then((res) => {
16 console.log(res.data)
17})

这个例子中我们额外引入了 qs 库,它是一个查询字符串解析和字符串化的库。

比如我们的例子中对于 {a:1} 经过 qs.stringify 变成 a=1。

由于我们的例子给默认值添加了 post 和 common 的 headers,我们在请求前做配置合并,于是我们请求的 header 就添加了 Content-Type 字段,它的值是 application/x-www-form-urlencoded;另外我们也添加了 test2 字段,它的值是 123。

至此,我们合并配置的逻辑就实现完了。我们在前面的章节编写 axios 的基础功能的时候对请求数据和响应数据都做了处理,官方 axios 则把这俩部分逻辑也做到了默认配置中,意味这用户可以去修改这俩部分的逻辑,实现自己对请求和响应数据处理的逻辑。我们就来实现这个 feature。

请求和响应配置化

需求分析

官方的 axios 库 给默认配置添加了 transformRequest 和 transformResponse 两个字段,它们的值是一个数组或者是一个函数。

其中 transformRequest 允许你在将请求数据发送到服务器之前对其进行修改,这只适用于请求方法 put、post 和 patch,如果值是数组,则数组中的最后一个函数必须返回一个字符串或 FormData、URLSearchParams、Blob 等类型作为 xhr.send 方法的参数,而且在 transform 过程中可以修改 headers 对象。

而 transformResponse 允许你在把响应数据传递给 then 或者 catch 之前对它们进行修改。

当值为数组的时候,数组的每一个函数都是一个转换函数,数组中的函数就像管道一样依次执行,前者的输出作为后者的输入。‍

举个例子:

代码语言:javascript
复制
 1axios({
 2  transformRequest: [(function(data) {
 3 return qs.stringify(data)
 4  }), ...axios.defaults.transformRequest],
 5  transformResponse: [axios.defaults.transformResponse, function(data) {
 6 if (typeof data === 'object') {
 7 data.b = 2
 8    }
 9 return data
10  }],
11  url: '/config/post',
12  method: 'post',
13 data: {
14    a: 1
15  }
16})

修改默认配置

先修改 AxiosRequestConfig 的类型定义,添加 transformRequest 和 transformResponse 俩个可选属性。

types/index.ts:

代码语言:javascript
复制
1export interface AxiosRequestConfig {
2 // ...
3  transformRequest?: AxiosTransformer | AxiosTransformer[]
4  transformResponse?: AxiosTransformer | AxiosTransformer[]
5}
6
7export interface AxiosTransformer {
8  (data: any, headers?: any): any
9}

接着修改默认配置,如下:

defaults.ts:

代码语言:javascript
复制
 1import { processHeaders } from './helpers/headers'
 2import { transformRequest, transformResponse } from './helpers/data'
 3
 4const defaults: AxiosRequestConfig = {
 5 // ...
 6  transformRequest: [
 7 function(data: any, headers: any): any {
 8      processHeaders(headers, data)
 9 return transformRequest(data)
10    }
11  ],
12
13 transformResponse: [
14 function(data: any): any {
15 return transformResponse(data)
16    }
17  ]
18}

我们把之前对请求数据和响应数据的处理逻辑,放到了默认配置中,也就是默认处理逻辑。

transform 逻辑重构

接下来,我们就要重构之前写的对请求数据和响应数据的处理逻辑了。由于我们可能会编写多个转换函数,我们先定义一个 transform 函数来处理这些转换函数的调用逻辑。

core/transform.ts

代码语言:javascript
复制
 1import { AxiosTransformer } from '../types'
 2
 3export default function transform(
 4  data: any,
 5  headers: any,
 6  fns?: AxiosTransformer | AxiosTransformer[]
 7): any {
 8 if (!fns) {
 9 return data
10  }
11 if (!Array.isArray(fns)) {
12    fns = [fns]
13  }
14  fns.forEach(fn => {
15    data = fn(data, headers)
16  })
17 return data
18}

transform 函数中接收 data、headers、fns 3 个参数,其中 fns 代表一个或者多个转换函数,内部逻辑很简单,遍历 fns,执行这些转换函数,并且把 data 和 headers 作为参数传入,每个转换函数返回的 data 会作为下一个转换函数的参数 data 传入。

接下来修改对请求数据和响应数据的处理逻辑。

dispatchRequest.ts:

代码语言:javascript
复制
 1import transform from './transform'
 2
 3function processConfig(config: AxiosRequestConfig): void {
 4 config.url = transformURL(config)
 5 config.data = transform(config.data, config.headers, config.transformRequest)
 6 config.headers = flattenHeaders(config.headers, config.method!)
 7}
 8
 9function transformResponseData(res: AxiosResponse): AxiosResponse {
10  res.data = transform(res.data, res.headers, res.config.transformResponse)
11 return res
12}

我们把对请求数据的处理和对响应数据的处理改成使用 transform 函数实现,并把配置中的 transformRequest 及 transformResponse 分别传入。

demo 编写

代码语言:javascript
复制
 1axios({
 2 transformRequest: [(function(data) {
 3 return qs.stringify(data)
 4  }), ...(axios.defaults.transformRequest as AxiosTransformer[])],
 5 transformResponse: [...(axios.defaults.transformResponse as AxiosTransformer[]), function(data) {
 6 if (typeof data === 'object') {
 7      data.b = 2
 8    }
 9 return data
10  }],
11 url: '/config/post',
12 method: 'post',
13 data: {
14 a: 1
15  }
16}).then((res) => {
17 console.log(res.data)
18})

我们对 transformRequest 做了修改,在执行它默认的 transformRequest 之前,我们先用 qs.stringify 库对传入的数据 data 做了一层转换。同时也对 transformResponse 做了修改,在执行完默认的 transformResponse 后,会给响应的 data 对象添加一个 data.b = 2。

因为之前我们实现了配置的合并,而且我们传入的 transformRequest 和 transformResponse 遵循默认合并策略,它们会覆盖默认的值。

至此,我们就实现了请求和响应的配置化。到目前为止,我们的 axios 都是一个单例,一旦我们修改了 axios 的默认配置,会影响所有的请求。官网提供了一个 axios.create 的工厂方法允许我们创建一个新的 axios 实例,同时允许我们传入新的配置和默认配置合并,并做为新的默认配置。下面一节课我们就来实现这个 feature。

扩展 axios.create 静态接口

需求分析

目前为止,我们的 axios 都是一个单例,一旦我们修改了 axios 的默认配置,会影响所有的请求。我们希望提供了一个 axios.create 的静态接口允许我们创建一个新的 axios 实例,同时允许我们传入新的配置和默认配置合并,并做为新的默认配置。

举个例子:

代码语言:javascript
复制
 1const instance = axios.create({
 2  transformRequest: [(function(data) {
 3 return qs.stringify(data)
 4  }), ...(axios.defaults.transformRequest as AxiosTransformer[])],
 5  transformResponse: [...(axios.defaults.transformResponse as AxiosTransformer[]), function(data) {
 6 if (typeof data === 'object') {
 7 data.b = 2
 8    }
 9 return data
10  }]
11})
12
13instance({
14  url: '/config/post',
15  method: 'post',
16 data: {
17    a: 1
18  }
19})

静态方法扩展

由于 axios 扩展了一个静态接口,因此我们先来修改接口类型定义。

types/index.ts:

代码语言:javascript
复制
1export interface AxiosStatic extends AxiosInstance{
2  create(config?: AxiosRequestConfig): AxiosInstance
3}

create 函数可以接受一个 AxiosRequestConfig 类型的配置,作为默认配置的扩展,也可以接受不传参数。

接着我们来实现 axios.create 静态方法。

axios.ts:

代码语言:javascript
复制
 1function createInstance(config: AxiosRequestConfig): AxiosStatic {
 2 const context = new Axios(config)
 3 const instance = Axios.prototype.request.bind(context)
 4
 5  extend(instance, context)
 6
 7 return instance as AxiosStatic
 8}
 9axios.create = function create(config) {
10 return createInstance(mergeConfig(defaults, config))
11}

内部调用了 createInstance 函数,并且把参数 config 与 defaults 合并,作为新的默认配置。注意这里我们需要 createInstance 函数的返回值类型为 AxiosStatic。

demo 编写

代码语言:javascript
复制
 1const instance = axios.create({
 2 transformRequest: [(function(data) {
 3 return qs.stringify(data)
 4  }), ...(axios.defaults.transformRequest as AxiosTransformer[])],
 5 transformResponse: [...(axios.defaults.transformResponse as AxiosTransformer[]), function(data) {
 6 if (typeof data === 'object') {
 7      data.b = 2
 8    }
 9 return data
10  }]
11})
12
13instance({
14 url: '/config/post',
15 method: 'post',
16 data: {
17 a: 1
18  }
19}).then((res) => {
20 console.log(res.data)
21})

我们对上节的示例做了小小的修改,通过 axios.create 方法创建一个新的实例 instance,并传入了 transformRequest 和 transformResponse 的配置修改了默认配置,然后通过 instance 发送请求,效果和之前是一样的。

至此我们实现了 axios.create 静态接口的扩展,整个 ts-axios 的配置化也告一段落。官方 axios 库还支持了对请求取消的能力,在发送请求前以及请求发送出去未响应前都可以取消该请求。我们就来实现这个 feature。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-11-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 二少爷的花间集 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云服务器
云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档