当前位置 : 首页 > 维修数据

# 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

栏目列表