完善axios:参数处理与响应封装
<|begin▁of▁sentence|># 1. 前言
在上一篇文章中,我们实现了`axios`的基础功能,但到目前为止,我们都只是实现了一个很简单的`axios`,不管是传入的参数还是返回的响应数据都还很简单,距离实现一个真正的`axios`还差的很远。那么从这篇文章开始,我们就来一步步完善这个`axios`。
首先,我们先来把`axios`的请求参数和响应数据完善一下,目前我们请求参数只支持传入`url`,而响应数据只能返回`responseText`,这显然是不够的,我们期望请求的时候还能传入配置对象`config`,并且该配置对象里面可以配置很多参数,如`method`、`data`、`headers`等等;而响应数据我们希望返回的是一个封装好的响应对象,该对象里面包含如`data`、`status`、`statusText`、`headers`、`config`等等。
那么,接下来,我们就先来把请求参数和响应数据这两块来完善一下。
# 2. 修改参数类型
在原来的代码中,我们`axios`函数只接收一个参数,即请求的`url`地址,而现在我们想让它同时支持接收两个参数:`url`和配置对象`config`,如下:
```typescript
axios({
url: "/api/addParameters",
method: "post",
});
```
或者
```typescript
axios("/api/addParameters", {
method: "post",
});
```
并且,配置对象`config`里面可以配置很多参数,我们暂时先考虑以下几个常用的配置属性:
- `url`:请求的`url`地址;
- `method`:请求方法;
- `data`:请求体数据;
- `params`:`URL`参数;
- `headers`:请求头;
那么,接下来,我们就先来定义请求参数的类型。
## 2.1 定义请求参数类型
我们在`src`目录下新建`types`目录,用来存放项目中用到的类型定义。然后在`types`目录下创建`index.ts`文件,用来统一导出类型定义。
### 2.1.1 创建 types/index.ts
```typescript
export * from "./axios";
```
### 2.1.2 创建 types/axios.ts
```typescript
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;
}
```
我们为请求参数定义了`AxiosRequestConfig`接口,其中`url`为必选参数,其余属性为可选参数。并且`method`属性的类型是上面定义的`Method`类型。
## 2.2 修改 axios 函数参数类型
类型定义好之后,我们就来用它们。首先,我们修改`src/index.ts`文件中`axios`函数的参数类型。
```typescript
import { AxiosRequestConfig } from "./types";
function axios(config: AxiosRequestConfig) {
processConfig(config);
xhr(config);
}
```
此时,`axios`函数只接收一个参数`config`,该参数类型为`AxiosRequestConfig`。但是,我们还想让它支持接收两个参数,即:
```typescript
axios(url, config);
```
所以,我们需要对参数进行重载。
### 2.2.1 函数重载
利用`TypeScript`的函数重载,我们来定义`axios`函数多种参数形式。
```typescript
import { AxiosRequestConfig } from "./types";
function axios(config: AxiosRequestConfig): void;
function axios(url: string, config: AxiosRequestConfig): void;
function axios(url: any, config?: any) {
if (typeof url === "string") {
if (!config) {
config = {};
}
config.url = url;
} else {
config = url;
}
processConfig(config);
xhr(config);
}
```
我们定义了`axios`函数的两种参数形式,当第一个参数`url`为字符串类型,并且第二个参数`config`是可选的时候,说明用户使用的是第二种参数形式,那么我们就判断如果用户没有传`config`,就给他一个空对象,然后把`url`赋值给`config.url`;如果第一个参数不是字符串类型,那么说明用户使用的就是第一种参数形式,并且第一个参数就是`config`,我们把它赋值给我们内部的`config`变量。
OK,参数类型修改完了,接下来我们就要处理传入的`config`了。
# 3. 处理 config 参数
从上面代码中可以看到,在处理`config`参数的时候我们调用了`processConfig`函数,那么接下来我们就来实现这个函数。
我们在`src`目录下创建`helpers`目录,用来放置一些辅助工具函数,然后在该目录下创建`url.ts`文件,用来处理与`url`相关的辅助函数,接着再创建`data.ts`文件,用来处理与`data`相关的辅助函数,最后创建`headers.ts`文件,用来处理与`headers`相关的辅助函数。
## 3.1 处理 URL
由于我们传人的`config`里面可能带有`params`参数,所以我们需要把这些参数拼接到`url`后面,组成一个完整的`URL`再发送请求。这里,我们需要编写一个工具函数,把`params`对象转换成`URL`参数字符串。
我们在`helpers/url.ts`文件中编写该函数:
```typescript
import { isDate, isPlainObject } 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 buildURL(url: string, params?: any): string {
if (!params) {
return url;
}
const parts: string[] = [];
Object.keys(params).forEach(key => {
let val = params[key];
if (val === null || typeof val === "undefined") {
return;
}
let values: string[];
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;
}
```
代码说明:
- 首先,判断`params`是否存在,如果不存在,则直接返回`url`;
- 如果存在,则对`params`进行遍历,`params`可能是一个数组,也可能是一个普通对象,还可能是一个`Date`对象,所以我们需要对不同类型进行不同处理;
- 如果`params`是一个数组,则我们需要给该参数名后面加上`[]`,如:`params: { foo: ["bar", "baz"] }`会被转换成`foo[]=bar&foo[]=baz`;
- 如果`params`是一个普通对象,则我们需要把它序列化成`JSON`字符串;
- 如果`params`是一个`Date`对象,则我们需要调用它的`toISOString`方法将其转换成`ISO`字符串;
- 然后,对转换后的参数名和参数值进行`encode`编码,并且把编码后的键值对拼接到`parts`数组中;
- 接着,把`parts`数组用`&`符号拼接成一个字符串;
- 最后,判断原`url`中是否已有参数,如果有,则用`&`拼接,否则用`?`拼接;
另外,我们还用到了两个工具函数`isDate`和`isPlainObject`,我们在`helpers/util.ts`中定义这两个函数:
```typescript
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]";
}
```
## 3.2 处理请求 body 数据
我们通过`XMLHttpRequest`发送请求的时候,如果是对象,我们需要把它转换成`JSON`字符串,并且同时需要给请求头`headers`里面设置一个`Content-Type: application/json;charset=utf-8`,所以我们需要一个工具函数来对请求`body`数据进行处理。
我们在`helpers/data.ts`文件中编写该函数:
```typescript
import { isPlainObject } from "./util";
export function transformRequest(data: any): any {
if (isPlainObject(data)) {
return JSON.stringify(data);
}
return data;
}
```
## 3.3 处理请求 headers
我们还需要一个工具函数来对请求头`headers`做处理。在发送请求之前,我们需要检查一下传入的`headers`,如果用户传入的`headers`里面没有`Content-Type`属性,并且请求`body`数据是一个普通对象的话,那么我们需要自动给`headers`设置`Content-Type`为`application/json;charset=utf-8`。
我们在`helpers/headers.ts`文件中编写该函数:
```typescript
import { isPlainObject } from "./util";
export function processHeaders(headers: any, data: any): any {
if (isPlainObject(data)) {
if (headers && !headers["Content-Type"]) {
headers["Content-Type"] = "application/json;charset=utf-8";
}
}
return headers;
}
```
## 3.4 实现 processConfig 函数
工具函数都写好了,接下来我们就来实现`processConfig`函数,该函数用来处理传入的`config`参数。
我们在`src/xhr.ts`文件中实现该函数:
```typescript
import { transformRequest } from "./helpers/data";
import { processHeaders } from "./helpers/headers";
import { buildURL } from "./helpers/url";
import { AxiosRequestConfig } from "./types";
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): any {
const { headers = {}, data } = config;
return processHeaders(headers, data);
}
```
代码说明:
- 在`processConfig`函数内部,我们分别调用了`transformURL`、`transformHeaders`、`transformRequestData`这三个函数来分别处理`config`中的`url`、`headers`和`data`;
- `transformURL`函数:内部调用`buildURL`函数,把`config.url`和`config.params`传入,返回拼接好的`URL`;
- `transformRequestData`函数:内部调用`transformRequest`函数,把`config.data`传入,返回转换后的数据;
- `transformHeaders`函数:内部调用`processHeaders`函数,把`config.headers`和`config.data`传入,返回处理后的`headers`;
OK,`config`参数处理完了,接下来我们就要发送请求了,在发送请求的时候,我们需要把处理后的`config`传入。
# 4. 获取响应数据
在原来的代码中,我们发送请求后只获取了`xhr.responseText`,这显然是不够的,我们期望返回的响应数据应该是一个响应对象,该对象里面应该包含:`data`、`status`、`statusText`、`headers`、`config`等等。
那么,接下来,我们就来定义响应对象的类型,并且在发送请求后获取到这些数据,然后封装成响应对象返回。
## 4.1 定义响应对象类型
我们在`types/axios.ts`中定义响应对象类型:
```typescript
export interface AxiosResponse {
data: any;
status: number;
statusText: string;
headers: any;
config: AxiosRequestConfig;
request: any;
}
```
另外,我们期望`axios`函数能够返回一个`Promise`对象,这样就可以使用`then`方法了,所以`axios`函数的返回类型应该是`Promise`。
```typescript
export interface AxiosPromise extends Promise {}
```
## 4.2 修改 xhr 函数
接下来,我们来修改`xhr`函数,让其返回一个`Promise`对象,并且在`onreadystatechange`事件中,当请求成功时,`resolve`响应对象,当请求失败时,`reject`错误信息。
```typescript
import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from "./types";
export default function xhr(config: AxiosRequestConfig): AxiosPromise {
return new Promise((resolve, reject) => {
const { url, method = "get", data = null, headers } = config;
const request = new XMLHttpRequest();
request.open(method.toUpperCase(), url, true);
request.onreadystatechange = function handleLoad() {
if (request.readyState !== 4) {
return;
}
const responseHeaders = request.getAllResponseHeaders();
const responseData =
request.responseType === "text"
? request.responseText
: request.response;
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);
});
}
```
代码说明:
- 首先,我们让`xhr`函数返回一个`Promise`对象,并且在`Promise`内部创建`xhr`对象,发送请求;
- 接着,我们在`onreadystatechange`事件中,当`readyState`为 4 的时候,说明请求已经完成,我们就获取响应头`responseHeaders`、响应数据`responseData`,然后拼装成响应对象`response`,最后调用`resolve(response)`把该响应对象返回;
- 另外,我们还需要处理请求头`headers`,在设置请求头之前,我们需要检查一下如果`data`为空的话,需要删除`content-type`,因为如果`data`为空,我们是没有必要设置`content-type`的;
## 4.3 修改 axios 函数
由于`xhr`函数返回的是一个`Promise`对象,所以我们在`axios`函数中也要返回该`Promise`对象。
```typescript
import { AxiosRequestConfig, AxiosPromise } from "./types";
import xhr from "./xhr";
function axios(config: AxiosRequestConfig): AxiosPromise;
function axios(url: string, config?: AxiosRequestConfig): AxiosPromise;
function axios(url: any, config?: any): AxiosPromise {
if (typeof url === "string") {
if (!config) {
config = {};
}
config.url = url;
} else {
config = url;
}
processConfig(config);
return xhr(config);
}
```
OK,这样我们就实现了`axios`函数返回`Promise`,并且响应数据是一个封装好的响应对象。
# 5. 编写 demo
现在,我们来编写`demo`来测试下我们新写的代码。
在`examples`目录下创建`addParameters`目录,并在该目录下面创建`index.`:
```
< lang="en">
addParameters demo
>
```
接着再创建`app.ts`:
```typescript
import axios from "../../src/index";
axios({
method: "post",
url: "/api/addParameters",
data: {
a: 1,
b: 2,
},
});
axios("/api/addParameters", {
method: "post",
data: {
a: 1,
b: 2,
},
});
```
接着在`server.js`中添加路由:
```javascript
router.post("/api/addParameters", function(req, res) {
res.json(req.body);
});
```
最后在根目录下的`index.`中加上启动该`demo`的入口:
```
addParameters
```
OK, 我们在命令行中执行:
```bash
# 同时开启客户端和服务端
npm run server | npm start
```
接着浏览器打开`http://localhost:8000/`,接着点击`addParameters`,通过`F12`的网络面板我们可以看到发送了两个请求,并且请求的`URL`是正确的,并且请求头`Content-Type`也自动帮我们设置好了,并且请求体`body`数据也是`JSON`字符串,并且响应数据也是我们封装好的响应对象。




OK,这样我们就实现了对请求参数和响应数据的处理。
# 6. 遗留问题
虽然我们已经实现了对请求参数和响应数据的处理,但是还是有一些细节问题我们没有处理,比如:
- 我们没有处理请求超时;
- 我们没有处理网络异常;
- 我们没有处理非 200 状态码;
这些我们都会在后续的文章中一一实现。
# 7. 总结
本篇文章中,我们做了以下工作:
- 修改了`axios`函数的参数,让其支持接收两个参数:`url`和`config`;
- 定义了请求参数类型`AxiosRequestConfig`;
- 编写了处理`config`参数的工具函数:
- 处理`url`:把`params`对象转换成`URL`参数字符串;
最新文章
- 汽车刹车液保养指南
- 电动化与智能化并行:固态电池、激光雷达、AR-HUD引领汽车技术革命
- 水泵驱动汽车冷却系统高效运转
- 智能温控系统提升汽车驾驶舒适度
- 毫米波雷达:智能汽车的“透视眼”与安全守护神
- 智能网联与自动驾驶:电动化时代汽车技术新趋势
- 冬季汽车防冻液更换保养指南
- 特斯拉Model 3自动驾驶功能引领未来出行潮流
- 碳纤维轻量化汽车车身设计
- 固态电池革命:能量密度突破与快充技术重塑汽车未来
- 驾驶员疲劳检测系统实时监控行车安全
- 固态电池突破:能量密度提升50%,充电速度翻3倍
- 固态电池革命:能量密度提升50%,快充技术突破充电瓶颈
- 掌握安全跟车距离:车速、3秒法则与刹车性能全解析
- 点云地图:自动驾驶汽车的“数字眼睛”与高精基石
- 车道级定位:从精准导航到驾驶安全的革命性守护
- 轮胎磨损影响汽车行驶安全需定期检查
- 汽车碰撞保险理赔流程详解
- 汽车湿度传感器故障诊断与维修保养全指南
- 轻量化设计提升汽车燃油效率与性能
