# Axios 配置项实现与合并策略解析
<|begin▁of▁sentence|># 1. 前言
在上一篇文章中,我们实现了`axios`的基础功能,但到目前为止,我们都只是实现了一个很简单的`axios`,一个最最基础的`axios`,它只能发送请求,并且收到响应后将响应数据返回,除此之外,它没有任何其它功能,例如:我们想为请求添加配置项、为响应添加配置项、拦截请求和响应、转换请求数据和响应数据、取消请求等功能,这些我们统统都还没实现。那么从本篇文章开始,我们就来为我们的`axios`添加这些功能。
我们先从添加配置项开始,因为后续的很多功能都会依赖配置项,所以我们就先来实现配置项功能。
# 2. 配置项都有哪些?
我们先来看一下官方`axios`都有哪些配置项,我们可以在[axios](https://axios-http.com/zh/docs/req_config)官网看到,它有以下配置项:
```javascript
{
// `url` 是用于请求的服务器 URL
url: '/user',
// `method` 是创建请求时使用的方法
method: 'get', // 默认值
// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
baseURL: 'https://some-domain.com/api/',
// `transformRequest` 允许在向服务器发送前,修改请求数据
// 它只能用于 'PUT', 'POST' 和 'PATCH' 这几个请求方法
// 数组中最后一个函数必须返回一个字符串, 一个Buffer实例,ArrayBuffer,FormData,或 Stream
// 你可以修改请求头。
transformRequest: [function (data, headers) {
// 对发送的 data 进行任意转换处理
return data;
}],
// `transformResponse` 在传递给 then/catch 前,允许修改响应数据
transformResponse: [function (data) {
// 对接收的 data 进行任意转换处理
return data;
}],
// 自定义请求头
headers: {'X-Requested-With': 'XMLHttpRequest'},
// `params` 是与请求一起发送的 URL 参数
// 必须是一个简单对象或 URLSearchParams 对象
params: {
ID: 12345
},
// `paramsSerializer`是可选方法,主要用于序列化`params`
// (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
paramsSerializer: function (params) {
return Qs.stringify(params, {arrayFormat: 'brackets'})
},
// `data` 是作为请求体被发送的数据
// 仅适用 'PUT', 'POST', 'DELETE 和 'PATCH' 请求方法
// 在没有设置 `transformRequest` 时,则必须是以下类型之一:
// - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
// - 浏览器专属: FormData, File, Blob
// - Node 专属: Stream, Buffer
data: {
firstName: 'Fred'
},
// 发送请求体数据的可选语法
// 请求方式 post
// 只有 value 会被发送,key 则不会
data: 'Country=Brasil&City=Belo Horizonte',
// `timeout` 指定请求超时的毫秒数。
// 如果请求时间超过 `timeout` 的值,则请求会被中断
timeout: 1000, // 默认值是 `0` (永不超时)
// `withCredentials` 表示跨域请求时是否需要使用凭证
withCredentials: false, // default
// `adapter` 允许自定义处理请求,这使测试更加容易。
// 返回一个 promise 并应用一个有效的响应 (查阅 response 配置).
adapter: function (config) {
/* ... */
},
// `auth` HTTP Basic Auth
auth: {
username: 'janedoe',
password: 's00pers3cret'
},
// `responseType` 表示浏览器将要响应的数据类型
// 选项包括: 'arraybuffer', 'document', 'json', 'text', 'stream'
// 浏览器专属:'blob'
responseType: 'json', // 默认值
// `responseEncoding` 表示用于解码响应的编码 (Node.js 专属)
// 注意:忽略 `responseType` 的值为 'stream',或者是客户端请求
// Note: Ignored for `responseType` of 'stream' or client-side requests
responseEncoding: 'utf8', // 默认值
// `xsrfCookieName` 是 xsrf token 的值,被用作 cookie 的名称
xsrfCookieName: 'XSRF-TOKEN', // 默认值
// `xsrfHeaderName` 是带有 xsrf token 值的 http 请求头名称
xsrfHeaderName: 'X-XSRF-TOKEN', // 默认值
// `onUploadProgress` 允许为上传处理进度事件
// 浏览器专属
onUploadProgress: function (progressEvent) {
// 处理原生进度事件
},
// `onDownloadProgress` 允许为下载处理进度事件
onDownloadProgress: function (progressEvent) {
// 处理原生进度事件
},
// `maxContentLength` 定义了 node.js 中允许的 HTTP 响应内容的最大字节数
maxContentLength: 2000,
// `maxBodyLength`(仅Node)定义允许的 http 请求内容的最大字节数
maxBodyLength: 2000,
// `validateStatus` 定义了对于给定的 HTTP 状态码是 resolve 还是 reject promise。
// 如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),
// 则 promise 将会被 resolve,否则是 reject。
validateStatus: function (status) {
return status >= 200 && status < 300; // 默认值
},
// `maxRedirects` 定义了在 node.js 中要遵循的最大重定向数。
// 如果设置为 0,则不会进行重定向
maxRedirects: 5, // 默认值
// `socketPath` 定义了在 node.js 中使用的 UNIX Socket。
// e.g. '/var/run/docker.sock' 发送请求到 docker 守护进程。
// 只能指定 `socketPath` 或 `proxy`。
// 若都指定,这使用 `socketPath`。
socketPath: null, // default
// `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理。
// 允许配置类似 `keepAlive` 的选项,
// 默认不启用。
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),
// `proxy` 定义了代理服务器的主机名,端口和协议。
// 您可以使用常规的 `http_proxy` 和 `https_proxy` 环境变量。
// 使用 `false` 可以禁用代理功能,同时环境变量也会被忽略。
// `auth`表示应使用 HTTP Basic auth 连接到代理,并且提供凭据。
// 这将设置一个 `Proxy-Authorization` 请求头,它会覆盖 `proxy` 中已有的 `Proxy-Authorization` 请求头。
// 如果代理服务器使用 HTTPS,则必须设置 protocol 为`https`
proxy: {
protocol: 'https',
host: '127.0.0.1',
port: 9000,
auth: {
username: 'mikeymike',
password: 'rapunz3l'
}
},
// `cancelToken` 指定用于取消请求的 cancel token
// (查看后面的 Cancellation 这节了解更多)
cancelToken: new CancelToken(function (cancel) {
}),
// `decompress` 指示响应体是否需要被解压缩
// 如果为 true,则所有解压缩的响应体的响应头中都会移除 'content-encoding' 头
// 仅适用于 Node.js (XHR 无法关闭解压缩)
decompress: true // 默认值
}
```
可以看到,配置项还是蛮多的,我们不可能一次性全部实现,所以我们就先实现一些常用的配置项,后续再慢慢添加。
# 3. 实现思路
我们通过观察官方`axios`的使用方式,发现它有两种使用方式:
- 第一种:`axios(config)`
- 第二种:`axios(url[, config])`
并且,它还可以为`axios`添加默认配置,例如:
```javascript
axios.defaults.baseURL = "https://api.example.com";
axios.defaults.headers.common["Authorization"] = AUTH_TOKEN;
axios.defaults.headers.post["Content-Type"] =
"application/x-www-form-urlencoded";
```
所以,我们也要实现这两种使用方式和默认配置。
那么,我们该如何实现呢?
首先,我们要为`axios`函数添加一个`defaults`属性,该属性是一个对象,用来存储默认配置。
然后,在调用`axios`函数时,我们要把传入的配置和默认配置进行合并,合并后的配置才是最终的配置。
最后,我们还要支持第二种使用方式,即`axios(url[, config])`,这种方式其实等价于`axios({url: url, ...config})`,所以我们只需要在函数内部判断第一个参数是否是字符串,如果是字符串,那么就把它作为`url`,然后把第二个参数作为配置,然后合并到默认配置中即可。
OK,思路就是这些,接下来我们就开始实现。
# 4. 代码实现
## 4.1 创建默认配置对象
首先,我们在`src`目录下创建`defaults.ts`文件,该文件用来存储默认配置。
```typescript
// src/defaults.ts
import { AxiosRequestConfig } from "./types";
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;
```
在上面的代码中,我们创建了一个默认配置对象`defaults`,它包含了一些默认配置,例如`method`默认为`get`,`timeout`默认为`0`,`headers`中`common`属性表示所有请求都有的请求头,`methodsNoData`中的方法没有请求体,所以它们的请求头为空对象,`methodsWithData`中的方法有请求体,所以它们的请求头中包含了`Content-Type`属性。
## 4.2 修改 AxiosRequestConfig 类型定义
由于我们为`headers`添加了`common`、`get`、`post`等属性,所以我们需要修改`AxiosRequestConfig`类型定义。
```typescript
// src/types/index.ts
export interface AxiosRequestConfig {
url?: string;
method?: Method;
data?: any;
params?: any;
headers?: any;
responseType?: XMLHttpRequestResponseType;
timeout?: number;
}
// 改为
export interface AxiosRequestConfig {
url?: string;
method?: Method;
data?: any;
params?: any;
headers?: any;
responseType?: XMLHttpRequestResponseType;
timeout?: number;
[propName: string]: any;
}
export interface AxiosResponse {
data: any;
status: number;
statusText: string;
headers: any;
config: AxiosRequestConfig;
request: any;
}
```
我们为`AxiosRequestConfig`接口添加了索引签名,这样我们就可以在配置对象中添加任意属性了。
## 4.3 修改 axios 函数
然后,我们修改`src/axios.ts`文件,为`axios`函数添加`defaults`属性,并且在函数内部处理配置合并。
```typescript
// src/axios.ts
import { AxiosRequestConfig } from "./types";
import xhr from "./xhr";
import { buildURL } from "./helpers/url";
import { transformRequest } from "./helpers/data";
import { processHeaders } from "./helpers/headers";
import defaults from "./defaults";
function axios(config: AxiosRequestConfig): void {
processConfig(config);
xhr(config);
}
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);
}
// 为axios函数添加defaults属性
axios.defaults = defaults;
export default axios;
```
此时,`axios`函数已经有了`defaults`属性,并且我们在函数内部处理了配置合并,但是目前我们还没有实现配置合并的逻辑,我们只是简单地把传入的配置和默认配置合并,但是合并的逻辑我们还没有实现。
## 4.4 实现配置合并
接下来,我们来实现配置合并的逻辑。我们在`src`目录下创建`core`目录,然后在`core`目录下创建`mergeConfig.ts`文件,该文件用来实现配置合并的逻辑。
```typescript
// src/core/mergeConfig.ts
import { AxiosRequestConfig } from "../types";
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]) {
mergeField(key);
}
}
function mergeField(key: string): void {
const strat = strats[key] || defaultStrat;
config[key] = strat(config1[key], config2![key]);
}
return config;
}
```
在上面的代码中,我们创建了一个`mergeConfig`函数,该函数接收两个配置对象,返回合并后的配置对象。
合并的逻辑是:遍历`config2`的所有属性,然后调用`mergeField`函数合并属性;然后遍历`config1`的所有属性,如果`config2`中没有该属性,那么也调用`mergeField`函数合并属性。
`mergeField`函数会根据属性名从`strats`对象中获取合并策略函数,如果没有找到,则使用默认的合并策略函数`defaultStrat`。
接下来,我们来实现`strats`对象和`defaultStrat`函数。
```typescript
// src/core/mergeConfig.ts
import { AxiosRequestConfig } from "../types";
const strats = Object.create(null);
function defaultStrat(val1: any, val2: any): any {
return typeof val2 !== "undefined" ? val2 : val1;
}
function fromVal2Strat(val1: any, val2: any): any {
if (typeof val2 !== "undefined") {
return val2;
}
}
const stratKeysFromVal2 = ["url", "params", "data"];
stratKeysFromVal2.forEach((key) => {
strats[key] = fromVal2Strat;
});
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]) {
mergeField(key);
}
}
function mergeField(key: string): void {
const strat = strats[key] || defaultStrat;
config[key] = strat(config1[key], config2![key]);
}
return config;
}
```
在上面的代码中,我们实现了`defaultStrat`函数和`fromVal2Strat`函数。
`defaultStrat`函数的逻辑是:如果`val2`不是`undefined`,则返回`val2`,否则返回`val1`。
`fromVal2Strat`函数的逻辑是:如果`val2`不是`undefined`,则返回`val2`。
然后,我们为`strats`对象添加了`url`、`params`、`data`属性的合并策略函数,这些属性都使用`fromVal2Strat`函数,即只要`val2`不是`undefined`,就返回`val2`。
接下来,我们还要为`headers`属性添加合并策略函数,因为`headers`属性的合并逻辑比较复杂,它需要合并`common`、`get`、`post`等属性。
```typescript
// src/core/mergeConfig.ts
import { AxiosRequestConfig } from "../types";
import { deepMerge } from "../helpers/util";
const strats = Object.create(null);
function defaultStrat(val1: any, val2: any): any {
return typeof val2 !== "undefined" ? val2 : val1;
}
function fromVal2Strat(val1: any, val2: any): any {
if (typeof val2 !== "undefined") {
return val2;
}
}
function deepMergeStrat(val1: any, val2: any): any {
if (typeof val2 === "object") {
return deepMerge(val1, val2);
} else if (typeof val2 !== "undefined") {
return val2;
} else if (typeof val1 === "object") {
return deepMerge(val1);
} else {
return val1;
}
}
const stratKeysFromVal2 = ["url", "params", "data"];
stratKeysFromVal2.forEach((key) => {
strats[key] = fromVal2Strat;
});
const stratKeysDeepMerge = ["headers"];
stratKeysDeepMerge.forEach((key) => {
strats[key] = deep
最新文章
- 转向灯使用指南:安全驾驶与故障处理全解析
- 强劲动力与卓越操控完美结合
- 智能仪表盘革命:从机械指针到全息投影的驾驶体验升级
- 800V高压平台引领电动汽车技术革命:电池与电驱系统全面升级
- 智能驾驶与新能源技术重塑未来出行新生态
- 高压平台与自动驾驶:技术解析与安全冗余设计
- 固态电池与无钴正极技术引领电动车动力电池革命
- 保持安全车距确保行车安全
- 暴雨台风地震行车指南:发动机、悬挂与天窗防护要点
- 汽车制动系统故障灯亮起
- 电动汽车技术革新:固态电池与车联网引领未来出行
- 固态电池与毫米波雷达领跑汽车电动化与智能化革命
- 动力电池技术革新:从三元锂到固态电池的产业升级
- 奔驰驾驶体验舒适优雅
- 电动车与燃油车保险差异:电池组、电机及电控系统保障解析
- 宝马X5豪华SUV驾驶体验卓越
- 三元锂电池突破300Wh/kg能量密度,L3自动驾驶引领电动化浪潮
- 三大前沿技术重塑未来汽车:智能座舱、800V快充与线控转向
- 2023年智能驾驶、固态电池与车联网三大技术趋势解析
- 电动化浪潮下:电池技术突破与智能驾驶的未来发展
