基于ts重构axios

基于ts重构axios

ustbhuangyi 老师的 基于TypeScript从零重构axios学习记录。

知识点

TypeScript 常用语法:

基础类型函数变量声明接口泛型类型推新高级类型

axios js库:

项目脚手架基础功能实现异常情况处理接口扩展拦截器实现配置化实现取消功能实现其他功能实现等等

主要工具:
JestTSLintCommitizenPrettierRollupJSSemantic release

基本语法

点我

需求分析

Features

  • 在浏览器使用 XMLHttpRequest 对象通讯
  • 支持 Promise API
  • 支持请求和响应的拦截器
  • 支持请求数据和响应数据的转换
  • 支持请求的取消
  • JSON数据的自动转换
  • 客户端防止 XSRF

基于 XMLHttpRequest 编写基本请求代码

处理请求数据:url/body/headers

src/types/index.ts

1
2
3
4
5
6
7
8
export type Method = 'get' | 'GET' | 'delete' | 'Delete' | 'head' | 'HEAD' | 'options' | 'OPTIONS' | 'post' | 'POST' | 'put' | 'PUT' | 'patch' | 'PATCH'
export interface AxiosRequestConfig {
url: string
method?: Method
data?: any
params?: any
headers?: any
}

src/xhr.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { AxiosRequestConfig } from './types'

export default function xhr(config: AxiosRequestConfig): void {
const { data = null, url, method = 'get', headers } = config
const request = new XMLHttpRequest()
// method,url,async
request.open(method.toUpperCase(), url, true)
Object.keys(headers).forEach(name => {
if (data === null && name.toLowerCase() === 'content-type') {
delete headers[name]
} else {
request.setRequestHeader(name, headers[name])
}
})
request.send(data)
}

src/index.ts

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
import { AxiosRequestConfig } from './types'
import { buildURL } from './helpers/url';
import { transformRequest } from './helpers/data';
import xhr from './xhr'
import { processHeaders } from './helpers/header';

function axios(config: AxiosRequestConfig): void {
processConfig(config)
xhr(config)
}

function processConfig(config: AxiosRequestConfig): void {
config.url = transformURL(config)
config.data = transformRequestData(config)
config.headers = transformHeaders(config)
}

function transformHeaders(config: AxiosRequestConfig): void {
const { headers = {}, data } = config
return processHeaders(headers, data)
}

function transformRequestData(config: AxiosRequestConfig): void {
return transformRequest(config.data)
}

function transformURL(config: AxiosRequestConfig): string {
const { url, params } = config;
return buildURL(url, params)
}

export default axios

工具类

data.ts

1
2
3
4
5
6
7
8
import { isPlainObject } from "./util";

export function transformRequest(data: any): any {
if (isPlainObject(data)) {
return JSON.stringify(data)
}
return data
}

headers.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { isPlainObject } from "./util"

function normalizeHeaderName(headers: any, normalizedName: string): void {
if (!headers) {
return
}
Object.keys(headers).forEach(name => {
if (name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) {
headers[normalizedName] = headers[name]
delete headers[name]
}
})
}

export function processHeaders(headers: any, data: any): any {
normalizeHeaderName(headers, 'Content-Type')
if (isPlainObject(data)) {
if (headers && !headers['Content-Type']) {
headers['Content-Type'] = 'application/json;charset=utf-8'
}
}
return headers
}

url.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { isDate, isPlainObject } from './util'

function encode(val: string): string {
return encodeURIComponent(val)
.replace(/%40/g, '@')
.replace(/%3A/ig, ':')
.replace(/%24/g, '**util.ts**

​```tsx
const toString = Object.prototype.toString

export function isDate(val: any): val is Date {
return toString.call(val) === '[object Date]'
}

export function isPlainObject(val: any): val is Object {
return toString.call(val) === '[object Object]'
}

处理响应数据

定义响应接口

types/index

1
2
3
4
5
6
7
8
9
10
11
export interface AxiosResponse {
data: any
status: number
statusText: string
headers: any
config: AxiosRequestConfig
request: any
}
export interface AxiosPromise extends Promise<AxiosResponse> {

}

处理 headers 的数据

helpers/header.ts

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
export function processHeaders(headers: any, data: any): any {
normalizeHeaderName(headers, 'Content-Type')
if (isPlainObject(data)) {
if (headers && !headers['Content-Type']) {
headers['Content-Type'] = 'application/json;charset=utf-8'
}
}
return headers
}

export function parseHeaders(headers: string): any {
let parsed = Object.create(null)
if (!headers) {
return headers
}
headers.split('\r\n').forEach(line => {
let [key, val] = line.split(':')
key = key.trim().toLowerCase()
if (!key) {
return
}
if (val) {
val = val.trim();
}
parsed[key] = val
})
return parsed
}

处理 响应data

helpers/data.ts

1
2
3
4
5
6
7
8
9
10
export function transformResponse(data: any): any {
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch (e) {
// do nothing
}
}
return data
}

修改 xhr, 返回一个 Promise

xhr.ts

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
38
39
40
41
import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from './types'
import { parseHeaders } from './helpers/headers'

export default function xhr(config: AxiosRequestConfig): AxiosPromise {
return new Promise(resolve => {
const { data = null, url, method = 'get', headers, responseType } = config
const request = new XMLHttpRequest()
if (responseType) {
request.responseType = responseType
}
// method,url,async
request.open(method.toUpperCase(), url, true)

request.onreadystatechange = function handleLoad() {
if (request.readyState !== 4) {
return
}

const responseHeaders = parseHeaders(request.getAllResponseHeaders())
const responseData = responseType && responseType !== 'text' ? request.response : request.responseText
const response: AxiosResponse = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
}
resolve(response)
}
Object.keys(headers).forEach(name => {
if (data === null && name.toLowerCase() === 'content-type') {
delete headers[name]
} else {
request.setRequestHeader(name, headers[name])
}
})
request.send(data)
})

}

具体代码地址

)

.replace(/%2C/ig, ',')
.replace(/%20/g, '+')
.replace(/%5B/ig, '[')
.replace(/%5D/ig, ']')

}

export function buildURL(url: string, params?: any): string {
if (!params) {

return url

}
const parts: string[] = []
Object.keys(params).forEach(key => {

const val = params[key]
if (val === null || typeof val === 'undefined') {
  return
}
let values = []
if (Array.isArray(val)) {
  values = val
  key += '[]'
} else {
  values = [val]
}
values.forEach(val => {
  if (isDate(val)) {
    val = val.toISOString()
  } else if (isPlainObject(val)) {
    val = JSON.stringify(val)
  }
  parts.push( `${encode(key)}=${encode(val)}` )
})

})
let serializedParams = parts.join(‘&’)
if (serializedParams) {

const markIndex = url.indexOf('#')
if (markIndex !== -1) {
  url = url.slice(0, markIndex)
}
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams

}
return url
}

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

**util.ts**

​```tsx
const toString = Object.prototype.toString

export function isDate(val: any): val is Date {
return toString.call(val) === '[object Date]'
}

export function isPlainObject(val: any): val is Object {
return toString.call(val) === '[object Object]'
}

处理响应数据

定义响应接口

types/index

1
2
3
4
5
6
7
8
9
10
11
export interface AxiosResponse {
data: any
status: number
statusText: string
headers: any
config: AxiosRequestConfig
request: any
}
export interface AxiosPromise extends Promise<AxiosResponse> {

}

处理 headers 的数据

helpers/header.ts

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
export function processHeaders(headers: any, data: any): any {
normalizeHeaderName(headers, 'Content-Type')
if (isPlainObject(data)) {
if (headers && !headers['Content-Type']) {
headers['Content-Type'] = 'application/json;charset=utf-8'
}
}
return headers
}

export function parseHeaders(headers: string): any {
let parsed = Object.create(null)
if (!headers) {
return headers
}
headers.split('\r\n').forEach(line => {
let [key, val] = line.split(':')
key = key.trim().toLowerCase()
if (!key) {
return
}
if (val) {
val = val.trim();
}
parsed[key] = val
})
return parsed
}

处理 响应data

helpers/data.ts

1
2
3
4
5
6
7
8
9
10
export function transformResponse(data: any): any {
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch (e) {
// do nothing
}
}
return data
}

修改 xhr, 返回一个 Promise

xhr.ts

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
38
39
40
41
import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from './types'
import { parseHeaders } from './helpers/headers'

export default function xhr(config: AxiosRequestConfig): AxiosPromise {
return new Promise(resolve => {
const { data = null, url, method = 'get', headers, responseType } = config
const request = new XMLHttpRequest()
if (responseType) {
request.responseType = responseType
}
// method,url,async
request.open(method.toUpperCase(), url, true)

request.onreadystatechange = function handleLoad() {
if (request.readyState !== 4) {
return
}

const responseHeaders = parseHeaders(request.getAllResponseHeaders())
const responseData = responseType && responseType !== 'text' ? request.response : request.responseText
const response: AxiosResponse = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
}
resolve(response)
}
Object.keys(headers).forEach(name => {
if (data === null && name.toLowerCase() === 'content-type') {
delete headers[name]
} else {
request.setRequestHeader(name, headers[name])
}
})
request.send(data)
})

}

具体代码地址

0%