1.使用Typescript重构axios(一)——写在最前面
2.使用Typescript重构axios(二)——项目起手,跑通流程
3.使用Typescript重构axios(三)——实现基础功能:处理get请求url参数
4.使用Typescript重构axios(四)——实现基础功能:处理post请求参数
5.使用Typescript重构axios(五)——实现基础功能:处理请求的header
6.使用Typescript重构axios(六)——实现基础功能:获取响应数据
7.使用Typescript重构axios(七)——实现基础功能:处理响应header
8.使用Typescript重构axios(八)——实现基础功能:处理响应data
9.使用Typescript重构axios(九)——异常处理:基础版
10.使用Typescript重构axios(十)——异常处理:增强版
11.使用Typescript重构axios(十一)——接口扩展
12.使用Typescript重构axios(十二)——增加参数
13.使用Typescript重构axios(十三)——让响应数据支持泛型
14.使用Typescript重构axios(十四)——实现拦截器
15.使用Typescript重构axios(十五)——默认配置
16.使用Typescript重构axios(十六)——请求和响应数据配置化
17.使用Typescript重构axios(十七)——增加axios.create
18.使用Typescript重构axios(十八)——请求取消功能:总体思路
19.使用Typescript重构axios(十九)——请求取消功能:实现第二种使用方式
20.使用Typescript重构axios(二十)——请求取消功能:实现第一种使用方式
21.使用Typescript重构axios(二十一)——请求取消功能:添加axios.isCancel接口
22.使用Typescript重构axios(二十二)——请求取消功能:收尾
23.使用Typescript重构axios(二十三)——添加withCredentials属性
24.使用Typescript重构axios(二十四)——防御XSRF攻击
25.使用Typescript重构axios(二十五)——文件上传下载进度监控
26.使用Typescript重构axios(二十六)——添加HTTP授权auth属性
27.使用Typescript重构axios(二十七)——添加请求状态码合法性校验
28.使用Typescript重构axios(二十八)——自定义序列化请求参数
29.使用Typescript重构axios(二十九)——添加baseURL
30.使用Typescript重构axios(三十)——添加axios.getUri方法
31.使用Typescript重构axios(三十一)——添加axios.all和axios.spread方法
32.使用Typescript重构axios(三十二)——写在最后面(总结)
项目源码请猛戳这里!!!
1. 前言
在实际项目中,所有请求的请求配置对象 config 中有些字段其实都是相同的,例如请求超时事件 timeout ,亦或者说我们需要给所有请求都添加一个相同的字段,例如在进行身份认证的时候我们需要给所有请求都添加 Authorization 。我们现在实现的 axios 所有请求配置都是独立的,也就是说如果你需要给所有请求都加上某个配置字段,那么你需要在配置 axios 的配置对象的时候都加上这一字段,这无疑将会产生许多重复代码。而官方的 axios 为我们提供了默认配置对象 axios.defaults ,我们可以把所有相同的配置字段都写入该默认配置对象,那么这个配置字段将会在所有的请求中都生效。
接下来,我们也要实现这一默认配置功能。其实,这没有多么复杂,我们默认提供一个配置对象,然后只需将用户配置对象与默认配置对象进行合并,然后发出请求即可。
OK,我们接下来就来实现它。
2. 创建默认配置对象defaults
根据官方 axios 文档给出的默认配置示例:
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.headers.common['Authorization'] = AUTH_TOKEN 表示给所有请求的 headers 都添加 Authorization ,并且值为 AUTH_TOKEN ; axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; 表示给所有 POST 请求的 headers 都添加 Content-Type ,并且值为 application/x-www-form-urlencoded ;有了以上概念,我们就可以创建出默认对象 defaultes ,我们在 src 目录下新建 defaultes.ts 文件,在该文件内创建默认对象 defaultes ,如下:
// src/defaultes.ts import { AxiosRequestConfig } from "./types"; const defaults: AxiosRequestConfig = { 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 中只添加了默认请求超时时间 timeout 和请求头 headers ,并且我们在 headers 中设置了 common 属性,用于存放所有请求都需要的请求头字段,另外与 common 同级下还创建了每个请求方式属性,用于存放不同请求所特有的请求头字段。例如像需要数据的请求方式 post 、 put 、 patch 我们为其默认添加了 Content-Type 字段,而不需要数据的请求方式 delete 、 get 、 head 、 options 则为其留空。( 其实默认配置对象里面的内容远不止这些,详细内容可查看这里~ )
OK,默认配置对象 defaults 就已经创建好了。
3. 向Axios类中添加默认配置对象
在官方 axios 中,从 axios 对象上可以点出来 defaults 对象,所以我们还需要将创建好的默认配置对象添加到 Axios 类中,从而可以在实例 axios 对象上点出来 defaults
// src/core/Axios.ts export default class Axios { defaults: AxiosRequestConfig; interceptors: { request: InterceptorManager<AxiosRequestConfig>; response: InterceptorManager<AxiosResponse<any>>; }; constructor() { this.defaults = {}; this.interceptors = { request: new InterceptorManager<AxiosRequestConfig>(), response: new InterceptorManager<AxiosResponse>() }; } }
仅仅是这样还不行,虽然现在 axios 对象可以点出来 defaults ,但是点出来 defaults 却是一个空 {} ,我们还应该把上面创建的默认配置对象传进来,确保 axios 对象点出来的是真正的 defaults 。
我们把上面创建的默认配置对象通过 Axios 类的构造函数传进来,如下:
export default class Axios { defaults: AxiosRequestConfig; interceptors: { request: InterceptorManager<AxiosRequestConfig>; response: InterceptorManager<AxiosResponse<any>>; }; constructor(defaultConfig: AxiosRequestConfig) { this.defaults = defaultConfig; this.interceptors = { request: new InterceptorManager<AxiosRequestConfig>(), response: new InterceptorManager<AxiosResponse>() }; } }
然后在 src/axios.ts 中创建 axios 实例的地方接收该配置对象:
import { AxiosInstance, AxiosRequestConfig } from "./types"; import Axios from "./core/Axios"; import { extend } from "./helpers/util"; import defaults from "./defaultes"; function getAxios(config: AxiosRequestConfig): AxiosInstance { const context = new Axios(config); const axios = Axios.prototype.request.bind(context); extend(axios, context); return axios as AxiosInstance; } const axios = getAxios(defaults); export default axios;
这样我们就可以在执行 getAxios 创建 axios 对象的时候,把默认配置传入了。现在才算是把创建的默认配置对象 defaults 真正的添加到 Axios 类中了,另外,别忘了给 Axios 类的类型接口定义中添加该字段:
export interface Axios { defaults: AxiosRequestConfig; // ... }
默认配置对象有了之后,接下来,我们就该把用户的配置对象跟默认配置对象做一合并,把合并后配置对象随着请求发出就大功告成啦。
4. 合并配置对象
所谓合并配置对象,就是将默认配置对象 defaults 与用户自己配置的请求配置对象 config 进行合并,然后将合并后的配置对象作为真正的请求配置对象发出请求。合并之前,我们先来观察一下要合并的两个对象:
defaults = { method: 'get', timeout: 0, headers: { common: { Accept: 'application/json, text/plain, */*' } } } userConfig = { url: '/config/post', method: 'post', data: { a: 1 }, headers: { test: '321' } } mergedConfig = { url: '/config/post', method: 'post', data: { a: 1 }, timeout: 0, headers: { common: { Accept: 'application/json, text/plain, */*' } test: '321' } }
通过观察,我们发现,这两个对象的合并可不是简简单单的字段合并,这里面要分情况处理:
对于 timeout 、 responseType 等这些常规属性,合并起来比较容易,即如果用户配置了就用用户配置的,如果用户没配置,则用默认配置的; 对于一些属性如 url 、 method 、 params 、 data ,这些属性都是跟每个请求息息相关的,请求不同从而千变万化,所以像这四个属性我们在合并的时候不管默认配置对象里面有没有,我们只取用户配置的; 对于 header 、 auth 等这些属性就比较复杂了,这些属性的合并可不是取这个不取那个的问题,而是要将默认配置的与用户配置的做一次深度合并。如在 headers 中,字段不相同的要拷贝合并在一起,字段相同的,内容不同也要拷贝合并在一起;了解了以上三种情况后,接下来我们在合并的时候就要分情况处理。
首先,在 src/core 目录下创建 mergeConfig.ts 文件,在该文件内编写合并函数,函数框架如下:
import { AxiosRequestConfig } from "types"; export default function mergeConfig( defaultConfig: AxiosRequestConfig, userConfig?: AxiosRequestConfig ): AxiosRequestConfig { let config = Object.create(null); // 创建空对象,作为最终的合并结果 // 1.常规属性,如果用户配置了就用用户配置的,如果用户没配置,则用默认配置的; // 2.只接受用户配置,不管默认配置对象里面有没有,我们只取用户配置的; // 3.复杂对象深度合并 return config; }
OK,接下里我们就根据不同情况分别处理。
4.1 常规属性
对于常规属性,我们遵循如果用户配置了就用用户配置的,如果用户没配置,则用默认配置的;
// 1.常规属性,如果用户配置了就用用户配置的,如果用户没配置,则用默认配置的; let defaultToUserConfig = [ "baseURL", "transformRequest", "transformResponse", "paramsSerializer", "timeout", "withCredentials", "adapter", "responseType", "xsrfCookieName", "xsrfHeaderName", "onUploadProgress", "onDownloadProgress", "maxContentLength", "validateStatus", "maxRedirects", "httpAgent", "httpsAgent", "cancelToken", "socketPath" ]; defaultToUserConfig.forEach(prop => { userConfig = userConfig || {}; // 如果用户配置里有 if (typeof userConfig[prop] !== "undefined") { // 则用用户配置里的 config[prop] = userConfig[prop]; // 如果用户配置里没有,默认配置里有 } else if (typeof defaultConfig[prop] !== "undefined") { // 则用默认配置里的 config[prop] = defaultConfig[prop]; } });
4.2 只接受用户配置
对于 url 、 method 、 params 、 data 这些属性,只接受用户配置,不管默认配置对象里面有没有,我们只取用户配置的;
// 2.只接受自定义配置,不管默认配置对象里面有没有,我们只取用户配置的; let valueFromUserConfig = ["url", "method", "params", "data"]; valueFromUserConfig.forEach(prop => { userConfig = userConfig || {}; if (typeof userConfig[prop] !== 'undefined') { config[prop] = userConfig[prop]; } });
4.3 复杂对象深度合并
对于 header 、 auth 等这些属性我们就要进行深度合并,例如在默认配置对象和用户配置对象的 headers 属性中,我们需要把两个 headers 内字段不相同的属性要拷贝合并在一起,如果属性字段相同的,那么属性内容不同也要拷贝合并在一起;
// 3.复杂对象深度合并 let mergeDeepProperties = ["headers", "auth", "proxy"]; mergeDeepProperties.forEach(prop => { userConfig = userConfig || {}; if (isObject(userConfig[prop])) { config[prop] = deepMerge(defaultConfig[prop], userConfig[prop]); } else if (typeof userConfig[prop] !== 'undefined') { config[prop] = userConfig[prop]; } else if (isObject(defaultConfig[prop])) { config[prop] = deepMerge(defaultConfig[prop]); } else if (typeof defaultConfig[prop] !== 'undefined') { config[prop] = defaultConfig[prop]; } });
对于上述代码,还是拿 headers 属性举个例子说明一下:
如果在用户配置对象 userConfig 中配置了 headers 属性,并且该属性是个对象,那么就调用 deepMerge 函数把默认配置对象 defaultConfig 中的 headers 和用户配置对象 userConfig 中的 headers 进行合并,最后把合并结果放入最终返回的 config 对象中的 headers ; 如果 userConfig 中的 headers 不是对象,并且不为空,那直接就把它放入最终返回的 config 对象中的 headers ; 如果 userConfig 中的 headers 为空,表示用户没有配置该属性,并且如果 defaultConfig 中的 headers是个对象,那就直接把 defaultConfig 中的 headers 深拷贝一份放入最终返回的 config 对象中的 headers`; 如果 userConfig 中的 headers 为空,并且 defaultConfig 中的 headers 不是对象,也不为空,那直接就把它放入最终返回的 config 对象中的 headers ;这就是深度合并的逻辑,另外,这里面还调用的一个深度合并的工具函数 deepMerge ,接下来,我们就在 src/helpers/util.ts 中实现这个工具函数,该函数支持传入若干个对象,把传入的所有对象进行合并,最后返回。如下:
export function deepMerge(...objs: any[]): any { const result = Object.create(null); for (let i = 0; i < objs.length; i++) { const obj = objs[i]; for (let key in obj) { assignValue(obj[key], key); } } function assignValue(val: any, key: string) { if (isObject(result[key]) && isObject(val)) { result[key] = deepMerge(result[key], val); } else if (isObject(val)) { result[key] = deepMerge({}, val); } else { result[key] = val; } } return result; }
代码说明:
函数内部先创建了一个空对象 result ,作为最终返回的结果对象; 然后遍历传进来所有对象,每个对象再遍历所有的属性,调用 assignValue 子函数将当前遍历的对象中的每个属性都拷贝到 result 上; 把所有传进来的对象遍历完毕后,即把所有对象的所有属性都拷贝到了 result 上,最终将 result 返回;4.4 添加到request方法中
OK,合并逻辑实现好之后,我们就可以在 Axios 类的 request 方法中将默认配置对象与用户配置对象进行合并了。
// src/core/Axios.ts import mergeConfig from "./mergeConfig"; request(url: any, config?: any): AxiosPromise { if (typeof url === "string") { config = config ? config : {}; config.url = url; } else { config = url; } config = mergeConfig(this.defaults, config); // ... }
5. 扁平化headers
经过上面的配置对象合并后,其他属性都可以了,但是合并出来的 headers 却是如下形式的:
headers: { common: { Accept: 'application/json, text/plain, */*' }, post: { 'Content-Type':'application/x-www-form-urlencoded' } }
而真正发请求是所需要的 headers 是这样的:
headers: { Accept: 'application/json, text/plain, */*', 'Content-Type':'application/x-www-form-urlencoded' }
所以,我们还需要把合并后的 headers 扁平化,即把所有的属性提取出来放入 headers 下。这里要注意的是,对于 common 中定义的 header 字段,我们都要提取,而对于 post 、 get 这类提取,需要和该次请求的方法对应。
OK,那么我们就来实现一个函数,用于将合并后的 headers 扁平化,在 src/helpers/headers.ts 中创建 flattenHeaders 函数,如下:
// src/helpers/headers.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 这些属性删掉。最后返回的 headers 就是我们想要的扁平化后的 headers 。
实现好之后,我们就在 src/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 中的。
OK,终于该合并的已经合并完了,接下来,我们就可以编写 demo 来测试下效果如何。
6. demo编写
在 examples 目录下创建 mergeConfig 目录,在 mergeConfig 目录下创建 index.html :
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>mergeConfig demo</title> </head> <body> <script src="/__build__/mergeConfig.js"></script> </body> </html>
接着再创建 app.ts 作为入口文件:
import axios from "src/axios"; import qs from "qs"; axios.defaults.headers.common["NLRX"] = "Hello NLRX"; axios.defaults.headers.post["NLRX1"] = "post NLRX"; axios.defaults.headers.get["NLRX2"] = "get NLRX"; axios({ url: "/api/mergeConfig", method: "post", data: qs.stringify({ a: 1 }), headers: { test: "321" } }).then(res => { console.log(res.data); });
在该 demo 中,我们显示的给默认配置对象添加了 post 、 get 和 common 的 headers ,并且我们在请求中的配置对象也配置了 headers ,另外,我们的默认配置对象默认的会给 post 请求加上 Content-Type 字段,它的值是 application/x-www-form-urlencoded ;
我们可以预测下该请求中的 headers 应该包含哪些内容,由于这个请求时 post 类型,故 axios.defaults.headers.get["NLRX2"] = "get NLRX"; 不应该生效,所以它的 headers 至少应该包含如下:
headers = { // ... Accept: 'application/json, text/plain, */*', Content-Type:'application/x-www-form-urlencoded', NLRX:"Hello NLRX", NLRX1 : "post NLRX", test: "321", // ... }
我们可以在 demo 结果中观察验证是否如此。
接着在 server/server.js 添加新的接口路由:
// 默认配置合并 router.post("/api/mergeConfig", function(req, res) { res.json(req.body); });
最后在根目录下的 index.html 中加上启动该 demo 的入口:
<li><a href="examples/mergeConfig">mergeConfig</a></li>
OK,我们在命令行中执行:
# 同时开启客户端和服务端 npm run server | npm start
接着我们打开 chrome 浏览器,访问 http://localhost:8000/ 即可访问我们的 demo 了,我们点击 mergeConfig ,通过 F12 的 network 部分我们可以看到请求已正常发出,并且请求的 headers 如下:
从结果中我们可以看到,跟我们之前预测的结果完全相符,至此,默认配置合并就已经实现了。
(完)
查看更多关于使用Typescript重构axios(十五)——默认配置的详细内容...