基于TypeScript封装Axios笔记(九)

时间:2022-07-23
本文章向大家介绍基于TypeScript封装Axios笔记(九),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
withCredentials

需求分析

有些时候我们会发一些跨域请求,比如 http://domain-a.com 站点发送一个 http://api.domain-b.com/get 的请求,默认情况下,浏览器会根据同源策略限制这种跨域请求,但是可以通过 CORS 技术解决跨域问题。

在同域的情况下,我们发送请求会默认携带当前域下的 cookie,但是在跨域的情况下,默认是不会携带请求域下的 cookie 的,比如 http://domain-a.com 站点发送一个 http://api.domain-b.com/get 的请求,默认是不会携带 api.domain-b.com 域下的 cookie,如果我们想携带(很多情况下是需要的),只需要设置请求的 xhr 对象的 withCredentials 为 true 即可。‍

代码实现

先修改 AxiosRequestConfig 的类型定义。

types/index.ts:

1export interface AxiosRequestConfig {
2 // ...
3  withCredentials?: boolean
4}

然后修改请求发送前的逻辑。

core/xhr.ts:

1const { /*...*/ withCredentials } = config
2
3if (withCredentials) {
4  request.withCredentials = true
5}

demo 编写

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

 1<!DOCTYPE html>
 2<html lang="en">
 3 <head>
 4 <meta charset="utf-8">
 5 <title>More example</title>
 6 </head>
 7 <body>
 8 <script src="/__build__/more.js"></script>
 9 </body>
10</html>

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

 1import axios from '../../src/index'
 2
 3document.cookie = 'a=b'
 4
 5axios.get('/more/get').then(res => {
 6 console.log(res)
 7})
 8
 9axios.post('http://127.0.0.1:8088/more/server2', { }, {
10 withCredentials: true
11}).then(res => {
12 console.log(res)
13})

这次我们除了给 server.js 去配置了接口路由,还创建了 server2.js,起了一个跨域的服务。

 1const express = require('express')
 2const bodyParser = require('body-parser')
 3const cookieParser = require('cookie-parser')
 4
 5const app = express()
 6
 7app.use(bodyParser.json())
 8app.use(bodyParser.urlencoded({ extended: true }))
 9app.use(cookieParser())
10
11const router = express.Router()
12
13const cors = {
14 'Access-Control-Allow-Origin': 'http://localhost:8080',
15 'Access-Control-Allow-Credentials': true,
16 'Access-Control-Allow-Methods': 'POST, GET, PUT, DELETE, OPTIONS',
17 'Access-Control-Allow-Headers': 'Content-Type'
18}
19
20router.post('/more/server2', function(req, res) {
21  res.set(cors)
22  res.json(req.cookies)
23})
24
25router.options('/more/server2', function(req, res) {
26  res.set(cors)
27  res.end()
28})
29
30app.use(router)
31
32const port = 8088
33module.exports = app.listen(port)

这里需要安装一下 cookie-parser 插件,用于请求发送的 cookie。

通过 demo 演示我们可以发现,对于同域请求,会携带 cookie,而对于跨域请求,只有我们配置了 withCredentials 为 true,才会携带 cookie。

至此我们的 withCredentials feature 开发完毕,我们来实现 axios 对 XSRF的防御功能。

XSRF 防御

需求分析

CSRF 的防御手段有很多,比如验证请求的 referer,但是 referer 也是可以伪造的,所以杜绝此类攻击的一种方式是服务器端要求每次请求都包含一个 token,这个 token 不在前端生成,而是在我们每次访问站点的时候生成,并通过 set-cookie 的方式种到客户端,然后客户端发送请求的时候,从 cookie 中对应的字段读取出 token,然后添加到请求 headers 中。这样服务端就可以从请求 headers 中读取这个 token 并验证,由于这个 token 是很难伪造的,所以就能区分这个请求是否是用户正常发起的。

对于我们的 ts-axios 库,我们要自动把这几件事做了,每次发送请求的时候,从 cookie 中读取对应的 token 值,然后添加到请求 headers中。我们允许用户配置 xsrfCookieName 和 xsrfHeaderName,其中 xsrfCookieName 表示存储 token 的 cookie 名称,xsrfHeaderName 表示请求 headers 中 token 对应的 header 名称。‍

1axios.get('/more/get',{
2 xsrfCookieName: 'XSRF-TOKEN', // default
3  xsrfHeaderName: 'X-XSRF-TOKEN' // default
4}).then(res => {
5 console.log(res)
6})

我们提供 xsrfCookieName 和 xsrfHeaderName 的默认值,当然用户也可以根据自己的需求在请求中去配置 xsrfCookieName 和 xsrfHeaderName。

代码实现

先修改 AxiosRequestConfig 的类型定义。

types/index.ts:

1export interface AxiosRequestConfig {
2 // ...
3  xsrfCookieName?: string
4  xsrfHeaderName?: string
5}

然后修改默认配置。

defaults.ts:

1const defaults: AxiosRequestConfig = {
2 // ...
3  xsrfCookieName: 'XSRF-TOKEN',
4
5  xsrfHeaderName: 'X-XSRF-TOKEN',
6}

接下来我们要做三件事:

  • 首先判断如果是配置 withCredentials 为 true 或者是同域请求,我们才会请求 headers 添加 xsrf 相关的字段。
  • 如果判断成功,尝试从 cookie 中读取 xsrf 的 token 值。
  • 如果能读到,则把它添加到请求 headers 的 xsrf 相关字段中。

我们先来实现同域请求的判断。

helpers/url.ts:

 1interface URLOrigin {
 2 protocol: string
 3  host: string
 4}
 5
 6
 7export function isURLSameOrigin(requestURL: string): boolean {
 8 const parsedOrigin = resolveURL(requestURL)
 9 return (
10    parsedOrigin.protocol === currentOrigin.protocol && parsedOrigin.host === currentOrigin.host
11  )
12}
13
14const urlParsingNode = document.createElement('a')
15const currentOrigin = resolveURL(window.location.href)
16
17function resolveURL(url: string): URLOrigin {
18  urlParsingNode.setAttribute('href', url)
19 const { protocol, host } = urlParsingNode
20
21 return {
22    protocol,
23    host
24  }
25}

同域名的判断主要利用了一个技巧,创建一个 a 标签的 DOM,然后设置 href 属性为我们传入的 url,然后可以获取该 DOM 的 protocol、host。当前页面的 url 和请求的 url 都通过这种方式获取,然后对比它们的 protocol 和 host 是否相同即可。

接着实现 cookie 的读取。

helpers/cookie.ts:

1const cookie = {
2  read(name: string): string | null {
3 const match = document.cookie.match(new RegExp('(^|;\s*)(' + name + ')=([^;]*)'))
4 return match ? decodeURIComponent(match[3]) : null
5  }
6}
7
8export default cookie

cookie 的读取逻辑很简单,利用了正则表达式可以解析到 name 对应的值。

最后实现完整的逻辑。

core/xhr.ts:

 1const {
 2 /*...*/
 3  xsrfCookieName,
 4  xsrfHeaderName
 5} = config
 6
 7if ((withCredentials || isURLSameOrigin(url!)) && xsrfCookieName){
 8 const xsrfValue = cookie.read(xsrfCookieName)
 9 if (xsrfValue) {
10    headers[xsrfHeaderName!] = xsrfValue
11  }
12}

demo 编写

1const instance = axios.create({
2 xsrfCookieName: 'XSRF-TOKEN-D',
3 xsrfHeaderName: 'X-XSRF-TOKEN-D'
4})
5
6instance.get('/more/get').then(res => {
7 console.log(res)
8})

examples/server.js:

1app.use(express.static(__dirname, {
2  setHeaders (res) {
3    res.cookie('XSRF-TOKEN-D', '1234abc')
4  }
5}))

在访问页面的时候,服务端通过 set-cookie 往客户端种了 key 为 XSRF-TOKEN,值为 1234abc 的 cookie,作为 xsrf 的 token 值。

然后我们在前端发送请求的时候,就能从 cookie 中读出 key 为 XSRF-TOKEN 的值,然后把它添加到 key 为 X-XSRF-TOKEN 的请求 headers 中。

至此,我们实现了 XSRF 的自动防御的能力,我们来实现 ts-axios 对上传和下载请求的支持。

上传和下载的进度监控

需求分析

有些时候,当我们上传文件或者是请求一个大体积数据的时候,希望知道实时的进度,甚至可以基于此做一个进度条的展示。

我们希望给 axios 的请求配置提供 onDownloadProgress 和 onUploadProgress 2 个函数属性,用户可以通过这俩函数实现对下载进度和上传进度的监控。

 1axios.get('/more/get',{
 2  onDownloadProgress(progressEvent) {
 3 // 监听下载进度
 4  }
 5})
 6
 7axios.post('/more/post',{
 8  onUploadProgress(progressEvent) {
 9 // 监听上传进度
10  }
11})

xhr 对象提供了一个 progress 事件,我们可以监听此事件对数据的下载进度做监控;另外,xhr.uplaod 对象也提供了 progress 事件,我们可以基于此对上传进度做监控。

代码实现

首先修改一下类型定义。

types/index.ts:

1export interface AxiosRequestConfig {
2 // ...
3  onDownloadProgress?: (e: ProgressEvent) => void
4  onUploadProgress?: (e: ProgressEvent) => void
5}

接着在发送请求前,给 xhr 对象添加属性。

core/xhr.ts:

 1const {
 2  /*...*/
 3  onDownloadProgress,
 4  onUploadProgress
 5} = config
 6
 7if (onDownloadProgress) {
 8 request.onprogress = onDownloadProgress
 9}
10
11if (onUploadProgress) {
12 request.upload.onprogress = onUploadProgress
13}

另外,如果请求的数据是 FormData 类型,我们应该主动删除请求 headers 中的 Content-Type 字段,让浏览器自动根据请求数据设置 Content-Type。比如当我们通过 FormData 上传文件的时候,浏览器会把请求 headers 中的 Content-Type 设置为 multipart/form-data。

我们先添加一个判断 FormData 的方法。

helpers/util.ts:

1export function isFormData(val: any): boolean {
2 return typeof val !== 'undefined' && val instanceof FormData
3}

然后再添加相关逻辑。

core/xhr.ts:

1if (isFormData(data)) {
2 delete headers['Content-Type']
3}

我们发现,xhr 函数内部随着需求越来越多,代码也越来越臃肿,我们可以把逻辑梳理一下,把内部代码做一层封装优化。

export default function xhr(config: AxiosRequestConfig): AxiosPromise {
  2  return new Promise((resolve, reject) => {
  3    const {
  4      data = null,
  5      url,
  6      method = 'get',
  7      headers,
  8      responseType,
  9      timeout,
 10      cancelToken,
 11      withCredentials,
 12      xsrfCookieName,
 13      xsrfHeaderName,
 14      onDownloadProgress,
 15      onUploadProgress
 16    } = config
 17
 18    const request = new XMLHttpRequest()
 19
 20    request.open(method.toUpperCase(), url!, true)
 21
 22    configureRequest()
 23
 24    addEvents()
 25
 26    processHeaders()
 27
 28    processCancel()
 29
 30    request.send(data)
 31
 32    function configureRequest(): void {
 33      if (responseType) {
 34        request.responseType = responseType
 35      }
 36
 37      if (timeout) {
 38        request.timeout = timeout
 39      }
 40
 41      if (withCredentials) {
 42        request.withCredentials = withCredentials
 43      }
 44    }
 45
 46    function addEvents(): void {
 47      request.onreadystatechange = function handleLoad() {
 48        if (request.readyState !== 4) {
 49          return
 50        }
 51
 52        if (request.status === 0) {
 53          return
 54        }
 55
 56        const responseHeaders = parseHeaders(request.getAllResponseHeaders())
 57        const responseData =
 58          responseType && responseType !== 'text' ? request.response : request.responseText
 59        const response: AxiosResponse = {
 60          data: responseData,
 61          status: request.status,
 62          statusText: request.statusText,
 63          headers: responseHeaders,
 64          config,
 65          request
 66        }
 67        handleResponse(response)
 68      }
 69
 70      request.onerror = function handleError() {
 71        reject(createError('Network Error', config, null, request))
 72      }
 73
 74      request.ontimeout = function handleTimeout() {
 75        reject(
 76          createError(`Timeout of ${config.timeout} ms exceeded`, config, 'ECONNABORTED', request)
 77        )
 78      }
 79
 80      if (onDownloadProgress) {
 81        request.onprogress = onDownloadProgress
 82      }
 83
 84      if (onUploadProgress) {
 85        request.upload.onprogress = onUploadProgress
 86      }
 87    }
 88
 89    function processHeaders(): void {
 90      if (isFormData(data)) {
 91        delete headers['Content-Type']
 92      }
 93
 94      if ((withCredentials || isURLSameOrigin(url!)) && xsrfCookieName) {
 95        const xsrfValue = cookie.read(xsrfCookieName)
 96        if (xsrfValue) {
 97          headers[xsrfHeaderName!] = xsrfValue
 98        }
 99      }
100
101      Object.keys(headers).forEach(name => {
102        if (data === null && name.toLowerCase() === 'content-type') {
103          delete headers[name]
104        } else {
105          request.setRequestHeader(name, headers[name])
106        }
107      })
108    }
109
110    function processCancel(): void {
111      if (cancelToken) {
112        cancelToken.promise.then(reason => {
113          request.abort()
114          reject(reason)
115        })
116      }
117    }
118
119    function handleResponse(response: AxiosResponse): void {
120      if (response.status >= 200 && response.status < 300) {
121        resolve(response)
122      } else {
123        reject(
124          createError(
125            `Request failed with status code ${response.status}`,
126            config,
127            null,
128            request,
129            response
130          )
131        )
132      }
133    }
134  })
135}

我们把整个流程分为 7 步:

  • 创建一个 request 实例。
  • 执行 request.open 方法初始化。
  • 执行 configureRequest 配置 request 对象。
  • 执行 addEvents 给 request 添加事件处理函数。
  • 执行 processHeaders 处理请求 headers。
  • 执行 processCancel 处理请求取消逻辑。
  • 执行 request.send 方法发送请求。

这样拆分后整个流程就会显得非常清晰,未来我们再去新增需求的时候代码也不会显得越来越臃肿。

demo 编写

这节课的 demo 非常有意思,我们第一次给界面上增加了一些交互的按钮。

examples/more/index.html

 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4 <meta charset="utf-8">
 5 <title>More example</title>
 6 <link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"/>
 7</head>
 8<body>
 9<h1>file download</h1>
10<div>
11 <button id="download" class="btn btn-primary">Download</button>
12</div>
13<h1>file upload</h1>
14<form role="form" class="form" onsubmit="return false;">
15 <input id="file" type="file" class="form-control"/>
16 <button id="upload" type="button" class="btn btn-primary">Upload</button>
17</form>
18
19<script src="/__build__/more.js"></script>
20</body>
21</html>

另外,我们为了友好地展示上传和下载进度,我们引入了一个开源库 nprogress,它可以在页面的顶部展示进度条。

examples/more/app.ts:

 1const instance = axios.create()
 2
 3function calculatePercentage(loaded: number, total: number) {
 4 return Math.floor(loaded * 1.0) / total
 5}
 6
 7function loadProgressBar() {
 8 const setupStartProgress = () => {
 9    instance.interceptors.request.use(config => {
10      NProgress.start()
11 return config
12    })
13  }
14
15 const setupUpdateProgress = () => {
16 const update = (e: ProgressEvent) => {
17 console.log(e)
18      NProgress.set(calculatePercentage(e.loaded, e.total))
19    }
20    instance.defaults.onDownloadProgress = update
21    instance.defaults.onUploadProgress = update
22  }
23
24 const setupStopProgress = () => {
25    instance.interceptors.response.use(response => {
26      NProgress.done()
27 return response
28    }, error => {
29      NProgress.done()
30 return Promise.reject(error)
31    })
32  }
33
34  setupStartProgress()
35  setupUpdateProgress()
36  setupStopProgress()
37}
38
39loadProgressBar()
40
41const downloadEl = document.getElementById('download')
42
43downloadEl!.addEventListener('click', e => {
44  instance.get('https://img.mukewang.com/5cc01a7b0001a33718720632.jpg')
45})
46
47const uploadEl = document.getElementById('upload')
48
49uploadEl!.addEventListener('click', e => {
50 const data = new FormData()
51 const fileEl = document.getElementById('file') as HTMLInputElement
52 if (fileEl.files) {
53    data.append('file', fileEl.files[0])
54
55    instance.post('/more/upload', data)
56  }
57})

对于 progress 事件参数 e,会有 e.total 和 e.loaded 属性,表示进程总体的工作量和已经执行的工作量,我们可以根据这 2 个值算出当前进度,然后通过 Nprogess.set 设置。另外,我们通过配置请求拦截器和响应拦截器执行 NProgress.start() 和 NProgress.done()。

我们给下载按钮绑定了一个 click 事件,请求一张图片,我们可以看到实时的进度;另外我们也给上传按钮绑定了一个 click 事件,上传我们选择的文件,同样也能看到实时进度。

在服务端,我们为了处理上传请求,需要下载安装一个 express 的中间件 connect-multiparty,然后使用它。

example/server.js:

1const multipart = require('connect-multiparty')
2app.use(multipart({
3  uploadDir: path.resolve(__dirname, 'upload-file')
4}))
5
6router.post('/more/upload', function(req, res) {
7  console.log(req.body, req.files)
8  res.end('upload success!')
9})

这里我们需要在 examples 目录下创建一个 upload-file 的空目录,用于存放上传的文件。

通过这个中间件,我们就可以处理上传请求并且可以把上传的文件存储在 upload-file 目录下。

为了保证代码正常运行,我们还需要在 examples/webpack.config.js 中添加 css-loader 和 css-loader,不要忘记先安装它们。

至此,ts-axios 支持了上传下载进度事件的回调函数的配置,用户可以通过配置这俩函数实现对下载进度和上传进度的监控。我们来实现 http 的认证授权功能。

HTTP 授权

需求分析

HTTP 协议中的 Authorization 请求 header 会包含服务器用于验证用户代理身份的凭证,通常会在服务器返回 401 Unauthorized 状态码以及 WWW-Authenticate 消息头之后在后续请求中发送此消息头。

axios 库也允许你在请求配置中配置 auth 属性,auth 是一个对象结构,包含 username 和 password 2 个属性。一旦用户在请求的时候配置这俩属性,我们就会自动往 HTTP 的 请求 header 中添加 Authorization 属性,它的值为 Basic 加密串。‍

这里的加密串是 username:password base64 加密后的结果。

 1axios.post('/more/post', {
 2 a: 1
 3}, {
 4 auth: {
 5 username: 'Yee',
 6 password: '123456'
 7  }
 8}).then(res => {
 9 console.log(res)
10})

代码实现

首先修改一下类型定义。

types/index.ts:

1export interface AxiosRequestConfig {
2 // ...
3  auth?: AxiosBasicCredentials
4}
5
6export interface AxiosBasicCredentials {
7  username: string
8  password: string
9}

接着修改合并规则,因为 auth 也是一个对象格式,所以它的合并规则是 deepMergeStrat。

core/mergeConfig.ts:

1const stratKeysDeepMerge = ['headers', 'auth']

然后修改发送请求前的逻辑。

core/xhr.ts:

1const {
2 /*...*/
3  auth
4} = config
5
6if (auth) {
7  headers['Authorization'] = 'Basic ' + btoa(auth.username + ':' + auth.password)
8}

demo 编写

 1axios.post('/more/post', {
 2 a: 1
 3}, {
 4 auth: {
 5 username: 'Yee',
 6 password: '123456'
 7  }
 8}).then(res => {
 9 console.log(res)
10})

另外,我们在 server.js 中对于这个路由接口写了一段小逻辑:

 1router.post('/more/post', function(req, res) {
 2 const auth = req.headers.authorization
 3 const [type, credentials] = auth.split(' ')
 4 console.log(atob(credentials))
 5 const [username, password] = atob(credentials).split(':')
 6 if (type === 'Basic' && username === 'Yee' && password === '123456') {
 7    res.json(req.body)
 8  } else {
 9    res.end('UnAuthorization')
10  }
11})

注意,这里我们需要安装第三方库 atob 实现 base64 串的解码。

至此,ts-axios 支持了 HTTP 授权功能,用户可以通过配置 auth 对象实现自动在请求 header 中添加 Authorization 属性。我们来实现自定义合法状态码功能。

自定义合法状态码

需求分析

之前 ts-axios 在处理响应结果的时候,认为 HTTP status 在 200 和 300 之间是一个合法值,在这个区间之外则创建一个错误。有些时候我们想自定义这个规则,比如认为 304 也是一个合法的状态码,所以我们希望 ts-axios 能提供一个配置,允许我们自定义合法状态码规则。如下:

1axios.get('/more/304', {
2  validateStatus(status) {
3 return status >= 200 && status < 400
4  }
5}).then(res => {
6 console.log(res)
7}).catch((e: AxiosError) => {
8 console.log(e.message)
9})

通过在请求配置中配置一个 validateStatus 函数,它可以根据参数 status 来自定义合法状态码的规则。

代码实现

首先修改一下类型定义。

types/index.ts:

1export interface AxiosRequestConfig {
2 // ...
3  validateStatus?: (status: number) => boolean
4}

然后我们来修改默认配置规则。

defaults.ts:

1validateStatus(status: number): boolean {
2 return status >= 200 && status < 300
3}

添加默认合法状态码的校验规则。然后再请求后对响应数据的处理逻辑。

core/xhr.ts:

 1const {
 2  /*...*/
 3  validateStatus
 4} = config
 5
 6function handleResponse(response: AxiosResponse): void {
 7 if (!validateStatus || validateStatus(response.status)) {
 8    resolve(response)
 9  } else {
10    reject(
11      createError(
12        `Request failed with status code ${response.status}`,
13        config,
14 null,
15 request,
16 response
17      )
18    )
19  }
20}

如果没有配置 validateStatus 以及 validateStatus 函数返回的值为 true 的时候,都认为是合法的,正常 resolve(response),否则都创建一个错误。

demo 编写

 1axios.get('/more/304').then(res => {
 2 console.log(res)
 3}).catch((e: AxiosError) => {
 4 console.log(e.message)
 5})
 6
 7axios.get('/more/304', {
 8  validateStatus(status) {
 9 return status >= 200 && status < 400
10  }
11}).then(res => {
12 console.log(res)
13}).catch((e: AxiosError) => {
14 console.log(e.message)
15})

server.js 中我们编写了这个路由接口

1router.get('/more/304', function(req, res) {
2  res.status(304)
3  res.end()
4})

接口返回 304 状态码,对于默认的请求我们会输出一条错误信息。第二个请求中我们配置了自定义合法状态码规则,区间在 200 和 400 之间,这样就不会报错,而是可以正常输出响应对象。

至此 ts-axios 实现了自定义合法状态码功能,用户可以配置 validateStatus 自定义合法状态码规则。之前有同学会质疑 ts-axios 对于请求 url 参数的序列化处理规则,我们来实现自定义参数序列化规则功能。

自定义参数序列化

需求分析

我们对请求的 url 参数做了处理,我们会解析传入的 params 对象,根据一定的规则把它解析成字符串,然后添加在 url 后面。在解析的过程中,我们会对字符串 encode,但是对于一些特殊字符比如 @、+ 等却不转义,这是 axios 库的默认解析规则。当然,我们也希望自己定义解析规则,于是我们希望 ts-axios 能在请求配置中允许我们配置一个 paramsSerializer 函数来自定义参数的解析规则,该函数接受 params 参数,返回值作为解析后的结果,如下:

 1axios.get('/more/get', {
 2 params: {
 3    a: 1,
 4    b: 2,
 5    c: ['a', 'b', 'c']
 6  },
 7  paramsSerializer(params) {
 8 return qs.stringify(params, { arrayFormat: 'brackets' })
 9  }
10}).then(res => {
11  console.log(res)
12})

代码实现

首先修改一下类型定义。

types/index.ts:

1export interface AxiosRequestConfig { 2 // ... 3 paramsSerializer?: (params: any) => string 4}

然后修改 buildURL 函数的实现。

helpers/url.ts:

 export function buildURL(
 2  url: string,
 3  params?: any,
 4  paramsSerializer?: (params: any) => string
 5): string {
 6  if (!params) {
 7    return url
 8  }
 9
10  let serializedParams
11
12  if (paramsSerializer) {
13    serializedParams = paramsSerializer(params)
14  } else if (isURLSearchParams(params)) {
15    serializedParams = params.toString()
16  } else {
17    const parts: string[] = []
18
19    Object.keys(params).forEach(key => {
20      const val = params[key]
21      if (val === null || typeof val === 'undefined') {
22        return
23      }
24      let values = []
25      if (Array.isArray(val)) {
26        values = val
27        key += '[]'
28      } else {
29        values = [val]
30      }
31      values.forEach(val => {
32        if (isDate(val)) {
33          val = val.toISOString()
34        } else if (isPlainObject(val)) {
35          val = JSON.stringify(val)
36        }
37        parts.push(`${encode(key)}=${encode(val)}`)
38      })
39    })
40
41    serializedParams = parts.join('&')
42  }
43
44  if (serializedParams) {
45    const markIndex = url.indexOf('#')
46    if (markIndex !== -1) {
47      url = url.slice(0, markIndex)
48    }
49
50    url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
51  }
52
53  return url
54}

这里我们给 buildURL 函数新增了 paramsSerializer 可选参数,另外我们还新增了对 params 类型判断,如果它是一个 URLSearchParams 对象实例的话,我们直接返回它 toString 后的结果。

helpers/util.ts:

1export function isURLSearchParams(val: any): val is URLSearchParams {
2 return typeof val !== 'undefined' && val instanceof URLSearchParams
3}

最后我们要修改 buildURL 调用的逻辑。

core/dispatchRequest.ts:

1function transformURL(config: AxiosRequestConfig): string {
2  const { url, params, paramsSerializer } = config
3 return buildURL(url!, params, paramsSerializer)
4}

demo 编写

 1axios.get('/more/get', {
 2 params: new URLSearchParams('a=b&c=d')
 3}).then(res => {
 4 console.log(res)
 5})
 6
 7axios.get('/more/get', {
 8 params: {
 9 a: 1,
10 b: 2,
11 c: ['a', 'b', 'c']
12  }
13}).then(res => {
14 console.log(res)
15})
16
17const instance = axios.create({
18  paramsSerializer(params) {
19 return qs.stringify(params, { arrayFormat: 'brackets' })
20  }
21})
22
23instance.get('/more/get', {
24 params: {
25 a: 1,
26 b: 2,
27 c: ['a', 'b', 'c']
28  }
29}).then(res => {
30 console.log(res)
31})

我们编写了 3 种情况的请求,第一种满足请求的 params 参数是 URLSearchParams 对象类型的。后两种请求的结果主要区别在于前者并没有对 [] 转义,而后者会转义。

至此,ts-axios 实现了自定义参数序列化功能,用户可以配置 paramsSerializer 自定义参数序列化规则。我们来实现 ts-axios 对 baseURL 的支持。

baseURL

需求分析

有些时候,我们会请求某个域名下的多个接口,我们不希望每次发送请求都填写完整的 url,希望可以配置一个 baseURL,之后都可以传相对路径。如下:

1const instance = axios.create({
2  baseURL: 'https://some-domain.com/api'
3})
4
5instance.get('/get')
6
7instance.post('/post')

我们一旦配置了 baseURL,之后请求传入的 url 都会和我们的 baseURL 拼接成完整的绝对地址,除非请求传入的 url 已经是绝对地址。

代码实现

首先修改一下类型定义。

types/index.ts:

1export interface AxiosRequestConfig { 2 // ... 3 baseURL?: string 4}

接下来实现 2 个辅助函数。

helpers/url.ts:

1export function isAbsoluteURL(url: string): boolean {
2 return /^([a-z][a-zd+-.]*:)?///i.test(url)
3}
4
5export function combineURL(baseURL: string, relativeURL?: string): string {
6 return relativeURL ? baseURL.replace(//+$/, '') + '/' + relativeURL.replace(/^/+/, '') : baseURL
7}

最后我们来调用这俩个辅助函数。

core/dispatchRequest.ts:

1function transformURL(config: AxiosRequestConfig): string {
2 let { url, params, paramsSerializer, baseURL } = config
3 if (baseURL && !isAbsoluteURL(url!)) {
4    url = combineURL(baseURL, url)
5  }
6 return buildURL(url!, params, paramsSerializer)
7}

demo 编写

1const instance = axios.create({
2  baseURL: 'https://img.mukewang.com/'
3})
4
5instance.get('5cc01a7b0001a33718720632.jpg')
6
7instance.get('https://img.mukewang.com/szimg/5becd5ad0001b89306000338-360-202.jpg')

这个 demo 非常简单,我们请求了慕课网的 2 张图片,注意当第二个请求 url 已经是绝对地址的时候,我们并不会再去拼接 baseURL。

至此,ts-axios 就实现了 baseURL 的配置功能,接下来我们来实现 ts-axios 的静态方法扩展。

静态方法扩展

需求分析

官方 axios 库实现了 axios.all、axios.spread 等方法,它们的用法如下:

 1function getUserAccount() {
 2 return axios.get('/user/12345');
 3}
 4
 5function getUserPermissions() {
 6 return axios.get('/user/12345/permissions');
 7}
 8
 9axios.all([getUserAccount(), getUserPermissions()])
10  .then(axios.spread(function (acct, perms) {
11 // Both requests are now complete
12  }));

实际上,axios.all 就是 Promise.all 的封装,它返回的是一个 Promise 数组,then 函数的参数本应是一个参数为 Promise resolves(数组)的函数,在这里使用了 axios.spread 方法。所以 axios.spread 方法是接收一个函数,返回一个新的函数,新函数的结构满足 then 函数的参数结构。

个人认为 axios 这俩静态方法在目前看来很鸡肋,因为使用 Promise 一样可以完成这俩需求。

 1function getUserAccount() {
 2 return axios.get('/user/12345');
 3}
 4
 5function getUserPermissions() {
 6 return axios.get('/user/12345/permissions');
 7}
 8
 9Promise.all([getUserAccount(), getUserPermissions()])
10  .then(([acct,perms]) {
11 // Both requests are now complete
12  }));

在 Promise.all 的 resolve 函数中,我们可以直接通过数组的解构拿到每个请求对应的响应对象。

但是为了保持与官网 axios API 一致,我们也在 ts-axios 库中实现这俩方法。

官方 axios 库也通过 axios.Axios 对外暴露了 Axios 类(感觉也没有啥使用场景,但为了保持一致,我们也会实现)。

另外对于 axios 实例,官网还提供了 getUri 方法在不发送请求的前提下根据传入的配置返回一个 url,如下:

 1const fakeConfig = {
 2  baseURL: 'https://www.baidu.com/',
 3  url: '/user/12345',
 4 params: {
 5    idClient: 1,
 6    idTest: 2,
 7    testString: 'thisIsATest'
 8  }
 9}
10console.log(axios.getUri(fakeConfig))
11// https://www.baidu.com/user/12345?idClient=1&idTest=2&testString=thisIsATest

代码实现

首先修改类型定义。

types/index.ts:

 1export interface AxiosClassStatic {
 2 new (config: AxiosRequestConfig): Axios
 3}
 4
 5export interface AxiosStatic extends AxiosInstance {
 6 // ...
 7
 8  all<T>(promises: Array<T | Promise<T>>): Promise<T[]>
 9
10  spread<T, R>(callback: (...args: T[]) => R): (arr: T[]) => R
11
12  Axios: AxiosClassStatic
13}
14
15export interface Axios {
16 // ...
17
18  getUri(config?: AxiosRequestConfig): string
19}

然后我们去实现这几个静态方法。

axios.ts:

 1axios.all = function all(promises) {
 2 return Promise.all(promises)
 3}
 4
 5axios.spread = function spread(callback) {
 6 return function wrap(arr) {
 7 return callback.apply(null, arr)
 8  }
 9}
10
11axios.Axios = Axios

最后我们去给 Axios 添加实例方法 getUri。

core/Axios.ts:

1getUri(config?: AxiosRequestConfig): string {
2 config = mergeConfig(this.defaults, config)
3 return transformURL(config)
4}

先和默认配置合并,然后再通过 dispatchRequest 中实现的 transformURL 返回一个新的 url。

demo 编写

 1function getA() {
 2 return axios.get('/more/A')
 3}
 4
 5function getB() {
 6 return axios.get('/more/B')
 7}
 8
 9axios.all([getA(), getB()])
10  .then(axios.spread(function(resA, resB) {
11 console.log(resA.data)
12 console.log(resB.data)
13  }))
14
15
16axios.all([getA(), getB()])
17  .then(([resA, resB]) => {
18 console.log(resA.data)
19 console.log(resB.data)
20  })
21
22const fakeConfig = {
23 baseURL: 'https://www.baidu.com/',
24 url: '/user/12345',
25 params: {
26 idClient: 1,
27 idTest: 2,
28 testString: 'thisIsATest'
29  }
30}
31console.log(axios.getUri(fakeConfig))

这里我们通过 axios.all 同时发出了 2 个请求,返回了 Promise 数组,,我们可以在 axios.spread 的参数函数中拿到结果,也可以直接在 then 函数的参数函数中拿到结果。另外,我们可以根据 axios.getUri 方法在不发送请求的情况下根据配置得到最终请求的 url 结果。

至此,ts-axios 就实现了官网 axios 库在浏览器端的所有需求。