Axios
是前端最常用的异步请求模块,了解 Axios
源码有助于我们在开发中,更合理的去配置 Axios
,也能更快的定位请求失败时的异常。
目录结构
├── /dist/ # 项目输出目录
├── /lib/ # 项目源码目录
│ ├── /cancel/ # 定义取消功能
│ ├── /core/ # 一些核心功能
│ │ ├── Axios.js # axios的核心主类
│ │ ├── dispatchRequest.js # 用来调用http请求适配器方法发送请求
│ │ ├── InterceptorManager.js # 拦截器构造函数
│ │ └── settle.js # 根据http响应状态,改变Promise的状态
│ ├── /helpers/ # 一些辅助方法
│ ├── /adapters/ # 定义请求的适配器 xhr、http
│ │ ├── http.js # 实现http适配器
│ │ └── xhr.js # 实现xhr适配器
│ ├── axios.js # 对外暴露接口
│ ├── defaults.js # 默认配置
│ └── utils.js # 公用工具
├── package.json # 项目信息
├── index.d.ts # 配置TypeScript的声明文件
└── index.js # 入口文件
复制代码
语法糖
Axios 的常用API
import axios from 'axios';
const apiLogin = '/login';
const apiUser = '/user';
const headers = {'x-token': 'asdfasdf'};
const data = {username: '', pwd: ''};
axios.defaults.timeout= 60 * 10000;
axios.interceptors.response.use(response => {
return response.data;
}, error => {
return Promise.reject(error);
})
// axios(options);
axios({ url: apiLogin, method: 'post', headers })
// axios(url[, options]);
axios(apiLogin, {mehtod: 'post', headers }
// axios[method](url[, options])
// 适用方法 get/delete/head/options
axios.get(apiUser, { headers })
// axios[method](url[, data[, options]])
// 适用方法 put/post/patch
axios.post(apiLogin, data, { headers })
axios.request({url: apiLogin, mehtod: 'post', headers })
复制代码
语法糖的源码解析
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
// 等同于 var instance = Axios.prototype.request.bind(context);
// intance 等同于 context.request,以便于 axios(options) 或 axios(url, options) 调用
var instance = bind(Axios.prototype.request, context);
// 将 Axios.prototype 原型上的所有方法扩展到 instance 上,方法的上下文指向 context
// 给 intance 扩展其它方法,以便于 axios.get()、axios.request()、axios.all() 等方式调用
utils.extend(instance, Axios.prototype, context);
// 将 context(axios的实例) 上的属性 defaults、 interceptors 扩展到 instance 上
// 以实现 axios.defaults.timeout、axios.interceptors.request.use()方式调用
utils.extend(instance, context);
return instance;
}
复制代码
Axios 的类结构
Axios的属性和原型方法
function Axios(instanceConfig) {
// 请求配置
this.defaults = instanceConfig;
// 请求和响应拦截器的配置
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
// 实现 axios(config)、axios(url, config) 的调用方式
Axios.prototype.request = function request(config) {
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
config = mergeConfig(this.defaults, config);
var promise;
// ... 省略实现
return promise;
};
Axios.prototype.getUri = function getUri(config) {};
// 实现 axios.get(url[, config]) 调用方式
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: (config || {}).data
}));
};
});
// 实现 axios.post(url[, data[, config]]) 调用方式
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
Axios.prototype[method] = function(url, data, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: data
}));
};
});
复制代码
请求的基本流程
拦截器
拦截器的分类
拦截器分为 请求拦截器 和 响应拦截器,使用以下方式进行注册
- 请求拦截器:用于修改
config
配置数据, 使用interceptors.request.use()
注册 - 响应拦截器:用于修改
response
响应数据, 使用interceptors.response.use()
注册
// 请求拦截器
axios.interceptors.request.use(
(config) => { return config; },
(error) => { return Promise.reject(error);}
);
// 响应拦截器
axios.interceptors.response.use(
(response) => { return response; },
(error) => { return Promise.reject(error);}
);
复制代码
源码中如何处理拦截器?
先看 Promise 的链式调用
在没有添加任何拦截器时,axios
的 promise
链式执行机制
- 先将配置数据
config
包装为一个promise
对象,使其拥有then
方法 - 将执行
XMLHttpRequest
的方法包装为promise
, 使其可以链接调用 - 通过then方法执行,将
config
传递给dispatchRequest
- 通过
dispatchRequest
执行,将response
传递出来
const config = {url: '/login', data: {username: 'abc'}};
// 使用 Promise.resolve 包装config为一个promise对象
let promise = Promise.resolve(config);
// 包装 xhr 请求为promise
const dispatchRequest = config => {
return new Promise((resolve, reject) => {
// 执行请求...
const response = {code: 0, data: {}};
resolve(response)
})
}
// 定义一个 promise 队列
const chain = [dispatchRequest, undefined];
// 执行 promise 队列
promise = promise.then(chain.shift(), chain.shift());
// 等价于 promise = promise.then(dispatchRequest, undefined);
// 等价于 promise = promise.then((config) => dispatchRequest(config), undefined);
复制代码
拦截器的处理
Axios.prototype.request = function request(config) {
// ... 省略
config = mergeConfig(this.defaults, config);
// ... 省略
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
// 将请求拦截器添加到 chain 的前面
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 将响应拦截器添加到 chain 的后面
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
/*
chain = [
reqSuccessFn1, reqErrorFn1,
reqSuccessFn2, reqErrorFn2,
...
dispatchRequest, undefined,
resSuccessFn1, resErrorFn1,
resSuccessFn2, resErrorFn2,
...
]
*/
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
复制代码
promise
链的执行流程
- 执行 请求拦截器,处理
config
- 处理好的
config
传递给dispatchRequest
- 执行 异步请求
dispatchRequest
并返回一个promise
,传递请求结果response
- 执行 响应拦截器,处理
response
- 返回
response
或error
configPromise
// reqSuccessFn1, reqErrorFn1,
.then(config => {
return reqSuccessFn1(config)
}, () => {
return reqErrorFn1()
})
// reqSuccessFn2, reqErrorFn2,
.then(config => {
return reqSuccessFn2(config)
}, (err) => {
return reqErrorFn2(err)
})
// dispatchRequest, undefined,
.then(config => {
return dispatchRequest(config)
}, undefined)
// resSuccessFn1, resErrorFn1,
.then(response => {
return resSuccessFn1(response)
}, (err) => {
return resErrorFn1(err)
})
// resSuccessFn2, resErrorFn2,
.then(response => {
return resSuccessFn2(response)
}, (err) => {
return resErrorFn1(err)
})
.then(response => {
console.log(response)
})
.catch(err => {
console.log(err)
})
复制代码
超时处理
axios
配置里有一个timeout
配置项,默认为 0timeout
会直接赋值给xhr.timeout
config
里是可以自定义超时的消息内容的config.timeoutErrorMessage
var xhr = new XMLHttpRequest();
xhr.open('GET', '/server', true);
xhr.timeout = 2000; // 超时时间,单位是毫秒 ms
xhr.onload = function () {
// 请求完成。在此进行处理。
};
// 监听超时事件
xhr.ontimeout = function handleTimeout() {
// 超时的消息内容
var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
// 这里可以看出来,config 里是可以自定义超时的消息内容的
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(createError(
timeoutErrorMessage,
config,
config.transitional && config.transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',
request));
// Clean up request
request = null;
};
xhr.send(null);
复制代码
超时的异常处理
axios.defaults.timeout=30*1000;
// 单个请求处理超时
axios(url).catch(error => {
const { message } = error;
if(error.message.includes('timeout')){
console.log('数据响应超时,请刷新重试')
}
})
// 全局处理超时
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
if(error.message.includes('timeout')){
console.log('数据响应超时')
}
return Promise.reject(error);
});
复制代码
取消请求
Axios 中取消请求的用法
取消所有请求
请求配置通过使用同一个 cancelToken
,可以一次性取消所有请求
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios
.get('/cancel/server', {
cancelToken: source.token
})
.then(function (response) {
console.log(response.data)
})
.catch(function (err) {
console.log(err)
});
axios
.get('/cancel/server',{
cancelToken: source.token
})
.then(function (response) {
console.log(response.data)
})
.catch(function (err) {
console.log(err)
});
setTimeout(() => {
source.cancel('我取消了所有的请求');
}, 6*1000)
复制代码
取消单个请求
请求配置中,使用不同 cancelToken
可以按需取消不同的请求
let cancel1;
let cancel2;
axios
.get('/cancel/server',{
cancelToken: new CancelToken(function executor(c) {
cancel1 = c;
})
})
.then(function (response) {
console.log(response.data)
})
.catch(function (err) {
console.log(err)
});
axios
.get('/cancel/server',{
cancelToken: new CancelToken(function executor(c) {
cancel2 = c;
})
})
.then(function (response) {
console.log(response.data)
})
.catch(function (err) {
console.log(err)
});
setTimeout(() => {
cancel1('我取消了 1 的请求');
}, 3*1000)
setTimeout(() => {
cancel2('我取消了 2 的请求');
}, 6*1000)
复制代码
取消请求的源码解析
CancelToken
源码路径 lib/cancel/CancelToken.js
new CancelToken
产出一个cancelToken
实例,实例上有一个promise
对象new CancelToken
接收的参数为一个函数,这个函数执行,会将promise
状态变成resolved
,从而触发promise
链中注册的then
方法CancelToken.source()
会返回一个CancelToken
实例token
和一个取消的函数cancel
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
// 生成一个promise 属性(一个promise对象),把resolve 方法赋值给 resolvePromise 变量
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
// 接收的函数的参数是一个取消的异常消息
// 函数执行时,会将 this.promise 状态改为 resolved,并传递取消的消息对象给注册的 then 方法
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
// 生成取消的消息对象,赋值给 reason 属性
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
// CancelToken.source() 静态方法会返回一个 {token实例、取消方法}
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
复制代码
cancelToken
的监听和触发
文件 lib/adapters/xhr.js
- 如果
cancelToken
注册的cancel
函数执行,则会将promsie
的状态变为resolved
resolved
之后,then
方法注册的回调会触发,会调用xhr.abort()
执行,从而取消请求reject(cancel)
会将cancel
消息对象传递出去,外面请求通过catch
方法接收到 取消消息对象cancel
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// ...
var request = new XMLHttpRequest();
// ...
// 168行
if (config.cancelToken) {
// 如果请求还没有返回,执行cancel会触发这里的then回调执行
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
// cancel 为接收到的取消消息对象
reject(cancel);
// request 实例置为null
request = null;
});
}
// ...
})
}
复制代码
如果cancel执行时,还没有发起请求,或请求已经完成,那么会直接中断执行,抛出一个取消的消息,被外层的promise.catch
所捕获
文件 lib/core/dispatchRequest.js
// 23行
module.exports = function dispatchRequest(config) {
// 请求未开始
throwIfCancellationRequested(config);
// ...
return adapter(config).then(function onAdapterResolution(response) {
// 请求已完成,返回成功
throwIfCancellationRequested(config);
// ...
return response;
}, function onAdapterRejection(reason) {
// 请求已完成,返回异常
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
// ...
}
return Promise.reject(reason);
});
};
复制代码
throwIfCancellationRequested
会调用 cancelToken.throwIfRequested()
// lib/core/dispatchRequest.js
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
// lib/cancel/CancelToken.js
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
// 抛出取消的消息
throw this.reason;
}
};
复制代码
数据转换器和Content-Type
Axios 中有三个数据转换器,两个用于请求数据转换,一个用于响应数据转换,请求数据转换器又分为url数据转换器和body(请求体)数据转换器。
transformRequest
请求数据转换器,对请求体(body)中的数据格式化,修改headersparamsSerializer
对配置数据params
做转换,params
一般用于get/delete/head
请求,其它请求也可以设置params
用于链接传参 ,主要指定数据序列化(字符串化)的规则transformResponse
响应数据转换器,对响应数据进行解析或格式化
注意 transformRequest
与 transformResponse
的配置是一个函数数组,而 paramsSerializer
值是一个函数
transformRequest 与 transformResponse
文件: lib/defaults.js
中有 transformRequest
与 transformResponse
的默认配置
transformRequest: [function transformRequest(data, headers) {
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
// 如果body的类型是 FormData、Buffer、数据流、文件则返回 data,对headers不做处理,FormData浏览器会自动添加 contentType
if (utils.isFormData(data) ||
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
// 如果是URLSearchParams对象,没有设置则设置 contentType为 'application/x-www-form-urlencoded;charset=utf-8' 数据被编码成以&分隔的键值对
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
// 如果是 JSON对象,没有设置则设置 contentType 为 application/json, 并将data进行序列化
if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {
setContentTypeIfUnset(headers, 'application/json');
return JSON.stringify(data);
}
return data;
}],
transformResponse: [function transformResponse(data) {
var transitional = this.transitional;
var silentJSONParsing = transitional && transitional.silentJSONParsing;
var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
var strictJSONParsing = !silentJSONParsing && this.responseType === 'json';
if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) {
try {
return JSON.parse(data);
} catch (e) {
// JSON.parse 出错后,是否抛出异常信息,是的话,将抛出异常,不返回原始数据
if (strictJSONParsing) {
if (e.name === 'SyntaxError') {
throw enhanceError(e, this, 'E_JSON_PARSE');
}
throw e;
}
}
}
return data;
}],
复制代码
contentType
对于delete
,get
,head
是没有必要设置的,因为他们的参数是在url中传递的,只有 请求体body
中的数据才需要设置contentType
- 如果不设置
contentType
,post
,put
,patch
默认设置为application/x-www-form-urlencoded
- 如果
body
的类型是FormData
,axios
会删除contentType
, 浏览器会默认设置contentType
为multipart/form-data;
覆盖和追加 transformRequest
与 transformResponse
import axios from 'axios'
// 追加请求转换器
axios.defaults.transformRequest.push((data, headers) => {
// ...处理data
return data;
});
// 覆盖请求转换器
axios.defaults.transformRequest = [(data, headers) => {
// ...处理data
return data;
}];
// transformResponse 同理
复制代码
paramsSerializer
paramsSerializer
的调用时机
源码文件 lib/adapters/xhr.js
32行
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// ...
// 在建立连接时,将 params 序列化成字符串,拼接在 url 后面
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// ...
});
};
复制代码
buildURL
在源码文件 lib/helpers/buildURL.js
module.exports = function buildURL(url, params, paramsSerializer) {
// 如果你没有配置 params,则返回url,不处理
if (!params) {
return url;
}
var serializedParams;
// 如果配置了 paramsSerializer 序列化函数,则调用函数
if (paramsSerializer) {
serializedParams = paramsSerializer(params);
// 如果配置的 params 是 URLSearchParams 对象,则直接 toString
} else if (utils.isURLSearchParams(params)) {
serializedParams = params.toString();
// 如果你没有指定 paramsSerializer 序列化方式,则使用默认的规则进行序列化
} else {
var parts = [];
// 两层 forEach 循环 params
utils.forEach(params, function serialize(val, key) {
// 【重要】 如果 value 是 null 或 undefinded, 则会删除当前这个key,有时候服务端也需要值为 null 的key
if (val === null || typeof val === 'undefined') {
return;
}
// 【重要】如果 value 是数组,则key后面拼接上 [] 字符,有时服务端需要 [] 中带索引
if (utils.isArray(val)) {
key = key + '[]';
} else {
// 不是数组类型的value,会先变成数组,方便下面循环
val = [val];
}
// 循环 value 数组
utils.forEach(val, function parseValue(v) {
// 日期对象会转换为 IOSString
if (utils.isDate(v)) {
v = v.toISOString();
// 其它对象为直接使用 JSON.stringify
} else if (utils.isObject(v)) {
v = JSON.stringify(v);
}
// 将 key和value都进行 encode,但会保留 : $ , + [ ] 这几个符号
parts.push(encode(key) + '=' + encode(v));
});
});
// 通过 & 拼接起来
serializedParams = parts.join('&');
}
// 如果生成了 序列化的字符串
if (serializedParams) {
// 如果url带有 # 号,请求时,会将 # 及后面的字符都去掉
var hashmarkIndex = url.indexOf('#');
if (hashmarkIndex !== -1) {
url = url.slice(0, hashmarkIndex);
}
// url 中有 ? 号,则用 & 拼接,没有则用 ? 拼接
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams;
}
// 返回拼接好的 url
return url;
};
复制代码
重要
params
序列化方式会直接影响服务端的接收,这里要和服务端约定好序列化的规则,特别是数组序列化为字符串的规则
不同的规则,会产出不同的字符串,会直接影响服务端数据接收方式,我们可以使用 qs 个包来进行数据的序列化
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' })
// 'a[0]=b&a[1]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' })
// 'a[]=b&a[]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })
// 'a=b&a=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'comma' })
// 'a=b,c'
复制代码
示例
为单个请求指定 params
的序列化规则
import Qs from 'qs'
axios.get('/userList', {
params: {
page: 1,
pageSize: 20,
keywords: 'a',
ids: [1, 2, 3]
},
paramsSerializer: function(params) {
return Qs.stringify(params, {arrayFormat: 'brackets'})
},
}
复制代码
请所有请求指定 params
的序列化规则
import Qs from 'qs'
axios.defaults.paramsSerializer = params => Qs.stringify(params, {arrayFormat: 'brackets'})
复制代码
header的配置
种配置 header 的方式
import axios from 'axios'
// 通用header
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; // xhr标识
// 针对某种请求类型设置的header
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';
// 设置某次请求的header
axios.get(url, {
headers: {
'Authorization': 'whr1',
},
})
复制代码
源码文件 lib/core/dispatchRequest.js
38行
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers
);
复制代码
header 配置的优先级
具体请求的配置 > 方法类型配置 > common 配置
JWT中如何配置 header ?
如果你的项目的用户认证是JWT方式,那么用户认证token一般是通过 header 传递给服务端进行校验的,一般配置流程如下:
- 用户登录
- 服务端响应登录数据和token
- 前端存储 token 到本地,一般为 localstorage 也可以是 cookie
- 后面需要认证的请求都把 token 带到header里传给服务端
- 服务端校验token是否有效,有效则正常响应,无效则返回异常消息
// 配置请求时, header 中带上 token
axios.interceptors.request.use(function (config) {
const token = window.localstorage.getItem('token');
if(token){
config.headers.common['Authorization'] = token;
}
return config;
});
// 业务中 登录后缓存 token
axios.post( '/login', { username: '', pwd: '' }).then(res => {
const token = res.data.token
window.localstorage.setItem('token', token)
})
复制代码