当前位置 : 首页 > 购买指南

# Axios 默认配置与合并策略详解

<|begin▁of▁sentence|># 1. 前言 在上一篇文章中,我们实现了`axios`的基础功能,但到目前为止,我们都只是实现了一个很简单的`axios`,一个最最基础的`axios`,我们还有很多功能没有实现,比如:`axios`的默认配置、拦截器、`xsrf`防御、上传下载进度监控等等。从本篇文章开始,我们就来把这些功能一一实现。 我们先从`axios`的默认配置开始,用过`axios`的同学都知道,`axios`提供了一些默认配置,用户只需要配置一些必需的参数,其他参数可以不必配置,`axios`会使用默认值。那么,`axios`的默认配置都有哪些呢?我们可以在`axios`官网的配置章节看到,如下: - [请求配置](http://www.axios-js.com/zh-cn/docs/#请求配置) - [响应配置](http://www.axios-js.com/zh-cn/docs/#响应结构) 从上面可以看到,`axios`提供的默认配置有很多,我们不可能在这一篇文章中全部实现,我们挑选一些常用的配置来实现,其他的配置实现方式都大同小异,同学们可以自行实现。 我们挑选以下几个配置作为我们第一阶段要实现的默认配置: - `url`:请求的接口地址 - `method`:请求方法 - `baseURL`:请求的基准地址 - `transformRequest`:请求数据转换函数 - `transformResponse`:响应数据转换函数 - `headers`:请求头 - `params`:`url`参数 - `paramsSerializer`:`url`参数序列化函数 - `data`:请求体数据 - `timeout`:超时时间 - `withCredentials`:跨域时是否携带`cookie` - `xsrfCookieName`:`xsrf`的`cookie`名称 - `xsrfHeaderName`:`xsrf`的`header`名称 - `onUploadProgress`:上传进度监控函数 - `onDownloadProgress`:下载进度监控函数 - `auth`:`HTTP`基础认证 - `validateStatus`:自定义合法状态码函数 # 2. 默认配置 ## 2.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, */*", }, }, xsrfCookieName: "XSRF-TOKEN", xsrfHeaderName: "X-XSRF-TOKEN", transformRequest: [ function (data: any, headers: any): any { return data; }, ], transformResponse: [ function (data: any): any { return data; }, ], validateStatus(status: number): boolean { return status >= 200 && status < 300; }, }; 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` 常量,它包含了一些默认的配置值,并且这些配置是我们在未来会陆续实现的。其中 `headers` 做了特殊处理,请求头中 `common` 表示任何请求方法都有的请求头配置,然后我们还分别对不需要携带数据的请求方法如 `get`、`delete`、`head`、`options` 和需要携带数据的请求方法如 `post`、`put`、`patch` 分别进行了请求头配置,并且需要携带数据的请求方法我们默认设置了它的 `Content-Type` 属性为 `application/x-www-form-urlencoded`。 ## 2.2 配置合并策略 定义了默认配置后,我们在发送请求的时候需要把用户传入的配置与这些默认配置做合并,合并的规则是:优先使用用户传入的配置,如果用户没有传入,则使用默认配置。 接下来,我们就来实现配置的合并策略。 我们在`src`目录下创建`core`目录,并在`core`目录下创建`mergeConfig.ts`文件,在该文件内实现配置合并。 ```typescript // src/core/mergeConfig.ts import { AxiosRequestConfig } from "../types"; import { deepMerge, isPlainObject } 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 (isPlainObject(val2)) { return deepMerge(val1, val2); } else if (typeof val2 !== "undefined") { return val2; } else if (isPlainObject(val1)) { 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] = deepMergeStrat; }); 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; } ``` 合并规则如下: - 默认合并策略:如果`config2`中有配置,则使用`config2`中的配置,否则使用`config1`中的配置; - 只从`config2`合并策略:只取`config2`中的配置,合并到最终配置中,`config1`中对应的配置忽略; - 深度合并策略:对于`config1`和`config2`都是对象的配置,如`headers`,我们需要对它们进行深度合并,并且对于`config2`中的配置,如果它是普通对象,则与`config1`中对应的配置进行深度合并,否则直接取`config2`中的配置;如果`config2`中没有配置,但是`config1`中有配置,并且是普通对象,则对`config1`中的配置进行深度合并,否则直接取`config1`中的配置; 我们通过 `stratKeysFromVal2` 指定了哪些属性使用只从 `config2` 合并的策略,即 `url`、`params`、`data`,这些属性都是和请求强相关的,所以我们只从用户传入的配置 `config2` 中获取,默认配置 `config1` 中获取的无效。 我们还通过 `stratKeysDeepMerge` 指定了哪些属性使用深度合并策略,这里只对 `headers` 属性使用了深度合并策略,因为 `headers` 属性比较复杂,它包含了 `common`、`post`、`get` 等,我们需要对它们进行深度合并。 对于其它属性,我们使用默认合并策略。 ## 2.3 修改 Axios 类 配置合并策略实现好之后,我们需要修改`Axios`类,在发送请求之前,先把用户传入的配置与默认配置做合并。 ```typescript // src/core/Axios.ts import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from "../types"; import { parseHeaders } from "../helpers/headers"; import { createError } from "../helpers/error"; import { isURLSameOrigin } from "../helpers/url"; import cookie from "../helpers/cookie"; import { isFormData } from "../helpers/util"; import defaults from "../defaults"; import mergeConfig from "./mergeConfig"; export default class Axios { defaults: AxiosRequestConfig; constructor(initConfig: AxiosRequestConfig) { this.defaults = initConfig; } request(url: any, config?: any): AxiosPromise { if (typeof url === "string") { if (!config) { config = {}; } config.url = url; } else { config = url; } config = mergeConfig(this.defaults, config); // 设置请求方法,默认为get config.method = config.method.toLowerCase(); // 待实现中间件 // 待实现请求/响应拦截器 // 发起请求 return dispatchRequest(config); } get(url: string, config?: AxiosRequestConfig): AxiosPromise { return this._requestMethodWithoutData("get", url, config); } delete(url: string, config?: AxiosRequestConfig): AxiosPromise { return this._requestMethodWithoutData("delete", url, config); } head(url: string, config?: AxiosRequestConfig): AxiosPromise { return this._requestMethodWithoutData("head", url, config); } options(url: string, config?: AxiosRequestConfig): AxiosPromise { return this._requestMethodWithoutData("options", url, config); } post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise { return this._requestMethodWithData("post", url, data, config); } put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise { return this._requestMethodWithData("put", url, data, config); } patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise { return this._requestMethodWithData("patch", url, data, config); } _requestMethodWithoutData( method: string, url: string, config?: AxiosRequestConfig ) { return this.request( Object.assign(config || {}, { method, url, }) ); } _requestMethodWithData( method: string, url: string, data?: any, config?: AxiosRequestConfig ) { return this.request( Object.assign(config || {}, { method, url, data, }) ); } } function dispatchRequest(config: AxiosRequestConfig): AxiosPromise { throwIfCancellationRequested(config); processConfig(config); return xhr(config).then( (res) => { return transformResponseData(res); }, (e) => { if (e && e.response) { e.response = transformResponseData(e.response); } return Promise.reject(e); } ); } 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, paramsSerializer, baseURL } = config; if (baseURL && !isAbsoluteURL(url!)) { return combineURL(baseURL, url!); } return buildURL(url!, params, paramsSerializer); } function transformRequestData(config: AxiosRequestConfig): any { return transformRequest(config.data, config.headers, config.transformRequest); } function transformHeaders(config: AxiosRequestConfig): any { const { headers = {}, data, method } = config; return processHeaders(headers, data, method!.toLowerCase()); } function transformResponseData(res: AxiosResponse): AxiosResponse { res.data = transformResponse( res.data, res.headers, res.config.transformResponse ); return res; } function transformRequest( data: any, headers: any, fns?: any ): any { if (!fns) { return data; } if (!Array.isArray(fns)) { fns = [fns]; } fns.forEach((fn: any) => { data = fn(data, headers); }); return data; } function transformResponse( data: any, headers: any, fns?: any ): any { if (!fns) { return data; } if (!Array.isArray(fns)) { fns = [fns]; } fns.forEach((fn: any) => { data = fn(data, headers); }); return data; } function xhr(config: AxiosRequestConfig): AxiosPromise { return new Promise((resolve, reject) => { const { url, method = "get", data = null, headers, responseType, timeout, cancelToken, withCredentials, xsrfCookieName, xsrfHeaderName, onDownloadProgress, onUploadProgress, auth, validateStatus, } = config; const request = new XMLHttpRequest(); request.open(method.toUpperCase(), url!, true); configureRequest(); addEvents(); processHeaders(); processCancel(); request.send(data); function configureRequest(): void { if (responseType) { request.responseType = responseType; } if (timeout) { request.timeout = timeout; } if (withCredentials) { request.withCredentials = withCredentials; } } function addEvents(): void { request.onreadystatechange = function handleLoad() { if (request.readyState !== 4) { return; } if (request.status === 0) { return; } const responseHeaders = parseHeaders(request.getAllResponseHeaders()); const responseData = responseType && responseType !== "text" ? request.response : request.responseText; const response: AxiosResponse = { data: responseData, status: request.status, statusText: request.statusText, headers: responseHeaders, config, request, }; handleResponse(response); }; request.onerror = function handleError() { reject(createError("Network Error", config, null, request)); }; request.ontimeout = function handleTimeout() { reject( createError( `Timeout of ${timeout} ms exceeded`, config, "ECONNABORTED", request ) ); }; if (onDownloadProgress) { request.onprogress = onDownloadProgress; } if (onUploadProgress) { request.upload.onprogress = onUploadProgress; } } function processHeaders(): void { if (isFormData(data)) { delete headers["Content-Type"]; } if ((withCredentials || isURLSameOrigin(url!)) && xsrfCookieName) { const xsrfValue = cookie.read(xsrfCookieName); if (xsrfValue && xsrfHeaderName) { headers[xsrfHeaderName] = xsrfValue; } } if (auth) { headers["Authorization"] = "Basic " + btoa(auth.username + ":" + auth.password); } Object.keys(headers).forEach((name) => { if (data === null && name.toLowerCase() === "content-type") { delete headers[name]; } else { request.setRequestHeader(name, headers[name]); } }); } function processCancel(): void { if (cancelToken) { cancelToken.promise .then((reason) => { request.abort(); reject(reason); }) .catch( /* istanbul ignore next */ () => { // do nothing } ); } } function handleResponse(response: AxiosResponse): void { if (!validateStatus || validateStatus(response.status)) { resolve(response); } else { reject( createError( `Request failed with status code ${response.status}`, config, null, request, response ) ); } } }); } function throwIfCancellationRequested(config: AxiosRequestConfig): void { if (config.cancelToken) { config.cancelToken.throwIfRequested(); } } ``` 代码非常多,我们一点点来看。 首先,我们在`Axios`类的构造函数中增加了一个`defaults`属性,用来存储默认配置。 ```typescript export default class Axios { defaults: AxiosRequestConfig; constructor(initConfig: AxiosRequestConfig) { this.defaults = initConfig; } // ... } ``` 然后,我们在`request`方法中,把用户传入的配置与默认配置做了合并。 ```typescript request(url: any, config?: any): AxiosPromise { if (typeof url === "string") { if (!config) { config = {} } config.url = url } else { config = url } config = mergeConfig(this.defaults, config) // ... } ``` 接下来,我们修改了`transformURL`函数,在函数内部增加了对`baseURL`的处理。 ```typescript function transformURL(config: AxiosRequestConfig): string { const { url, params, paramsSerializer, baseURL } = config; if (baseURL && !isAbsoluteURL(url!)) { return combineURL(baseURL, url!); } return buildURL(url!, params, paramsSerializer); } ``` 如果配置了 `baseURL`,并且 `url` 不是绝对地址,我们会把 `baseURL` 和 `url` 做拼接,这里我们使用了 `isAbsoluteURL` 和 `combineURL` 两个辅助函数,我们在 `src/helpers/url.ts` 中实现它们。 ```typescript // src/helpers/url.ts // ... export function isAbsoluteURL(url: string): boolean { return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url); } export function combineURL(baseURL: string, relativeURL: string): string { return relativeURL ? baseURL.replace(/\/+$/, "") + "/" + relativeURL.replace(/^\/+/, "") : baseURL; } ```

栏目列表