此外,还会有一些 axios 库支持的一些其它的 feature。
使用TypeScript library starter脚手架工具
先通过 git clone
把项目代码拉到 ts-axios
目录,然后运行 npm install
安装依赖,并且给项目命名。
生成的目录结构如下:
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── code-of-conduct.md
├── node_modules
├── package-lock.json
├── package.json
├── rollup.config.ts // rollup 配置文件
├── src // 源码目录
├── test // 测试目录
├── tools // 发布到 GitHup pages 以及 发布到 npm 的一些配置脚本工具
├── tsconfig.json // TypeScript 编译配置文件
└── tslint.json // TypeScript lint 文件
集成了很多优秀的开源工具:
利用 Node.js 的 express
库去运行我们的 demo,利用 webpack
来作为 demo 的构建工具。
先安装编写 demo 需要的依赖包,如下:
"webpack": "^4.28.4",
"webpack-dev-middleware": "^3.5.0",
"webpack-hot-middleware": "^2.24.3",
"ts-loader": "^5.3.3",
"tslint-loader": "^3.5.4",
"express": "^4.16.4",
"body-parser": "^1.18.3"
其中,webpack
是打包构建工具,webpack-dev-middleware
和 webpack-hot-middleware
是 2 个 express
的 webpack
中间件,ts-loader
和 tslint-loader
是 webpack
需要的 TypeScript 相关 loader,express
是 Node.js 的服务端框架,body-parser
是 express
的一个中间件,解析 body
数据用的。
然后在 examples
目录下创建 webpack
配置文件 webpack.config.js
:
const fs = require("fs");
const path = require("path");
const webpack = require("webpack");
module.exports = {
mode: "development",
/**
* 我们会在 examples 目录下建多个子目录
* 我们会把不同章节的 demo 放到不同的子目录中
* 每个子目录的下会创建一个 app.ts
* app.ts 作为 webpack 构建的入口文件
* entries 收集了多目录个入口文件,并且每个入口还引入了一个用于热更新的文件
* entries 是一个对象,key 为目录名
*/
entry: fs.readdirSync(__dirname).reduce((entries, dir) => {
const fullDir = path.join(__dirname, dir);
const entry = path.join(fullDir, "app.ts");
if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
entries[dir] = ["webpack-hot-middleware/client", entry];
}
return entries;
}, {}),
/**
* 根据不同的目录名称,打包生成目标 js,名称和目录名一致
*/
output: {
path: path.join(__dirname, "__build__"),
filename: "[name].js",
publicPath: "/__build__/",
},
module: {
rules: [
{
test: /\.ts$/,
enforce: "pre",
use: [
{
loader: "tslint-loader",
},
],
},
{
test: /\.tsx?$/,
use: [
{
loader: "ts-loader",
options: {
transpileOnly: true,
},
},
],
},
],
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
],
};
在 examples
目录下创建 server.js
文件:
const express = require("express");
const bodyParser = require("body-parser");
const webpack = require("webpack");
const webpackDevMiddleware = require("webpack-dev-middleware");
const webpackHotMiddleware = require("webpack-hot-middleware");
const WebpackConfig = require("./webpack.config");
const app = express();
const compiler = webpack(WebpackConfig);
app.use(
webpackDevMiddleware(compiler, {
publicPath: "/__build__/",
stats: {
colors: true,
chunks: false,
},
})
);
app.use(webpackHotMiddleware(compiler));
app.use(express.static(__dirname));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const port = process.env.PORT || 8080;
module.exports = app.listen(port, () => {
console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`);
});
然后在 examples
目录下创建对应功能实现的目录,在每个目录下再对应创建 index.html
和 app.ts
文件来编写各自demo。
在src
目录下创建一个 index.ts
文件,作为整个库的入口文件,然后定义一个 axios
方法,并把它导出。
// function axios(config) {}
// export default axios;
import { AxiosRequestConfig } from "./types";
function axios(config: AxiosRequestConfig) {}
export default axios;
接下来,我们需要给 config
参数定义一种接口类型。我们创建一个 types
目录,在下面创建一个 index.ts
文件,作为我们项目中公用的类型定义文件。
接下来定义 AxiosRequestConfig
接口类型:
export interface AxiosRequestConfig {
url: string;
method?: Method;
data?: any;
params?: any;
}
其中,url
为请求的地址,必选属性;而其余属性都是可选属性。method
是请求的 HTTP 方法;data
是 post
、patch
等类型请求的数据,放到 request body
中的;params
是 get
、head
等类型请求的数据,拼接到 url
的 query string
中的。
为了让 method
只能传入合法的字符串,定义一种字符串字面量类型 Method
export type Method =
| "get"
| "GET"
| "delete"
| "Delete"
| "head"
| "HEAD"
| "options"
| "OPTIONS"
| "post"
| "POST"
| "put"
| "PUT"
| "patch"
| "PATCH";
我们不在 index.ts
中去实现发送请求的逻辑,利用模块化的编程思想,把这个功能拆分到一个单独的模块中。
在 src
目录下创建一个 xhr.ts
文件,我们导出一个 xhr
方法,它接受一个 config
参数,类型也是 AxiosRequestConfig
类型
import { AxiosRequestConfig } from "./types";
export default function xhr(config: AxiosRequestConfig): void {
const { data = null, url, method = "get" } = config;
const request = new XMLHttpRequest();
request.open(method.toUpperCase(), url, true);
request.send(data);
}
我们首先通过解构赋值的语法从 config
中拿到对应的属性值赋值给变量,并且还定义了一些默认值,因为在 AxiosRequestConfig
接口的定义中,有些属性是可选的。
接着我们实例化了一个 XMLHttpRequest
对象,然后调用了它的 open
方法,传入了对应的一些参数,最后调用 send
方法发送请求。
编写好了 xhr
模块,我们就需要在 index.ts
中去引入这个模块。
import { AxiosRequestConfig } from "./types";
import xhr from "./xhr";
function axios(config: AxiosRequestConfig): void {
xhr(config);
}
export default axios;
至此,基本的发送请求代码就编写完毕。
需求分析:接下来需要处理把 params
对象的 key 和 value 拼接到 url
上
处理情况包括:参数值为数组、参数值为对象、参数值为 Date 类型、特殊字符支持、空值忽略、丢弃 url 中的哈希标记、保留 url 中已存在的参数。
根据需求分析,我们要实现一个工具函数,把 params
拼接到 url
上。我们希望把项目中的一些工具函数、辅助方法独立管理,于是我们创建一个 helpers
目录,在这个目录下创建 url.ts
文件,未来会把处理 url
相关的工具函数都放在该文件中。
// helpers/url.ts
import { isDate, isObject } from './util'
function encode (val: string): string {
return encodeURIComponent(val)
.replace(/%40/g, '@')
.replace(/%3A/gi, ':')
.replace(/%24/g, '$')
.replace(/%2C/gi, ',')
.replace(/%20/g, '+')
.replace(/%5B/gi, '[')
.replace(/%5D/gi, ']')
}
export function bulidURL (url: string, params?: any) {
if (!params) {
return url
}
const parts: string[] = []
Object.keys(params).forEach((key) => {
let val = params[key]
if (val === null || typeof val === 'undefined') {
return
}
// /base/get?foo[]=bar&foo[]=baz
let values: string[]
if (Array.isArray(val)) {
values = val
key += '[]'
} else {
values = [val]
}
// /base/get?date=2019-04-01T05:55:39.030Z
values.forEach((val) => {
if (isDate(val)) {
val = val.toISOString()
} else if (isObject(val)) {
val = JSON.stringify(val)
}
parts.push(`${encode(key)}=${encode(val)}`)
})
})
let serializedParams = parts.join('&')
// 判断不是一个空数组
if (serializedParams) {
const markIndex = url.indexOf('#') // 判断是否有hash,如果有就把后面的都删除掉
if (markIndex !== -1) {
url = url.slice(0, markIndex)
}
// 有问号证明有参数了,直接加&,没问号就加个问号
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
}
return url
}
// helpers/util.ts
const toString = Object.prototype.toString
// 判断类型的方法 Object.prototype.toString.call()
// 类型保护 val is Date
export function isDate (val: any): val is Date {
return toString.call(val) === '[object Date]'
}
export function isObject (val: any): val is Object {
return val !== null && typeof val === 'object'
}
我们已经实现了 buildURL
函数,接下来我们来利用它实现 url
参数的处理逻辑
function axios (config: AxiosRequestConfig): void {
processConfig(config)
xhr(config)
}
function processConfig (config: AxiosRequestConfig): void {
config.url = transformUrl(config)
}
function transformUrl (config: AxiosRequestConfig): string {
const { url, params } = config
return bulidURL(url, params)
}
在执行 xhr
函数前,先执行 processConfig
方法,对 config
中的数据做处理,除了对 url
和 params
处理之外,未来还会处理其它属性。在 processConfig
函数内部,通过执行 transformUrl
函数修改了 config.url
,该函数内部调用了 buildURL
。
至此,对 url
参数处理逻辑就实现完了。
需求分析:我们通过执行 XMLHttpRequest
对象实例的 send
方法来发送请求,并通过该方法的参数设置请求 body
数据。 send
方法的参数支持 Document
和 BodyInit
类型,BodyInit
包括了 Blob
, BufferSource
, FormData
, URLSearchParams
, ReadableStream
、USVString
,当没有数据的时候,我们还可以传入 null
。但是我们最常用的场景还是传一个普通对象给服务端,这个时候 data
是不能直接传给 send
函数的,我们需要把它转换成 JSON 字符串。
根据需求分析,我们要实现一个工具函数,对 request 中的 data
做一层转换。我们在 helpers
目录新建 data.ts
文件。
import { isPlainObject } from './util'
export function transformRequest (data: any): any {
if (isPlainObject(data)) {
return JSON.stringify(data)
}
return data
}
// helpers/util.js
export function isPlainObject (val: any): val is Object {
return toString.call(val) === '[object Object]'
}
这里为什么要使用 isPlainObject
函数判断,而不用之前的 isObject
函数呢,因为 isObject
的判断方式,对于 FormData
、ArrayBuffer
这些类型,isObject
判断也为 true
,但是这些类型的数据我们是不需要做处理的,而 isPlainObject
的判断方式,只有我们定义的普通 JSON
对象才能满足。
// index.ts
import { transformRequest } from './helpers/data'
function processConfig (config: AxiosRequestConfig): void {
config.url = transformURL(config)
config.data = transformRequestData(config)
}
function transformRequestData (config: AxiosRequestConfig): any {
return transformRequest(config.data)
}
定义 transformRequestData
函数,去转换请求 body
的数据,内部调用了 transformRequest
方法。
然后我们在 processConfig
内部添加了这段逻辑,在处理完 url 后接着对 config
中的 data
做处理。
接下来编写demo,但是 base/post
请求的 response 里却返回的是一个空对象。
实际上是因为我们虽然执行 send
方法的时候把普通对象 data
转换成一个 JSON
字符串,但是我们请求 header
的 Content-Type
是 text/plain;charset=UTF-8
,导致了服务端接受到请求并不能正确解析请求 body
的数据。
需求分析:我们做了请求数据的处理,把 data
转换成了 JSON 字符串,但是数据发送到服务端的时候,服务端并不能正常解析我们发送的数据,因为并没有给请求 header
设置正确的 Content-Type
。所以首先我们要支持发送请求的时候,可以支持配置 headers
属性。并且在当我们传入的 data
为普通对象的时候,headers
如果没有配置 Content-Type
属性,需要自动设置 Content-Type
字段为:application/json;charset=utf-8
。
根据需求分析,我们要实现一个工具函数,对 request 中的 headers
做一层加工。我们在 helpers
目录新建 headers.ts
文件。
import { isPlainObject } from './util'
// 因为请求 header 属性是大小写不敏感的,比如我们之前的例子传入 header 的属性名 content-type 就是全小写的,所以我们先要把一些 header 属性名规范化
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
}
接下来实现请求 header 处理逻辑。
先修改一下 AxiosRequestConfig
接口类型的定义,添加 headers
这个可选属性。
// types/index.ts
export interface AxiosRequestConfig {
url: string
method?: Method
data?: any
params?: any
headers?: any
}
//index.ts
function processConfig (config: AxiosRequestConfig): void {
config.url = transformURL(config)
config.headers = transformHeaders(config)
config.data = transformRequestData(config)
}
function transformHeaders (config: AxiosRequestConfig) {
const { headers = {}, data } = config
return processHeaders(headers, data)
}
因为我们处理 header
的时候依赖了 data
,所以要在处理请求 body
数据之前处理请求 header
。
// xhr.ts
export default function xhr (config: AxiosRequestConfig): void {
const { data = null, url, method = 'get', headers } = config
const request = new XMLHttpRequest()
request.open(method.toUpperCase(), url, true)
Object.keys(headers).forEach((name) => {
// 当我们传入的 data 为空的时候,请求 header 配置 Content-Type 是没有意义的,于是我们把它删除。
if (data === null && name.toLowerCase() === 'content-type') {
delete headers[name]
} else {
request.setRequestHeader(name, headers[name])
}
})
request.send(data)
}
需求分析:我们发送的请求可以从网络层面接收到服务端返回的数据,但是代码层面并没有做任何关于返回数据的处理。我们希望能处理服务端响应的数据,并支持 Promise 链式调用的方式。
axios({
method: 'post',
url: '/base/post',
data: {
a: 1,
b: 2
}
}).then((res) => {
console.log(res)
})
我们可以拿到 res
对象,并且我们希望该对象包括:服务端返回的数据 data
,HTTP 状态码 status
,状态消息 statusText
,响应头 headers
、请求配置对象 config
以及请求的 XMLHttpRequest
对象实例 request
。
根据需求,我们可以定义一个 AxiosResponse
接口类型
export interface AxiosResponse {
data: any
status: number
statusText: string
headers: any
config: AxiosRequestConfig
request: any
}
另外,axios
函数返回的是一个 Promise
对象,我们可以定义一个 AxiosPromise
接口,它继承于 Promise
这个泛型接口
// 当 axios 返回的是 AxiosPromise 类型,那么 resolve 函数中的参数就是一个 AxiosResponse 类型
export interface AxiosPromise extends Promise<AxiosResponse> {
}
对于一个 AJAX 请求的 response
,我们是可以指定它的响应的数据类型的,通过设置 XMLHttpRequest
对象的 responseType
属性,于是我们可以给 AxiosRequestConfig
类型添加一个可选属性
export interface AxiosRequestConfig {
// ...
responseType?: XMLHttpRequestResponseType
}
responseType
的类型是一个 XMLHttpRequestResponseType
类型,它的定义是 "" | "arraybuffer" | "blob" | "document" | "json" | "text"
字符串字面量类型。
接下来实现获取响应数据逻辑。首先我们要在 xhr
函数添加 onreadystatechange
事件处理函数,并且让 xhr
函数返回的是 AxiosPromise
类型。
// 实现 axios 函数的 Promise 化
export default function xhr(config: AxiosRequestConfig): AxiosPromise {
return new Promise((resolve) => {
const { data = null, url, method = 'get', headers, responseType } = config
const request = new XMLHttpRequest() // 创建一个 request 实例
// 这里判断了如果config中配置了responseType,把它设置到request.responseType中。
if (responseType) {
request.responseType = responseType
}
request.open(method.toUpperCase(), url, true)
request.onreadystatechange = function handleLoad() {
if (request.readyState !== 4) {
return
}
const responseHeaders = 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) => {
// 当传入的 data 为空,请求 header 配置 Content-Type 是没有意义的,所以把它删除
if (data === null && name.toLowerCase() === 'content-type') {
delete headers[name]
} else {
request.setRequestHeader(name, headers[name])
}
})
request.send(data)
})
}
这样我们就实现了 axios
函数的 Promise 化。
需求分析:我们通过 XMLHttpRequest
对象的 getAllResponseHeaders
方法获取到的值是如下一段字符串
date: Fri, 05 Apr 2019 12:40:49 GMT
etag: W/"d-Ssxx4FRxEutDLwo2+xkkxKc4y0k"
connection: keep-alive
x-powered-by: Express
content-length: 13
content-type: application/json; charset=utf-8
每一行都是以回车符和换行符 \r\n
结束,它们是每个 header
属性的分隔符。对于上面这串字符串,我们希望最终解析成一个对象结构:
{
date: 'Fri, 05 Apr 2019 12:40:49 GMT'
etag: 'W/"d-Ssxx4FRxEutDLwo2+xkkxKc4y0k"',
connection: 'keep-alive',
'x-powered-by': 'Express',
'content-length': '13'
'content-type': 'application/json; charset=utf-8'
}
根据需求分析,我们要实现一个 parseHeaders
工具函数。
// helpers/headers.ts
export function parseHeaders(headers: string): any {
let parsed = Object.create(null)
if (!headers) {
return parsed
}
// split()方法用于把一个字符串分割成字符串数组
headers.split('\r\n').forEach(line => {
let [key, ...vals] = line.split(':')
key = key.trim().toLowerCase()
if (!key) {
return
}
let val = vals.join(':').trim()
parsed[key] = val
})
return parsed
}
然后我们在xhr.ts
中使用这个工具函数
const responseHeaders = parseHeaders(request.getAllResponseHeaders())
此时发现响应的 headers
字段从字符串解析成对象结构了,接下来解决对响应 data
字段的处理。
需求分析:在不设置 responseType
的情况下,当服务端返回的数据是字符串类型,把它转换成 JSON 对象。
根据需求分析,我们要实现一个 transformResponse
工具函数。
// helpers/data.ts
export function transformResponse(data: any): any {
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch (e) {
// do nothing
}
}
return data
}
然后在 index.ts 里去使用该方法
function axios(config: AxiosRequestConfig): AxiosPromise {
processConfig(config)
return xhr(config).then((res) => {
return transformResponseData(res)
})
}
function transformResponseData(res: AxiosResponse): AxiosResponse {
res.data = transformResponse(res.data)
return res
}
此时响应的 data
字段从字符串解析成 JSON 对象结构了。
至此, ts-axios
的基础功能已经实现完毕。不过到目前为止,我们都仅仅实现的是正常情况的逻辑,接下来我们要处理各种异常情况的逻辑。
我们希望程序能捕获到错误,做进一步的处理。
如果在请求的过程中发生任何错误,我们都可以在 reject
回调函数中捕获到。
当网络出现异常(比如不通)的时候发送请求会触发 XMLHttpRequest
对象实例的 error
事件,于是我们可以在 onerror
的事件回调函数中捕获此类错误。
我们在 xhr
函数中添加如下代码:
request.onerror = function handleError() {
reject(new Error('Network Error'))
}
我们可以设置某个请求的超时时间 timeout
,也就是当请求发送后超过某个时间后仍然没收到响应,则请求自动终止,并触发 timeout
事件。
请求默认的超时时间是 0,即永不超时。所以我们首先需要允许程序可以配置超时时间:
export interface AxiosRequestConfig {
// ...
timeout?: number
}
接着在 xhr
函数中添加如下代码:
const { /*...*/ timeout } = config
if (timeout) {
request.timeout = timeout
}
request.ontimeout = function handleTimeout() {
reject(new Error(`Timeout of ${timeout} ms exceeded`))
}
对于一个正常的请求,往往会返回 200-300 之间的 HTTP 状态码,对于不在这个区间的状态码,我们也把它们认为是一种错误的情况做处理。
request.onreadystatechange = function handleLoad() {
if (request.readyState !== 4) {
return
}
if (request.status === 0) {
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
}
handleResponse(response)
}
function handleResponse(response: AxiosResponse) {
if (response.status >= 200 && response.status < 300) {
resolve(response)
} else {
reject(new Error(`Request failed with status code ${response.status}`))
}
}
我们在 onreadystatechange
的回调函数中,添加了对 request.status
的判断,因为当出现网络错误或者超时错误的时候,该值都为 0。
接着我们在 handleResponse
函数中对 request.status
的值再次判断,如果是 2xx
的状态码,则认为是一个正常的请求,否则抛错。
至此对各种错误都做了处理,并把它们抛给了程序应用方,让他们对错误可以做进一步的处理。
但是这里的错误都仅仅是简单的 Error 实例,只有错误文本信息,并不包含是哪个请求、请求的配置、响应对象等其它信息。那么接下来我们会对错误信息做增强。
目前错误信息提供的非常有限,我们希望对外提供的信息不仅仅包含错误文本信息,还包括了请求对象配置 config
,错误代码 code
,XMLHttpRequest
对象实例 request
以及自定义响应对象 response
。
axios({
method: 'get',
url: '/error/timeout',
timeout: 2000
}).then((res) => {
console.log(res)
}).catch((e: AxiosError) => {
console.log(e.message)
console.log(e.request)
console.log(e.code)
})
首先先来定义 AxiosError
类型接口,用于外部使用。
// types/index.ts
export interface AxiosError extends Error {
config: AxiosRequestConfig
code?: string
request?: any
response?: AxiosResponse
isAxiosError: boolean
}
接着我们创建 error.ts
文件,然后实现 AxiosError
类,它是继承于 Error
类。
import { AxiosRequestConfig, AxiosResponse } from '../types'
export class AxiosError extends Error {
isAxiosError: boolean
config: AxiosRequestConfig
code?: string | null
request?: any
response?: AxiosResponse
constructor(
message: string,
config: AxiosRequestConfig,
code?: string | null,
request?: any,
response?: AxiosResponse
) {
super(message)
this.config = config
this.code = code
this.request = request
this.response = response
this.isAxiosError = true
// 这行代码是官方文档的推荐,目的是为了解决 TypeScript 继承一些内置对象的时候的坑
Object.setPrototypeOf(this, AxiosError.prototype)
}
}
// 为了方便使用,对外暴露了一个 createError 的工厂方法
export function createError(
message: string,
config: AxiosRequestConfig,
code?: string | null,
request?: any,
response?: AxiosResponse
): AxiosError {
const error = new AxiosError(message, config, code, request, response)
return error
}
接下来修改关于错误对象创建部分的逻辑
// xhr.ts
import { createError } from './helpers/error'
request.onerror = function handleError() {
reject(createError(
'Network Error',
config,
null, // code
request
))
}
request.ontimeout = function handleTimeout() {
reject(createError(
`Timeout of ${config.timeout} ms exceeded`,
config,
'ECONNABORTED', // code:software caused connection abort 软件引起的连接中止
request
))
}
function handleResponse(response: AxiosResponse) {
if (response.status >= 200 && response.status < 300) {
resolve(response)
} else {
reject(createError(
`Request failed with status code ${response.status}`,
config,
null,
request,
response
))
}
}
TypeScript 并不能把 e
参数推断为 AxiosError
类型,于是我们需要手动指明类型,为了让外部应用能引入 AxiosError
类型,我们也需要把它们导出。
我们创建 axios.ts
文件,把之前的 index.ts
的代码拷贝过去,然后修改 index.ts
的代码。
import axios from './axios'
export * from './types'
export default axios
这样我们在 demo 中就可以引入 AxiosError
类型了。
至此,关于 ts-axios
的异常处理逻辑就告一段落。下面我们会对 ts-axios
的接口做扩展,让它提供更多好用和方便的 API。
为了用户更加方便地使用 axios 发送请求,我们可以为所有支持请求方法扩展一些接口:
axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.options(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
如果使用了这些方法,我们就不必在 config
中指定 url
、method
、data
这些属性了。
从需求上来看,axios
不再单单是一个方法,更像是一个混合对象,本身是一个方法,又有很多方法属性,接下来我们就来实现这个混合对象。
根据需求分析,混合对象 axios
本身是一个函数,我们再实现一个包括它属性方法的类,然后把这个类的原型属性和自身属性再拷贝到 axios
上。先来给 axios
混合对象定义接口:
// types/index.ts
export interface Axios {
request(config: AxiosRequestConfig): AxiosPromise;
get(url: string, config?: AxiosRequestConfig): AxiosPromise;
delete(url: string, config?: AxiosRequestConfig): AxiosPromise;
head(url: string, config?: AxiosRequestConfig): AxiosPromise;
options(url: string, config?: AxiosRequestConfig): AxiosPromise;
post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise;
put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise;
patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise;
}
export interface AxiosInstance extends Axios {
(config: AxiosRequestConfig): AxiosPromise;
}
export interface AxiosRequestConfig {
url?: string;
// ...
}
首先定义一个 Axios
类型接口,它描述了 Axios
类中的公共方法,接着定义了 AxiosInstance
接口继承 Axios
,它就是一个混合类型的接口。
另外 AxiosRequestConfig
类型接口中的 url
属性变成了可选属性。
然后创建一个 Axios
类,来实现接口定义的公共方法。
我们创建一个 core
目录,用来存放发送请求核心流程的代码,在 core
目录下创建 Axios.ts
文件。
import { AxiosRequestConfig, AxiosPromise, Method } from "../types";
import dispatchRequest from "./dispatchRequest";
export default class Axios {
request(config: AxiosRequestConfig): AxiosPromise {
return dispatchRequest(config);
}
get(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData("get", url, config);
}
delete(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData("delete", url, config);
}
head(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData("head", url, config);
}
options(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData("options", url, config);
}
post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithData("post", url, data, config);
}
put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithData("put", url, data, config);
}
patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithData("patch", url, data, config);
}
_requestMethodWithoutData(
method: Method,
url: string,
config?: AxiosRequestConfig
) {
return this.request(
// Object.assign () 是对象的静态方法,可以用来复制对象的可枚举属性到目标对象,利用这个特性可以实现对象属性的合并。 Object.assign(target, sources)。如果只是想将两个或多个对象的属性合并到一起,不改变原有对象的属性,可以用一个空的对象作为 target 对象:Object.assign({},target,source)
Object.assign(config || {}, {
method,
url,
})
);
}
_requestMethodWithData(
method: Method,
url: string,
data?: any,
config?: AxiosRequestConfig
) {
return this.request(
Object.assign(config || {}, {
method,
url,
data,
})
);
}
}
其中 request
方法的功能和我们之前的 axios
函数功能是一致。axios
函数的功能就是发送请求,基于模块化编程的思想,我们把这部分功能抽出一个单独的模块,在 core
目录下创建 dispatchRequest
方法,把之前 axios.ts
的相关代码拷贝过去。另外我们把 xhr.ts
文件也迁移到 core
目录下。
// core/dispatchRequest.ts
import { AxiosPromise, AxiosRequestConfig, AxiosResponse } from "../types";
import xhr from "./xhr";
import { buildURL } from "../helpers/url";
import { transformRequest, transformResponse } from "../helpers/data";
import { processHeaders } from "../helpers/headers";
export default function dispatchRequest(
config: AxiosRequestConfig
): AxiosPromise {
processConfig(config);
return xhr(config).then(
res => {
return transformResponseData(res)
},
// 当我们发送请求失败后,也能把响应数据转换成 JSON 格式
e => {
if (e && e.response) {
e.response = transformResponseData(e.response)
}
return Promise.reject(e)
}
)
}
function processConfig(config: AxiosRequestConfig): void {
config.url = transformURL(config);
config.headers = transformHeaders(config);
config.data = transformRequestData(config);
}
function transformURL(config: AxiosRequestConfig): string {
const { url, params } = config;
return buildURL(url, params);
}
function transformRequestData(config: AxiosRequestConfig): any {
return transformRequest(config.data);
}
function transformHeaders(config: AxiosRequestConfig) {
const { headers = {}, data } = config;
return processHeaders(headers, data);
}
function transformResponseData(res: AxiosResponse): AxiosResponse {
res.data = transformResponse(res.data);
return res;
}
回到 Axios.ts
文件,对于 get
、delete
、head
、options
、post
、patch
、put
这些方法,都是对外提供的语法糖,内部都是通过调用 request
方法实现发送请求,只不过在调用之前对 config
做了一层合并处理。
接下来实现混合对象。首先这个对象是一个函数,其次这个对象要包括 Axios
类的所有原型属性和实例属性,我们首先来实现一个辅助函数 extend
。
// helpers/util.ts
export function extend(to: T, from: U): T & U {
for (const key in from) {
(to as T & U)[key] = from[key] as any;
}
return to as T & U;
}
extend
方法的实现用到了交叉类型,并且用到了类型断言。
extend
的最终目的是把 from
里的属性都扩展到 to
中,包括原型上的属性。
接下来对 axios.ts
文件做修改,我们用工厂模式去创建一个 axios
混合对象。
import { AxiosInstance } from "./types";
import Axios from "./core/Axios";
import { extend } from "./helpers/util";
function createInstance(): AxiosInstance {
const context = new Axios(); // 实例化了 Axios 实例 context
const instance = Axios.prototype.request.bind(context); // 创建 instance 指向 Axios.prototype.request 方法,因为用了 this,所以绑定了上下文 context
extend(instance, context); // 通过 extend 方法把 context 中的原型方法和实例方法全部拷贝到 instance 上
// 这样就实现了一个混合对象: instance 本身是一个函数,又拥有了 Axios 类的所有原型和实例属性,最终把这个 instance 返回
return instance as AxiosInstance; // 不能正确推断 instance 的类型,把它断言成 AxiosInstance 类型
}
const axios = createInstance();
export default axios;
这样我们就可以通过 createInstance
工厂函数创建了 axios
,当直接调用 axios
方法就相当于执行了 Axios
类的 request
方法发送请求,当然我们也可以调用 axios.get
、axios.post
等方法。
需求分析:目前我们的 axios 函数只支持传入 1 个参数,如下:
axios({
url: "/extend/post",
method: "post",
data: {
msg: "hi",
},
});
我们希望该函数也能支持传入 2 个参数,如下:
axios("/extend/post", {
method: "post",
data: {
msg: "hello",
},
});
第一个参数是 url
,第二个参数是 config
,这个函数有点类似 axios.get
方法支持的参数类型,不同的是如果我们想要指定 HTTP 方法类型,仍然需要在 config
传入 method
。
首先我们要修改 AxiosInstance
的类型定义。
types/index.ts
:
export interface AxiosInstance extends Axios {
(config: AxiosRequestConfig): AxiosPromise;
(url: string, config?: AxiosRequestConfig): AxiosPromise;
}
我们增加一种函数的定义,它支持 2 个参数,其中 url
是必选参数,config
是可选参数。
由于 axios
函数实际上指向的是 request
函数,所以我们来修改 request
函数的实现。
core/Axios.ts
:
// 函数重载
request(url: any, config?: any): AxiosPromise {
if (typeof url === 'string') {
if (!config) {
config = {} // 可能不传,如果为空则构造一个空对象
}
config.url = url
} else {
config = url // 如果 url 不是字符串类型,则说明传入的就是单个参数,且 url 就是 config
}
return dispatchRequest(config)
}
我们把 request
函数的参数改成 2 个,url
和 config
都是 any
类型,config
还是可选参数。
接着在函数体我们判断 url
是否为字符串类型,一旦它为字符串类型,则继续对 config
判断,因为它可能不传,如果为空则构造一个空对象,然后把 url
添加到 config.url
中。如果 url
不是字符串类型,则说明我们传入的就是单个参数,且 url
就是 config
,因此把 url
赋值给 config
。
这里要注意的是,我们虽然修改了 request
的实现,支持了 2 种参数,但是我们对外提供的 request
接口仍然不变,可以理解为这仅仅是内部的实现的修改,与对外接口不必一致,只要保留实现兼容接口即可
需求分析:通常情况下,我们会把后端返回数据格式单独放入一个接口中:
// 请求接口数据
export interface ResponseData<T = any> {
/**
* 状态码
* @type { number }
*/
code: number
/**
* 数据
* @type { T }
*/
result: T
/**
* 消息
* @type { string }
*/
message: string
}
我们可以把 API 抽离成单独的模块:
import { ResponseData } from './interface.ts';
export function getUser<T>() {
return axios.get<ResponseData<T>>('/somepath')
.then(res => res.data)
.catch(err => console.error(err))
}
接着我们写入返回的数据类型 User
,这可以让 TypeScript 顺利推断出我们想要的类型:
interface User {
name: string
age: number
}
async function test() {
// user 被推断出为
// {
// code: number,
// result: { name: string, age: number },
// message: string
// }
const user = await getUser<User>()
}
根据需求分析,我们需要给相关的接口定义添加泛型参数。
types/index.ts
:
export interface AxiosResponse<T = any> {
data: T
status: number
statusText: string
headers: any
config: AxiosRequestConfig
request: any
}
export interface AxiosPromise<T = any> extends Promise<AxiosResponse<T>> {
}
export interface Axios {
request<T = any>(config: AxiosRequestConfig): AxiosPromise<T>
get<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>
delete<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>
head<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>
options<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>
patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>
}
export interface AxiosInstance extends Axios {
<T = any>(config: AxiosRequestConfig): AxiosPromise<T>
<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>
}
这里我们先给 AxiosResponse
接口添加了泛型参数 T
,T=any
表示泛型的类型参数默认值为 any
。
接着我们为 AxiosPromise
、Axios
以及 AxiosInstance
接口都加上了泛型参数。我们可以看到这些请求的返回类型都变成了 AxiosPromise
,也就是 Promise
,这样我们就可以从响应中拿到了类型 T
了。
需求分析:我们希望能对请求的发送和响应做拦截,也就是在发送请求之前和接收到响应之后做一些额外逻辑。
我们希望设计的拦截器的使用方式如下:
// 添加一个请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前可以做一些事情
return config;
}, function (error) {
// 处理请求错误
return Promise.reject(error);
});
// 添加一个响应拦截器
axios.interceptors.response.use(function (response) {
// 处理响应数据
return response;
}, function (error) {
// 处理响应错误
return Promise.reject(error);
});
在 axios
对象上有一个 interceptors
对象属性,该属性又有 request
和 response
2 个属性,它们都有一个 use
方法,use
方法支持 2 个参数,第一个参数类似 Promise 的 resolve
函数,第二个参数类似 Promise 的 reject
函数。我们可以在 resolve
函数和 reject
函数中执行同步代码或者是异步代码逻辑。
并且我们是可以添加多个拦截器的,拦截器的执行顺序是链式依次执行的方式。对于 request
拦截器,后添加的拦截器会在请求前的过程中先执行;对于 response
拦截器,先添加的拦截器会在响应后先执行。
axios.interceptors.request.use(config => {
config.headers.test += '1'
return config
})
axios.interceptors.request.use(config => {
config.headers.test += '2'
return config
})
此外,我们也可以支持删除某个拦截器,如下:
const myInterceptor = axios.interceptors.request.use(function () {/*...*/})
axios.interceptors.request.eject(myInterceptor)
先了解一下拦截器工作流程,整个过程是一个链式调用的方式,每个拦截器都可以支持同步和异步处理。
因此我们自然而然地就联想到使用 Promise 链的方式来实现整个调用过程。
在这个 Promise 链的执行过程中,请求拦截器 resolve
函数处理的是 config
对象,而相应拦截器 resolve
函数处理的是 response
对象。
在了解了拦截器工作流程后,我们先要创建一个拦截器管理类,允许我们去添加 删除和遍历拦截器。
根据需求,axios
拥有一个 interceptors
对象属性,该属性又有 request
和 response
2 个属性,它们对外提供一个 use
方法来添加拦截器,我们可以把这俩属性看做是一个拦截器管理对象。
use
方法支持 2 个参数,第一个是 resolve
函数,第二个是 reject
函数,对于 resolve
函数的参数,请求拦截器是 AxiosRequestConfig
类型的,而响应拦截器是 AxiosResponse
类型的;而对于 reject
函数的参数类型则是 any
类型的。
根据上述分析,我们先来定义一下拦截器管理对象对外的接口。
// types/index.ts
export interface AxiosInterceptorManager {
use(resolved: ResolvedFn, rejected?: RejectedFn): number
eject(id: number): void
}
export interface ResolvedFn {
(val: T): T | Promise
}
export interface RejectedFn {
(error: any): any
}
这里我们定义了 AxiosInterceptorManager
泛型接口,因为对于 resolve
函数的参数,请求拦截器和响应拦截器是不同的。
import { ResolvedFn, RejectedFn } from '../types'
interface Interceptor<T> {
resolved: ResolvedFn<T>
rejected?: RejectedFn
}
export default class InterceptorManager<T> {
// 内部维护一个私有属性 interceptors,它是一个数组,用来存储拦截器
private interceptors: Array<Interceptor<T> | null>
constructor() {
this.interceptors = []
}
// use 接口就是添加拦截器到 interceptors 中,并返回一个 id 用于删除
use(resolved: ResolvedFn<T>, rejected?: RejectedFn): number {
this.interceptors.push({
resolved,
rejected
})
return this.interceptors.length - 1
}
// forEach 接口就是遍历 interceptors 用的,它支持传入一个函数,遍历过程中会调用该函数,并把每一个 interceptor 作为该函数的参数传入
forEach(fn: (interceptor: Interceptor<T>) => void): void {
this.interceptors.forEach(interceptor => {
if (interceptor !== null) {
fn(interceptor)
}
})
}
// eject 就是删除一个拦截器,通过传入拦截器的 id 删除
eject(id: number): void {
if (this.interceptors[id]) {
this.interceptors[id] = null // 不能用数组删除,长度会乱
}
}
}
我们定义了一个 InterceptorManager
泛型类,内部维护了一个私有属性 interceptors
,它是一个数组,用来存储拦截器。该类还对外提供了 3 个方法,其中 use
接口就是添加拦截器到 interceptors
中,并返回一个 id
用于删除;forEach
接口就是遍历 interceptors
用的,它支持传入一个函数,遍历过程中会调用该函数,并把每一个 interceptor
作为该函数的参数传入;eject
就是删除一个拦截器,通过传入拦截器的 id
删除。
当我们实现好拦截器管理类,接下来就是在 Axios
中定义一个 interceptors
属性,它的类型如下:
interface Interceptors {
request: InterceptorManager<AxiosRequestConfig>
response: InterceptorManager<AxiosResponse>
}
export default class Axios {
interceptors: Interceptors
constructor() {
this.interceptors = {
request: new InterceptorManager<AxiosRequestConfig>(),
response: new InterceptorManager<AxiosResponse>()
}
}
}
Interceptors
类型拥有 2 个属性,一个请求拦截器管理类实例,一个是响应拦截器管理类实例。我们在实例化 Axios
类的时候,在它的构造器去初始化这个 interceptors
实例属性。
接下来,我们修改 request
方法的逻辑,添加拦截器链式调用的逻辑:
core/Axios.ts
:
interface PromiseChain {
resolved: ResolvedFn | ((config: AxiosRequestConfig) => AxiosPromise)
rejected?: RejectedFn
}
request(url: any, config?: any): AxiosPromise {
if (typeof url === 'string') {
if (!config) {
config = {}
}
config.url = url
} else {
config = url
}
// 构造 PromiseChain 类型的数组 chain,并把 dispatchRequest 函数赋值给 resolved 属性
const chain: PromiseChain[] = [{
resolved: dispatchRequest,
rejected: undefined
}]
// 注意拦截器的执行顺序,对于请求拦截器,先执行后添加的,再执行先添加的;
// 而对于响应拦截器,先执行先添加的,后执行后添加的。
// 先遍历请求拦截器插入到 chain 的前面
this.interceptors.request.forEach(interceptor => {
chain.unshift(interceptor)
})
// 再遍历响应拦截器插入到 chain 后面
this.interceptors.response.forEach(interceptor => {
chain.push(interceptor)
})
// 定义一个已经 resolve 的 promise
let promise = Promise.resolve(config)
// 循环这个 chain,拿到每个拦截器对象,把它们的 resolved 函数和 rejected 函数添加到 promise.then 的参数中,这样就相当于通过 Promise 的链式调用方式,实现了拦截器一层层的链式调用的效果
while (chain.length) {
const { resolved, rejected } = chain.shift()!
promise = promise.then(resolved, rejected)
}
return promise
}
首先,构造一个 PromiseChain
类型的数组 chain
,并把 dispatchRequest
函数赋值给 resolved
属性;接着先遍历请求拦截器插入到 chain
的前面;然后再遍历响应拦截器插入到 chain
后面。
接下来定义一个已经 resolve 的 promise
,循环这个 chain
,拿到每个拦截器对象,把它们的 resolved
函数和 rejected
函数添加到 promise.then
的参数中,这样就相当于通过 Promise 的链式调用方式,实现了拦截器一层层的链式调用的效果。
注意我们拦截器的执行顺序,对于请求拦截器,先执行后添加的,再执行先添加的;而对于响应拦截器,先执行先添加的,后执行后添加的。
至此,我们给 ts-axios
实现了拦截器功能,它是一个非常实用的功能,在实际工作中我们可以利用它做一些需求如登录权限认证。
我们目前通过 axios
发送请求,往往会传入一堆配置,但是我们也希望 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
属性。
接下来,先实现默认配置定义
// defaults.ts
import { AxiosRequestConfig } from './types'
// 定义 defaults 常量,包含默认请求的方法、超时时间,以及 headers 配置
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
然后要给 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>()
}
}
// ...
}
我们给 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)
这样我们就可以在执行 createInstance
创建 axios
对象的时候,把默认配置传入了。
定义了默认配置后,我们发送每个请求的时候需要把自定义配置和默认配置做合并,它并不是简单的 2 个普通对象的合并,对于不同的字段合并,会有不同的合并策略。
我们在 core/mergeConfig.ts
中实现合并方法,在 mergeField
方法中,我们会针对不同的属性使用不同的合并策略。
// 合并方法的整体思路就是对 config1 和 config2 中的属性遍历,执行 mergeField 方法做合并,这里 config1 代表默认配置,config2 代表自定义配置
// 策 略 模 式
import { AxiosRequestConfig } from "../types";
import { isPlainObject,deepMerge } from "../helpers/util";
const strats = Object.create(null)
// 这是大部分属性的合并策略,它很简单,如果有 val2 则返回 val2,否则返回 val1,也就是如果自定义配置中定义了某个属性,就采用自定义的,否则就用默认配置。
function defaultStrat(val1: any,val2: any): any{
return typeof val2 !== 'undefined' ? val2 : val1
}
// 这是对于一些属性如 url、params、data的合并策略
function formVal2Strat(val1: any,val2: any): any{
if(typeof val2 !== 'undefined'){
return val2
}
}
// 因为对于 url、params、data 这些属性,默认配置显然是没有意义的,它们是和每个请求强相关的,所以我们只从自定义配置中获取
const stratKeysFromVal2 = ['url','params','data']
stratKeysFromVal2.forEach(key => {
strats[key] = formVal2Strat
})
// 对于 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 {
return val1
}
}
// const stratKeysDeepMerge = ['headers']
// 修改合并规则,因为 auth 也是一个对象格式,所以它的合并规则是 deepMergeStrat
const stratKeysDeepMerge = ['headers', 'auth']
stratKeysDeepMerge.forEach(key => {
strats[key] = deepMergeStrat
})
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]) { // config2中没有
mergeField(key)
}
}
function mergeField(key: string): void {
const strat = strats[key] || defaultStrat
config[key] = strat(config1[key], config2![key])
}
return config
}
遍历过程中,我们会通过 config2[key]
这种索引的方式访问,所以需要给 AxiosRequestConfig
的接口定义添加一个字符串索引签名:
export interface AxiosRequestConfig {
// ...
[propName: string]: any
}
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
}
最后我们在 request
方法里添加合并配置的逻辑:
config = mergeConfig(this.defaults, config)
经过合并后的配置中的 headers
是一个复杂对象,多了 common
、post
、get
等属性,而这些属性中的值才是我们要真正添加到请求 header
中的。
举个例子:
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'
}
这里要注意的是,对于 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
}
我们可以通过 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!)
}
这样确保了我们配置中的 headers
是可以正确添加到请求 header
中的。
官方的 axios 库 给默认配置添加了 transformRequest
和 transformResponse
两个字段,它们的值是一个数组或者是一个函数。
其中 transformRequest
允许你在将请求数据发送到服务器之前对其进行修改,这只适用于请求方法 put
、post
和 patch
,如果值是数组,则数组中的最后一个函数必须返回一个字符串或 FormData
、URLSearchParams
、Blob
等类型作为 xhr.send
方法的参数,而且在 transform
过程中可以修改 headers
对象。
而 transformResponse
允许你在把响应数据传递给 then
或者 catch
之前对它们进行修改。
当值为数组的时候,数组的每一个函数都是一个转换函数,数组中的函数就像管道一样依次执行,前者的输出作为后者的输入。
举个例子:
axios({
transformRequest: [
function(data) {
return qs.stringify(data);
},
...axios.defaults.transformRequest,
],
transformResponse: [
axios.defaults.transformResponse,
function(data) {
if (typeof data === "object") {
data.b = 2;
}
return data;
},
],
url: "/config/post",
method: "post",
data: {
a: 1,
},
});
先修改 AxiosRequestConfig
的类型定义,添加 transformRequest
和 transformResponse
俩个可选属性。
types/index.ts
:
export interface AxiosRequestConfig {
// ...
transformRequest?: AxiosTransformer | AxiosTransformer[];
transformResponse?: AxiosTransformer | AxiosTransformer[];
}
export interface AxiosTransformer {
(data: any, headers?: any): any;
}
接着修改默认配置,如下:
defaults.ts
:
import { processHeaders } from "./helpers/headers";
import { transformRequest, transformResponse } from "./helpers/data";
const defaults: AxiosRequestConfig = {
// ...
transformRequest: [
function(data: any, headers: any): any {
processHeaders(headers, data);
return transformRequest(data);
},
],
transformResponse: [
function(data: any): any {
return transformResponse(data);
},
],
};
我们把之前对请求数据和响应数据的处理逻辑,放到了默认配置中,也就是默认处理逻辑。
接下来,我们就要重构之前写的对请求数据和响应数据的处理逻辑了。由于我们可能会编写多个转换函数,我们先定义一个 transform
函数来处理这些转换函数的调用逻辑。
import { AxiosTransformer } from "../types";
export default function transform(
data: any,
headers: any,
fns?: AxiosTransformer | AxiosTransformer[]
): any {
if (!fns) {
return data;
}
if (!Array.isArray(fns)) {
fns = [fns];
}
fns.forEach((fn) => {
data = fn(data, headers);
});
return data;
}
transform
函数中接收 data
、headers
、fns
3 个参数,其中 fns
代表一个或者多个转换函数,内部逻辑很简单,遍历 fns
,执行这些转换函数,并且把 data
和 headers
作为参数传入,每个转换函数返回的 data
会作为下一个转换函数的参数 data
传入。
接下来修改对请求数据和响应数据的处理逻辑。
dispatchRequest.ts
:
import transform from "./transform";
function processConfig(config: AxiosRequestConfig): void {
config.url = transformURL(config);
config.data = transform(config.data, config.headers, config.transformRequest);
config.headers = flattenHeaders(config.headers, config.method!);
}
function transformResponseData(res: AxiosResponse): AxiosResponse {
res.data = transform(res.data, res.headers, res.config.transformResponse);
return res;
}
我们把对请求数据的处理和对响应数据的处理改成使用 transform
函数实现,并把配置中的 transformRequest
及 transformResponse
分别传入。
至此,我们就实现了请求和响应的配置化。
到目前为止 axios 都是一个单例,一旦我们修改了 axios 的默认配置,会影响所有的请求。官网提供了一个 axios.create
的工厂方法允许我们创建一个新的 axios
实例,同时允许我们传入新的配置和默认配置合并,并做为新的默认配置。
由于 axios
扩展了一个静态接口,因此我们先来修改接口类型定义。
types/index.ts
:
export interface AxiosStatic extends AxiosInstance {
create(config?: AxiosRequestConfig): AxiosInstance;
}
create
函数可以接受一个 AxiosRequestConfig
类型的配置,作为默认配置的扩展,也可以接受不传参数。
接着我们来实现 axios.create
静态方法。
axios.ts
:
function createInstance(config: AxiosRequestConfig): AxiosStatic {
const context = new Axios(config);
const instance = Axios.prototype.request.bind(context);
extend(instance, context);
return instance as AxiosStatic;
}
axios.create = function create(config) {
return createInstance(mergeConfig(defaults, config));
};
内部调用了 createInstance
函数,并且把参数 config
与 defaults
合并,作为新的默认配置。注意这里我们需要 createInstance
函数的返回值类型为 AxiosStatic
。
至此我们实现了 axios.create
静态接口的扩展,整个 ts-axios
的配置化也告一段落。
官方 axios 库还支持了对请求取消的能力,在发送请求前以及请求发送出去未响应前都可以取消该请求。
有些场景下,我们希望能主动取消请求,比如常见的搜索框案例,在用户输入过程中,搜索框的内容也在不断变化,正常情况每次变化我们都应该向服务端发送一次请求。但是当用户输入过快的时候,我们不希望每次变化请求都发出去,通常一个解决方案是前端用 debounce 的方案,比如延时 200ms 发送请求。这样当用户连续输入的字符,只要输入间隔小于 200ms,前面输入的字符都不会发请求。
但是还有一种极端情况是后端接口很慢,比如超过 1s 才能响应,这个时候即使做了 200ms 的 debounce,但是在我慢慢输入(每个输入间隔超过 200ms)的情况下,在前面的请求没有响应前,也有可能发出去多个请求。因为接口的响应时长是不定的,如果先发出去的请求响应时长比后发出去的请求要久一些,后请求的响应先回来,先请求的响应后回来,就会出现前面请求响应结果覆盖后面请求响应结果的情况,那么就乱了。因此在这个场景下,我们除了做 debounce,还希望后面的请求发出去的时候,如果前面的请求还没有响应,我们可以把前面的请求取消。
从 axios 的取消接口设计层面,我们希望做如下的设计:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (e) {
if (axios.isCancel(e)) {
console.log('Request canceled', e.message);
} else {
// 处理错误
}
});
// 取消请求 (请求原因是可选的)
source.cancel('Operation canceled by the user.');
我们给 axios
添加一个 CancelToken
的对象,它有一个 source
方法可以返回一个 source
对象,source.token
是在每次请求的时候传给配置对象中的 cancelToken
属性,然后在请求发出去之后,我们可以通过 source.cancel
方法取消请求。
我们还支持另一种方式的调用:
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
cancel = c;
})
});
// 取消请求
cancel();
axios.CancelToken
是一个类,我们直接把它实例化的对象传给请求配置中的 cancelToken
属性,CancelToken
的构造函数参数支持传入一个 executor
方法,该方法的参数是一个取消函数 c
,我们可以在 executor
方法执行的内部拿到这个取消函数 c
,赋值给我们外部定义的 cancel
变量,之后我们可以通过调用这个 cancel
方法来取消请求。
通过需求分析,我们知道想要实现取消某次请求,我们需要为该请求配置一个 cancelToken
,然后在外部调用一个 cancel
方法。
请求的发送是一个异步过程,最终会执行 xhr.send
方法,xhr
对象提供了 abort
方法,可以把请求取消。因为我们在外部是碰不到 xhr
对象的,所以我们想在执行 cancel
的时候,去执行 xhr.abort
方法。
现在就相当于我们在 xhr
异步请求过程中,插入一段代码,当我们在外部执行 cancel
函数的时候,会驱动这段代码的执行,然后执行 xhr.abort
方法取消请求。
我们可以利用 Promise 实现异步分离,也就是在 cancelToken
中保存一个 pending
状态的 Promise 对象,然后当我们执行 cancel
方法的时候,能够访问到这个 Promise 对象,把它从 pending
状态变成 resolved
状态,这样我们就可以在 then
函数中去实现取消请求的逻辑。类似如下的代码:
if (cancelToken) {
cancelToken.promise
.then(reason => {
request.abort()
reject(reason)
})
}
接下来,我们就来实现这个 CancelToken 类,先来看一下接口定义:
export interface AxiosRequestConfig {
// ...
cancelToken?: CancelToken
}
export interface CancelToken {
promise: Promise<string>
reason?: string
}
export interface Canceler {
(message?: string): void
}
export interface CancelExecutor {
(cancel: Canceler): void
}
其中 CancelToken
是实例类型的接口定义,Canceler
是取消方法的接口定义,CancelExecutor
是 CancelToken
类构造函数参数的接口定义。
我们单独创建 cancel
目录来管理取消相关的代码,在 cancel
目录下创建 CancelToken.ts
文件:
import { CancelExecutor } from '../types'
interface ResolvePromise {
(reason?: string): void
}
export default class CancelToken {
promise: Promise<string>
reason?: string
constructor(executor: CancelExecutor) {
let resolvePromise: ResolvePromise
this.promise = new Promise<string>(resolve => {
resolvePromise = resolve
})
executor(message => {
if (this.reason) {
return
}
this.reason = message
resolvePromise(this.reason)
})
}
}
在 CancelToken
构造函数内部,实例化一个 pending
状态的 Promise 对象,然后用一个 resolvePromise
变量指向 resolve
函数。接着执行 executor
函数,传入一个 cancel
函数,在 cancel
函数内部,会调用 resolvePromise
把 Promise 对象从 pending
状态变为 resolved
状态。
接着我们在 xhr.ts
中插入一段取消请求的逻辑。
core/xhr.ts
:
const { /*....*/ cancelToken } = config
if (cancelToken) {
cancelToken.promise.then(reason => {
request.abort()
reject(reason)
})
}
这样就满足了第二种使用方式,接着我们要实现第一种使用方式,给 CancelToken
扩展静态接口。
export interface CancelTokenSource {
token: CancelToken
cancel: Canceler
}
export interface CancelTokenStatic {
new(executor: CancelExecutor): CancelToken
source(): CancelTokenSource
}
其中 CancelTokenSource
作为 CancelToken
类静态方法 source
函数的返回值类型,CancelTokenStatic
则作为 CancelToken
类的类类型。
cancel/CancelToken.ts
:
export default class CancelToken {
// ...
static source(): CancelTokenSource {
let cancel!: Canceler
const token = new CancelToken(c => {
cancel = c
})
return {
cancel,
token
}
}
}
source
的静态方法很简单,定义一个 cancel
变量实例化一个 CancelToken
类型的对象,然后在 executor
函数中,把 cancel
指向参数 c
这个取消函数。
这样就满足了我们第一种使用方式,但是在第一种使用方式的例子中,我们在捕获请求的时候,通过 axios.isCancel
来判断这个错误参数 e 是不是一次取消请求导致的错误,接下来我们对取消错误的原因做一层包装,并且把给 axios
扩展静态方法
export interface Cancel {
message?: string
}
export interface CancelStatic {
new(message?: string): Cancel
}
export interface AxiosStatic extends AxiosInstance {
create(config?: AxiosRequestConfig): AxiosInstance
CancelToken: CancelTokenStatic
Cancel: CancelStatic
isCancel: (value: any) => boolean
}
其中 Cancel
是实例类型的接口定义,CancelStatic
是类类型的接口定义,并且我们给 axios
扩展了多个静态方法。
在 cancel
目录下创建 Cancel.ts
文件。
export default class Cancel {
message?: string
constructor(message?: string) {
this.message = message
}
}
export function isCancel(value: any): boolean {
return value instanceof Cancel
}
Cancel
类非常简单,拥有一个 message
的公共属性。isCancel
方法也非常简单,通过 instanceof
来判断传入的值是不是一个 Cancel
对象。
接着我们对 CancelToken
类中的 reason
类型做修改,把它变成一个 Cancel
类型的实例。
先修改定义部分。
types/index.ts
:
export interface CancelToken {
promise: Promise<Cancel>
reason?: Cancel
}
再修改实现部分:
import Cancel from './Cancel'
interface ResolvePromise {
(reason?: Cancel): void
}
export default class CancelToken {
promise: Promise<Cancel>
reason?: Cancel
constructor(executor: CancelExecutor) {
let resolvePromise: ResolvePromise
this.promise = new Promise<Cancel>(resolve => {
resolvePromise = resolve
})
executor(message => {
if (this.reason) {
return
}
this.reason = new Cancel(message)
resolvePromise(this.reason)
})
}
}
接下来我们给 axios
扩展一些静态方法,供用户使用。
axios.ts
:
import CancelToken from './cancel/CancelToken'
import Cancel, { isCancel } from './cancel/Cancel'
axios.CancelToken = CancelToken
axios.Cancel = Cancel
axios.isCancel = isCancel
除此之外,我们还需要实现一些额外逻辑,比如当一个请求携带的 cancelToken
已经被使用过,那么我们甚至都可以不发送这个请求,只需要抛一个异常即可,并且抛异常的信息就是我们取消的原因,所以我们需要给 CancelToken
扩展一个方法。
先修改定义部分。
types/index.ts
:
export interface CancelToken {
promise: Promise<Cancel>
reason?: Cancel
throwIfRequested(): void
}
添加一个 throwIfRequested
方法,接下来实现它:
cancel/CancelToken.ts
:
export default class CancelToken {
// ...
throwIfRequested(): void {
if (this.reason) {
throw this.reason
}
}
}
判断如果存在 this.reason
,说明这个 token
已经被使用过了,直接抛错。
接下来在发送请求前增加一段逻辑。
core/dispatchRequest.ts
:
export default function dispatchRequest(config: AxiosRequestConfig): AxiosPromise {
throwIfCancellationRequested(config)
processConfig(config)
// ...
}
function throwIfCancellationRequested(config: AxiosRequestConfig): void {
if (config.cancelToken) {
config.cancelToken.throwIfRequested()
}
}
发送请求前检查一下配置的 cancelToken 是否已经使用过了,如果已经被用过则不用法请求,直接抛异常。
需求分析:有些时候我们会发一些跨域请求,比如 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
:
export interface AxiosRequestConfig {
// ...
withCredentials?: boolean;
}
然后修改请求发送前的逻辑。
core/xhr.ts
:
const { /*...*/ withCredentials } = config;
if (withCredentials) {
request.withCredentials = true;
}
XSRF 又名 CSRF,跨站请求伪造,它是前端常见的一种攻击方式。
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
名称。
axios.get('/more/get',{
xsrfCookieName: 'XSRF-TOKEN', // default
xsrfHeaderName: 'X-XSRF-TOKEN' // default
}).then(res => {
console.log(res)
})
我们提供 xsrfCookieName
和 xsrfHeaderName
的默认值,当然用户也可以根据自己的需求在请求中去配置 xsrfCookieName
和 xsrfHeaderName
。
先修改 AxiosRequestConfig
的类型定义。
types/index.ts
:
export interface AxiosRequestConfig {
// ...
xsrfCookieName?: string
xsrfHeaderName?: string
}
然后修改默认配置。
defaults.ts
:
const defaults: AxiosRequestConfig = {
// ...
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
}
接下来我们要做三件事:
withCredentials
为 true
或者是同域请求,我们才会请求 headers
添加 xsrf
相关的字段。xsrf
的 token
值。headers
的 xsrf
相关字段中。我们先来实现同域请求的判断。
helpers/url.ts
:
interface URLOrigin {
protocol: string
host: string
}
export function isURLSameOrigin(requestURL: string): boolean {
const parsedOrigin = resolveURL(requestURL)
return (
parsedOrigin.protocol === currentOrigin.protocol && parsedOrigin.host === currentOrigin.host
)
}
const urlParsingNode = document.createElement('a')
const currentOrigin = resolveURL(window.location.href)
function resolveURL(url: string): URLOrigin {
urlParsingNode.setAttribute('href', url)
const { protocol, host } = urlParsingNode
return {
protocol,
host
}
}
同域名的判断主要利用了一个技巧,创建一个 a 标签的 DOM,然后设置 href
属性为我们传入的 url
,然后可以获取该 DOM 的 protocol
、host
。当前页面的 url
和请求的 url
都通过这种方式获取,然后对比它们的 protocol
和 host
是否相同即可。
接着实现 cookie 的读取。
helpers/cookie.ts
:
const cookie = {
read(name: string): string | null {
const match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)'))
return match ? decodeURIComponent(match[3]) : null
}
}
export default cookie
cookie
的读取逻辑很简单,利用了正则表达式可以解析到 name
对应的值。
最后实现完整的逻辑。
core/xhr.ts
:
const {
/*...*/
xsrfCookieName,
xsrfHeaderName
} = config
if ((withCredentials || isURLSameOrigin(url!)) && xsrfCookieName){
const xsrfValue = cookie.read(xsrfCookieName)
if (xsrfValue) {
headers[xsrfHeaderName!] = xsrfValue
}
}
有些时候,当我们上传文件或者是请求一个大体积数据的时候,希望知道实时的进度,甚至可以基于此做一个进度条的展示。
我们希望给 axios
的请求配置提供 onDownloadProgress
和 onUploadProgress
2 个函数属性,用户可以通过这俩函数实现对下载进度和上传进度的监控。
xhr
对象提供了一个 progress
事件,我们可以监听此事件对数据的下载进度做监控;另外,xhr.uplaod
对象也提供了 progress
事件,我们可以基于此对上传进度做监控。
首先修改一下类型定义。
types/index.ts
:
export interface AxiosRequestConfig {
// ...
onDownloadProgress?: (e: ProgressEvent) => void
onUploadProgress?: (e: ProgressEvent) => void
}
接着在发送请求前,给 xhr
对象添加属性。
core/xhr.ts
:
const {
/*...*/
onDownloadProgress,
onUploadProgress
} = config
if (onDownloadProgress) {
request.onprogress = onDownloadProgress
}
if (onUploadProgress) {
request.upload.onprogress = onUploadProgress
}
另外,如果请求的数据是 FormData
类型,我们应该主动删除请求 headers
中的 Content-Type
字段,让浏览器自动根据请求数据设置 Content-Type
。比如当我们通过 FormData
上传文件的时候,浏览器会把请求 headers
中的 Content-Type
设置为 multipart/form-data
。
我们先添加一个判断 FormData
的方法。
helpers/util.ts
:
export function isFormData(val: any): boolean {
return typeof val !== 'undefined' && val instanceof FormData
}
然后再添加相关逻辑。
core/xhr.ts
:
if (isFormData(data)) {
delete headers['Content-Type']
}
xhr
函数内部随着需求越来越多,代码也越来越臃肿,我们可以把逻辑梳理一下,把内部代码做一层封装优化。
我们把整个流程分为 7 步:
request
实例。request.open
方法初始化。configureRequest
配置 request
对象。addEvents
给 request
添加事件处理函数。processHeaders
处理请求 headers
。processCancel
处理请求取消逻辑。request.send
方法发送请求。这样拆分后整个流程就会显得非常清晰,未来我们再去新增需求的时候代码也不会显得越来越臃肿
HTTP 协议中的 Authorization 请求 header 会包含服务器用于验证用户代理身份的凭证,通常会在服务器返回 401 Unauthorized 状态码以及 WWW-Authenticate 消息头之后在后续请求中发送此消息头。
axios 库也允许在请求配置中配置 auth
属性,auth
是一个对象结构,包含 username
和 password
2 个属性。一旦用户在请求的时候配置这俩属性,我们就会自动往 HTTP 的 请求 header 中添加 Authorization
属性,它的值为 Basic 加密串
。 这里的加密串是 username:password
base64 加密后的结果。
首先修改一下类型定义。
types/index.ts
:
export interface AxiosRequestConfig {
// ...
auth?: AxiosBasicCredentials
}
export interface AxiosBasicCredentials {
username: string
password: string
}
接着修改合并规则,因为 auth 也是一个对象格式,所以它的合并规则是 deepMergeStrat
。
core/mergeConfig.ts
:
const stratKeysDeepMerge = ['headers', 'auth']
然后修改发送请求前的逻辑。
core/xhr.ts
:
const {
/*...*/
auth
} = config
if (auth) {
headers['Authorization'] = 'Basic ' + btoa(auth.username + ':' + auth.password)
}
之前 ts-axios
在处理响应结果的时候,认为 HTTP status在 200 和 300 之间是一个合法值,在这个区间之外则创建一个错误。有些时候我们想自定义这个规则,比如认为 304 也是一个合法的状态码,所以我们希望 ts-axios
能提供一个配置,允许我们自定义合法状态码规则。
首先修改一下类型定义。
types/index.ts
:
export interface AxiosRequestConfig {
// ...
validateStatus?: (status: number) => boolean
}
然后我们来修改默认配置规则。
defaults.ts
:
validateStatus(status: number): boolean {
return status >= 200 && status < 300
}
添加默认合法状态码的校验规则。然后再请求后对响应数据的处理逻辑。
core/xhr.ts
:
const {
/*...*/
validateStatus
} = config
function handleResponse(response: AxiosResponse): void {
if (!validateStatus || validateStatus(response.status)) {
resolve(response)
} else {
reject(
createError(
`Request failed with status code ${response.status}`,
config,
null,
request,
response
)
)
}
}
如果没有配置 validateStatus
以及 validateStatus
函数返回的值为 true 的时候,都认为是合法的,正常 resolve(response)
,否则都创建一个错误
在之前我们对请求的 url 参数做了处理,我们会解析传入的 params 对象,根据一定的规则把它解析成字符串,然后添加在 url 后面。在解析的过程中,我们会对字符串 encode,但是对于一些特殊字符比如 @
、+
等却不转义,这是 axios 库的默认解析规则。
当然,我们也希望自己定义解析规则,于是我们希望 ts-axios
能在请求配置中允许我们配置一个 paramsSerializer
函数来自定义参数的解析规则,该函数接受 params
参数,返回值作为解析后的结果。
首先修改一下类型定义。
types/index.ts
:
export interface AxiosRequestConfig {
// ...
paramsSerializer?: (params: any) => string
}
然后修改 buildURL
函数的实现。
helpers/url.ts
:
export function buildURL(
url: string,
params?: any,
paramsSerializer?: (params: any) => string
): string {
if (!params) {
return url
}
let serializedParams
if (paramsSerializer) {
serializedParams = paramsSerializer(params)
} else if (isURLSearchParams(params)) {
serializedParams = params.toString()
} else {
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)}`)
})
})
serializedParams = parts.join('&')
}
if (serializedParams) {
const markIndex = url.indexOf('#')
if (markIndex !== -1) {
url = url.slice(0, markIndex)
}
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
}
return url
}
这里我们给 buildURL
函数新增了 paramsSerializer
可选参数,另外我们还新增了对 params
类型判断,如果它是一个 URLSearchParams
对象实例的话,我们直接返回它 toString
后的结果。
helpers/util.ts
:
export function isURLSearchParams(val: any): val is URLSearchParams {
return typeof val !== 'undefined' && val instanceof URLSearchParams
}
最后我们要修改 buildURL
调用的逻辑。
core/dispatchRequest.ts
:
function transformURL(config: AxiosRequestConfig): string {
const { url, params, paramsSerializer } = config
return buildURL(url!, params, paramsSerializer)
}
有些时候,我们会请求某个域名下的多个接口,我们不希望每次发送请求都填写完整的 url,希望可以配置一个 baseURL
,之后都可以传相对路径。
我们一旦配置了 baseURL
,之后请求传入的 url
都会和我们的 baseURL
拼接成完整的绝对地址,除非请求传入的 url
已经是绝对地址。
首先修改一下类型定义。
types/index.ts
:
export interface AxiosRequestConfig {
// ...
baseURL?: string
}
接下来实现 2 个辅助函数。
helpers/url.ts
:
export function isAbsoluteURL(url: string): boolean {
return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url)
}
export function combineURL(baseURL: string, relativeURL?: string): string {
return relativeURL ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '') : baseURL
}
最后我们来调用这俩个辅助函数。
core/dispatchRequest.ts
:
function transformURL(config: AxiosRequestConfig): string {
let { url, params, paramsSerializer, baseURL } = config
if (baseURL && !isAbsoluteURL(url!)) {
url = combineURL(baseURL, url)
}
return buildURL(url!, params, paramsSerializer)
}
官方 axios 库实现了 axios.all
、axios.spread
等方法。
实际上,axios.all
就是 Promise.all
的封装,它返回的是一个 Promise
数组,then
函数的参数本应是一个参数为 Promise resolves
(数组)的函数,在这里使用了 axios.spread
方法。所以 axios.spread
方法是接收一个函数,返回一个新的函数,新函数的结构满足 then
函数的参数结构。
为了保持与官网 axios API 一致,也在 ts-axios
库中实现这俩方法。
官方 axios 库也通过 axios.Axios
对外暴露了 Axios
类 (感觉也没有啥使用场景)。
另外对于 axios 实例,官网还提供了 getUri
方法在不发送请求的前提下根据传入的配置返回一个 url
首先修改类型定义。
types/index.ts
:
export interface AxiosClassStatic {
new (config: AxiosRequestConfig): Axios;
}
export interface AxiosStatic extends AxiosInstance {
// ...
all<T>(promises: Array<T | Promise<T>>): Promise<T[]>;
spread<T, R>(callback: (...args: T[]) => R): (arr: T[]) => R;
Axios: AxiosClassStatic;
}
export interface Axios {
// ...
getUri(config?: AxiosRequestConfig): string;
}
然后我们去实现这几个静态方法。
axios.ts
:
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = function spread(callback) {
return function wrap(arr) {
return callback.apply(null, arr);
};
};
axios.Axios = Axios;
最后我们去给 Axios 添加实例方法 getUri
。
core/Axios.ts
:
getUri(config?: AxiosRequestConfig): string {
config = mergeConfig(this.defaults, config)
return transformURL(config)
}
先和默认配置合并,然后再通过 dispatchRequest
中实现的 transformURL
返回一个新的 url
至此,ts-axios
就实现了官网 axios 库在浏览器端的所有需求。
单元测试是前端一个很重要的方向,鉴别一个开源库是否靠谱的一个标准是它的单元测试是否完善。
有了完整的单元测试,未来去重构现有代码或者是增加新的需求都会有十足的把握不出现 regression bug。
在前面已经编写完成 ts-axios 库的代码,并通过 demo 的形式简单地对一些功能做了验证,但是 demo 可以走到的代码分支,覆盖的场景都是极其有限的。
为了用更科学的手段保证我们代码的可靠性,我们去编写单元测试,并尽可能达到 99% 以上的测试覆盖率。
我们使用开源测试框架 Jest,它是 Facebook 出品的一个测试框架,相对其他测试框架,它的一大特点就是内置了常用的测试工具,比如自带断言、测试覆盖率工具,实现了开箱即用。
单元测试部分不是本文重点,在此不做介绍,本项目最后测试覆盖率达到了100%。