Will's blog Will's blog
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • HTML
  • CSS
  • VUE
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

will

前端小学生
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • HTML
  • CSS
  • VUE
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 初识 TypeScript

  • TypeScript 常用语法

  • ts-axios 项目初始化

  • ts-axios 基础功能实现

  • ts-axios 异常情况处理

  • ts-axios 接口扩展

  • ts-axios 拦截器实现

  • ts-axios 配置化实现

    • 合并配置的设计与实现
      • 需求分析
      • 默认配置
        • 默认配置定义
        • 添加到 axios 对象中
      • 配置合并及策略
        • 合并方法
        • 默认合并策略
        • 只接受自定义配置合并策略
        • 复杂对象合并策略
      • flatten headers
      • demo 编写
    • 请求和响应配置化
    • 扩展 create 静态接口
  • ts-axios 取消功能实现

  • ts-axios 更多功能实现

  • ts-axios 单元测试

  • ts-axios 部署与发布

  • 《TypeScript 从零实现 axios》
  • ts-axios 配置化实现
HuangYi
2020-01-05
目录

合并配置的设计与实现

# 合并配置的设计与实现

# 需求分析

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

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

axios.defaults.headers.common['test'] = 123

axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'

axios.defaults.timeout = 2000

1
2
3

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

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

# 默认配置

# 默认配置定义

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

defaults.ts:

import { AxiosRequestConfig } from './types'



const defaults: AxiosRequestConfig = {

  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

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

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

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

# 添加到 axios 对象中

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

export default class Axios {

  defaults: AxiosRequestConfig

  interceptors: Interceptors



  constructor(initConfig: AxiosRequestConfig) {

    this.defaults = initConfig

    this.interceptors = {

      request: new InterceptorManager<AxiosRequestConfig>(),

      response: new InterceptorManager<AxiosResponse>()

    }

  }

  // ...

}

1
2
3
4
5
6
7
8
9
10
11
12
13

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

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

import defaults from './defaults'



function createInstance(config: AxiosRequestConfig): AxiosStatic {

  const context = new Axios(config)

  const instance = Axios.prototype.request.bind(context)



  // extend(instance, Axios.prototype, context)



  extend(instance, context)



  return instance as AxiosStatic

}



const axios = createInstance(defaults)

1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

# 配置合并及策略

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

config1 = {

  method: 'get',



  timeout: 0,



  headers: {

    common: {

      Accept: 'application/json, text/plain, */*'

    }

  }

}



config2 = {

  url: '/config/post',

  method: 'post',

  data: {

    a: 1

  },

  headers: {

    test: '321'

  }

}



merged = {

  url: '/config/post',

  method: 'post',

  data: {

    a: 1

  },

  timeout: 0,

  headers: {

    common: {

      Accept: 'application/json, text/plain, */*'

    }

    test: '321'

  }

}

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

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

# 合并方法

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

}

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

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

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

export interface AxiosRequestConfig {

  // ...



  [propName: string]: any

}

1
2
3
4
5

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

# 默认合并策略

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

function defaultStrat(val1: any, val2: any): any {

  return typeof val2 !== 'undefined' ? val2 : val1

}

1
2
3

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

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

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

function fromVal2Strat(val1: any, val2: any): any {

  if (typeof val2 !== 'undefined') {

    return val2

  }

}



const stratKeysFromVal2 = ['url', 'params', 'data']



stratKeysFromVal2.forEach(key => {

  strats[key] = fromVal2Strat

})

1
2
3
4
5
6
7
8
9
10
11

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

# 复杂对象合并策略

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

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

  }

}



const stratKeysDeepMerge = ['headers']



stratKeysDeepMerge.forEach(key => {

  strats[key] = deepMergeStrat

})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

helpers/util.ts:

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

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

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

config = mergeConfig(this.defaults, config)

1

# flatten headers

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

举个例子:

headers: {

  common: {

    Accept: 'application/json, text/plain, */*'

  },

  post: {

    'Content-Type':'application/x-www-form-urlencoded'

  }

}

1
2
3
4
5
6
7
8

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

headers: {

  Accept: 'application/json, text/plain, */*',

 'Content-Type':'application/x-www-form-urlencoded'

}

1
2
3
4

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

接下来我们实现 flattenHeaders 方法。

helpers/header.ts:

export function flattenHeaders(headers: any, method: Method): any {

  if (!headers) {

    return headers

  }

  headers = deepMerge(headers.common || {}, headers[method] || {}, headers)



  const methodsToDelete = ['delete', 'get', 'head', 'options', 'post', 'put', 'patch', 'common']



  methodsToDelete.forEach(method => {

    delete headers[method]

  })



  return headers

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

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

core/dispatchRequest.ts:

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

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

# demo 编写

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

<!DOCTYPE html>

<html lang="en">

  <head>

    <meta charset="utf-8">

    <title>Config example</title>

  </head>

  <body>

    <script src="/__build__/config.js"></script>

  </body>

</html>

1
2
3
4
5
6
7
8
9
10

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

import axios from '../../src/index'

import qs from 'qs'



axios.defaults.headers.common['test2'] = 123



axios({

  url: '/config/post',

  method: 'post',

  data: qs.stringify({

    a: 1

  }),

  headers: {

    test: '321'

  }

}).then((res) => {

  console.log(res.data)

})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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

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

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

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

编辑此页 (opens new window)
#TypeScript
上次更新: 2024/08/22, 14:42:43
拦截器设计与实现
请求和响应配置化

← 拦截器设计与实现 请求和响应配置化→

最近更新
01
我用AI写前端代码这一年:从怀疑到真香的转变
09-15
02
基于 Next.js 的无人机数据孪生可视化平台实践
07-17
03
vite 包缓存问题 处理
06-04
更多文章>
Theme by Vdoing | Copyright © 2019-2025 Will | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式