6-1、ts-axios配置化实现

需求

我们在发送请求的时候,可以传入配置,来决定请求的不同行为。我们也希望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

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

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

默认配置
  • 默认配置定义
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 = ['put', 'post', 'patch']
methodsWithData.forEach(method => {
  defaults.headers[method] = {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
})

export default defaults

我们定义了defaults常量,它包含默认请求的方法、超时时间以及headers配置,之后我们会根据新的需求添加更多的默认配置。

  • 添加到axios对象中
export default class Axios {
  defaults: AxiosRequestConfig
  interceptors: Interceptors
  constructor(initConfig: AxiosRequestConfig) {
    this.interceptors = {
      request: new InterceptorManager<AxiosRequestConfig>(),
      response: new InterceptorManager<AxiosResponse>()
    }
    this.defaults = initConfig
  }
  // ...
}

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

import { AxiosInstance, AxiosRequestConfig } from "./types";
import Axios from './core/Axios'
import { extend } from "./helpers/util";
import defaults from './core/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

这样我们就可以在执行createInstance的时候,传入默认配置了。

默认配置合并策略

定义了默认配置之后,我们发送每个请求的时候,需要把自定义配置和默认配置进行合并,它并不是简单的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'
  }
}
合并配置
import { AxiosRequestConfig, Method } from "../types";
import { isPlainObject } from "../helpers/util";

export default function mergeConfig(config1: AxiosRequestConfig, config2?: AxiosRequestConfig): AxiosRequestConfig {
  if (!config2) {
    config2 = {}
  }
  console.log('config1', config1)
  const config = Object.create(null)
  const strats = Object.create(null)
  const stratKeysFromVal2 = ['url', 'params', 'data']
  stratKeysFromVal2.forEach(key => {
    strats[key] = fromVal2Strat
  })
  const stratKeysDeepMerge = ['headers']
  stratKeysDeepMerge.forEach(key => {
    strats[key] = deepMergeStrat
  })
  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])
  }
  config.headers = flattenHeaders(config.headers, config.method)
  return config
}
function deepMergeStrat(val1: any, val2: any): any {
  val1 = val1 || {}
  val2 = val2 || {}
  return deepMerge(val1, val2)
}

// 复杂合并策略,比如headers
function deepMerge(...objs: any[]) {
  const result = Object.create(null)
  objs.forEach(obj => {
    if (!obj) {
      return
    }
    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
}

// 对于url, params, data默认配置是没有意义的,只要config2中配置过,就使用
function fromVal2Strat(val1: any, val2: any): any {
  return val2
}
// 默认合并策略,即config2中有,则以config2中的为准
function defaultStrat(val1: any, val2: any): any {
  return typeof val2 !== 'undefined' ? val2 : val1
}

因为在headers中会出现

headers: {
  common: {
    Accept: 'application/json, text/plain, */*'
  },
  post: {
    'Content-Type':'application/x-www-form-urlencoded'
  }
}

我们需要把它转化成这样

headers: {
  Accept: 'application/json, text/plain, */*',
 'Content-Type':'application/x-www-form-urlencoded'
}

所以我们需要下面的方法,把它压成一级

function flattenHeaders(headers: any, method: Method): any {
  if (!headers) {
    return headers
  }
  headers = deepMerge(headers.common || {}, headers[method] || {}, headers)
  const keysToDelete = ['delete', 'get', 'head', 'options', 'post', 'put', 'patch', 'common']
  keysToDelete.forEach(key => {
    delete headers[key]
  })
  return headers
}

在request中增加mergeConfig

request(url: string | AxiosRequestConfig, config?: AxiosRequestConfig): AxiosPromise {
    // 这里要定义一个新的newConfig变量,是因为AxiosPromise接收的泛型类型T是any,与config的类型不一致
    let newConfig: any
    newConfig = config || {}
    if (typeof url === 'string') {
      newConfig.url = url
    } else {
      newConfig = url
    }
    newConfig = mergeConfig(this.defaults, newConfig)
  // ...
}
demo
import axios from '../../src/index'
import qs from 'qs'
axios.defaults.headers.common['test2'] = 123

axios.interceptors.request.use(config => {
  config.headers.test += '1'
  return config
})
axios.interceptors.request.use(config => {
  config.headers.test += '2'
  return config
})
axios.interceptors.request.use(config => {
  config.headers.test += '3'
  return config
})

axios.interceptors.response.use(res => {
  res.data += '1'
  return res
})
let interceptor = axios.interceptors.response.use(res => {
  res.data += '2'
  return res
})
axios.interceptors.response.use(res => {
  res.data += '3'
  return res
})

axios.interceptors.response.eject(interceptor)

axios({
  url: '/api/extend/post',
  method: 'post',
  data: qs.stringify({
    a: 1
  }),
  headers: {
    test: '321'
  }
}).then((res) => {
  console.log(res.data)
})

这个例子中我们额外引入了qs库,它是一个查询字符串解析和字符串化的库。
比如我们例子中的{a: 1}经过qs.stringify变成a=1。
因为我们的例子给默认值添加了post和common的headers,我们在请求前做了配置合并,于是我们请求的headers中就添加了content-type字段,它的值是application/x-www-form-urlencoded,另外我们也加了test2字段,它的值是123.
至此,我们的合并配置的逻辑就实现完了。我们在前面的章节编写axios的基础功能的时候,对请求数据和响应数据都做了处理,官方axios则把这2部分的逻辑也做到了默认配置中,以为这用户可以去修改这2部分的逻辑,实现自己对请求和响应数据处理的逻辑。接下来,我们会来实现这部分feature。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259