当前位置 : 首页 > 保养数据

完善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`字符串,并且响应数据也是我们封装好的响应对象。 ![](~@/axios/04/01.png) ![](~@/axios/04/02.png) ![](~@/axios/04/03.png) ![](~@/axios/04/04.png) OK,这样我们就实现了对请求参数和响应数据的处理。 # 6. 遗留问题 虽然我们已经实现了对请求参数和响应数据的处理,但是还是有一些细节问题我们没有处理,比如: - 我们没有处理请求超时; - 我们没有处理网络异常; - 我们没有处理非 200 状态码; 这些我们都会在后续的文章中一一实现。 # 7. 总结 本篇文章中,我们做了以下工作: - 修改了`axios`函数的参数,让其支持接收两个参数:`url`和`config`; - 定义了请求参数类型`AxiosRequestConfig`; - 编写了处理`config`参数的工具函数: - 处理`url`:把`params`对象转换成`URL`参数字符串;

    栏目列表