需求分析
有些时候我们会发一些跨域请求,比如 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:
代码语言:javascript复制1export interface AxiosRequestConfig {
2 // ...
3 withCredentials?: boolean
4}
然后修改请求发送前的逻辑。
core/xhr.ts:
代码语言:javascript复制1const { /*...*/ withCredentials } = config
2
3if (withCredentials) {
4 request.withCredentials = true
5}
demo 编写
在 examples 目录下创建 more 目录,在 cancel 目录下创建 index.html:
代码语言:javascript复制 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 作为入口文件:
代码语言:javascript复制 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,起了一个跨域的服务。
代码语言:javascript复制 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 名称。
代码语言:javascript复制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:
代码语言:javascript复制1export interface AxiosRequestConfig {
2 // ...
3 xsrfCookieName?: string
4 xsrfHeaderName?: string
5}
然后修改默认配置。
defaults.ts:
代码语言:javascript复制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:
代码语言:javascript复制 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:
代码语言:javascript复制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:
代码语言:javascript复制 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 编写
代码语言:javascript复制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:
代码语言:javascript复制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 个函数属性,用户可以通过这俩函数实现对下载进度和上传进度的监控。
代码语言:javascript复制 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:
代码语言:javascript复制1export interface AxiosRequestConfig {
2 // ...
3 onDownloadProgress?: (e: ProgressEvent) => void
4 onUploadProgress?: (e: ProgressEvent) => void
5}
接着在发送请求前,给 xhr 对象添加属性。
core/xhr.ts:
代码语言:javascript复制 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:
代码语言:javascript复制1export function isFormData(val: any): boolean {
2 return typeof val !== 'undefined' && val instanceof FormData
3}
然后再添加相关逻辑。
core/xhr.ts:
代码语言:javascript复制1if (isFormData(data)) {
2 delete headers['Content-Type']
3}
我们发现,xhr 函数内部随着需求越来越多,代码也越来越臃肿,我们可以把逻辑梳理一下,把内部代码做一层封装优化。
代码语言:javascript复制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
代码语言:javascript复制 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:
代码语言:javascript复制 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:
代码语言:javascript复制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 加密后的结果。
代码语言:javascript复制 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:
代码语言:javascript复制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:
代码语言:javascript复制1const {
2 /*...*/
3 auth
4} = config
5
6if (auth) {
7 headers['Authorization'] = 'Basic ' btoa(auth.username ':' auth.password)
8}
demo 编写
代码语言:javascript复制 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 中对于这个路由接口写了一段小逻辑:
代码语言:javascript复制 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 能提供一个配置,允许我们自定义合法状态码规则。如下:
代码语言:javascript复制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:
代码语言:javascript复制1export interface AxiosRequestConfig {
2 // ...
3 validateStatus?: (status: number) => boolean
4}
然后我们来修改默认配置规则。
defaults.ts:
代码语言:javascript复制1validateStatus(status: number): boolean {
2 return status >= 200 && status < 300
3}
添加默认合法状态码的校验规则。然后再请求后对响应数据的处理逻辑。
core/xhr.ts:
代码语言:javascript复制 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 编写
代码语言:javascript复制 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 中我们编写了这个路由接口
代码语言:javascript复制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 参数,返回值作为解析后的结果,如下:
代码语言:javascript复制 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:
代码语言:javascript复制 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:
代码语言:javascript复制1export function isURLSearchParams(val: any): val is URLSearchParams {
2 return typeof val !== 'undefined' && val instanceof URLSearchParams
3}
最后我们要修改 buildURL 调用的逻辑。
core/dispatchRequest.ts:
代码语言:javascript复制1function transformURL(config: AxiosRequestConfig): string {
2 const { url, params, paramsSerializer } = config
3 return buildURL(url!, params, paramsSerializer)
4}
demo 编写
代码语言:javascript复制 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,之后都可以传相对路径。如下:
代码语言:javascript复制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:
代码语言:javascript复制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:
代码语言:javascript复制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 编写
代码语言:javascript复制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 等方法,它们的用法如下:
代码语言:javascript复制 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 一样可以完成这俩需求。
代码语言:javascript复制 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,如下:
代码语言:javascript复制 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:
代码语言:javascript复制 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:
代码语言:javascript复制 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:
代码语言:javascript复制1getUri(config?: AxiosRequestConfig): string {
2 config = mergeConfig(this.defaults, config)
3 return transformURL(config)
4}
先和默认配置合并,然后再通过 dispatchRequest 中实现的 transformURL 返回一个新的 url。
demo 编写
代码语言:javascript复制 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 库在浏览器端的所有需求。