Axios
是一个基于 promise
的网络请求库,可以用于浏览器和 node.js
。
在服务端它使用原生 node.js http
模块, 而在浏览器则使用 XMLHttpRequest
。
它是前端项目中最流行的http
请求插件,拥有很多强大的功能:
- 从浏览器创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求和响应数据
- 取消请求
- 自动转换JSON数据
了解它的基本原理,有利于我们更好的应用封装和扩展。
1,源码目录结构
Axios1.3x
的目录结构如下:
├── bin # 与构建相关的脚本和配置文件
├── dist # 构建后的文件
├── examples # 文档案例
├── lib # axios库源码
│ ├── abapter # 请求适配器xhr/http
│ ├── cancel # 取消请求相关
│ ├── core # Axios实例/请求/拦截器等核心功能源码
│ ├── defaults # 默认配置
│ ├── env
│ ├── helpers # 请求工具函数:包括对请求头和响应头的解析、对URL的处理,以及对各种错误的处理等。
│ ├── platform # 特定平台:浏览器/node
│ ├── axios.js // 入口文件
│ ├── utils.js // 全局通用封装方法,导出一个utils工具对象
├── sandbox # 沙盒模拟请求
├── templates # 模板文件
├── test # 测试相关
/lib
目录就是axios
的插件源码,是我们需要掌握了解的,我们首先从入口文件axios.js
开始。
2,axios实例的定义
查看axios.js
源码:
// lib/axios.js
import utils from './utils.js';
import bind from './helpers/bind.js';
import Axios from './core/Axios.js';
import mergeConfig from './core/mergeConfig.js';
import defaults from './defaults/index.js';
import formDataToJSON from './helpers/formDataToJSON.js';
import CanceledError from './cancel/CanceledError.js';
import CancelToken from './cancel/CancelToken.js';
import isCancel from './cancel/isCancel.js';
import {VERSION} from './env/data.js';
import toFormData from './helpers/toFormData.js';
import AxiosError from './core/AxiosError.js';
import spread from './helpers/spread.js';
import isAxiosError from './helpers/isAxiosError.js';
import AxiosHeaders from "./core/AxiosHeaders.js";
import HttpStatusCode from './helpers/HttpStatusCode.js';
# 使用默认配置:创建一个axios实例【wrap函数】,为了导出使用
const axios = createInstance(defaults);
# 在axios实例上挂载相关的属性和方法
// 挂载Axios类
axios.Axios = Axios;
// 挂载取消请求接口
axios.CanceledError = CanceledError;
axios.CancelToken = CancelToken;
axios.isCancel = isCancel;
axios.VERSION = VERSION;
// 挂载转formData格式方法
axios.toFormData = toFormData;
// 挂载错误处理类
axios.AxiosError = AxiosError;
// 取消请求兼容处理
axios.Cancel = axios.CanceledError;
// 挂载all方法【批量请求】
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = spread;
// Expose isAxiosError
axios.isAxiosError = isAxiosError;
// 挂载合并config方法
axios.mergeConfig = mergeConfig;
axios.AxiosHeaders = AxiosHeaders;
// 挂载转json方法
axios.formToJSON = thing => formDataToJSON(utils.isHTMLForm(thing) ? new FormData(thing) : thing);
// 挂载http状态码对象
axios.HttpStatusCode = HttpStatusCode;
axios.default = axios;
# 导出axios实例
export default axios
根据上面源码可以看出axios.js
入口文件的主要作用就是使用默认配置创建了一个axios
实例,并且在实例上挂载了一系列的属性和方法,最后导出axios
实例,我们就可以在前端项目中进行引入使用。
# 在项目中引入使用
import axios from 'axios'
// 创建请求实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 10000
})
// 注册拦截器
service.interceptors.request.use()
service.interceptors.response.use()
export default service
在使用之前,我们先打印一下引入的axios
实例,查看它的结构:
import axios from 'axios'
console.log(axios)
根据打印结果可以发现,axios
实例身上挂载属性和方法符合前面的定义。
同时我们我们还注意到了这里的axios
实例是一个wrap函数,不是我们想象的普通对象。
我们要了解其中的缘由,还得继续需要深入了解一下:
const axios = createInstance(defaults);
createInstance
查看createInstance
方法源码:
# 创建请求实例
function createInstance(defaultConfig) {
# 根据默认配置,初始化创建了一个axios实例
const context = new Axios(defaultConfig);
# 将axios对象实例包装为一个函数实例,
// 其实就是返回了一个wrap函数,它的内部执行了axios.request方法【所以,这里的wrap函数就是请求实例】
const instance = bind(Axios.prototype.request, context);
// Copy axios.prototype to instance
# 将axios对象实例上的内容:全部继承到函数实例身上
utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
// Copy context to instance
utils.extend(instance, context, null, {allOwnKeys: true});
# 定义实例的create方法,内部为调用的createInstance
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
# 返回axios函数实例
return instance;
}
根据上面的代码可以看出,首先根据默认配置对象defaultConfig
创建一个axios
对象实例【context
】,然后使用bind
方法创建了真正的axios
函数实例【instance
】。
这里我们需要查看bind
方法源码:
export default function bind(fn, thisArg) {
return function wrap() {
return fn.apply(thisArg, arguments);
};
}
【转换一下】这里的instance
等同于:
const instance = function wrap() {
return Axios.prototype.request.apply(Axios, arguments)
}
而createInstance
方法最后导出的实例是instance
,所以axios
实例其实就是wrap
函数。在调用bind
方法后,使用了utils.extend
方法,将axios
对象实例身上的所有内容复制到了wrap
函数身上。所以最终生成的axios
实例就是一个wrap
函数,符合我们前面打印的axios
实例结构。
这里不直接使用axios
对象实例,而是转换成函数实例。其实是为了扩展Axios API
,让用户可以直接使用axios(config)
发起请求。
// 发起一个post请求
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});
通过这样的处理,axios
可以支持更多方式来发起请求:
axios(config)
axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.post(url[, data[, config]])
等等...
当然它们的底层原理都是依赖于axios.request(config)
方法,后面我们会详细讲解这个方法。
根据前面的解析,我们已经知道了项目中引入的axios
实例的由来以及为啥它是一个函数实例。
下面我们再来看看项目中的一些应用:
import axios from 'axios'
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 10000
})
// 定义请求拦截器
service.interceptors.request.use(
config => {},
error => {}
)
// 定义响应拦截器
service.interceptors.response.use(
res => {},
error => {}
)
export default service
// 引入上面导出的axios实例
import axios from '../utils/axios'
// 登录
export function login(data) {
return axios({
url: '/api/admin/login',
method: 'post',
data
})
}
在项目中:我们可以通过axios
实例定义请求和响应拦截器,以及发起真正的http
请求。而在前面我们已经知道axios
实例【即wrap
函数】的属性和方法都是通过继承new Axios()
创建的axios
对象实例得来的,所以我们要了解拦截器和发起http
请求的方法实现,还得回到真正的axios对象中。
const context = new Axios(defaultConfig);
class Axios
查看Axios
类的源码:
class Axios {
constructor(instanceConfig) {
# 初始化两个属性
// 存储默认配置对象
this.defaults = instanceConfig;
// 创建拦截器对象interceptors
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
# 发起http请求
request() {
...
# 返回响应内容
return promise;
}
getUri(config) {}
}
Axios
类的源码内容并不多,构造器里只是简单的初始化了两个实例属性defaults
、interceptors
。这里要注意一下interceptors
拦截器对象,它里面有两个属性:request/response
,对应着请求拦截器和响应拦截器。然后Axios
里面的核心内容是它的request
方法了,这个方法的作用是发起真正的http
请求以及返回请求成功的响应内容,可以说它是axios
插件的核心,它的内容比较多,所以我们后面再讲解这部分内容。
在导出Axios
类之前,还做了一些额外的挂载,也需要我们了解一下。
// utils工具对象
import utils from './../utils.js';
class Axios {}
# 在Axios.prototype原型对象上挂载请求方法
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
return this.request(mergeConfig(config || {}, {
method,
url,
data: (config || {}).data
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
function generateHTTPMethod(isForm) {
return function httpMethod(url, data, config) {
return this.request(mergeConfig(config || {}, {
method,
headers: isForm ? {
'Content-Type': 'multipart/form-data'
} : {},
url,
data
}));
};
}
Axios.prototype[method] = generateHTTPMethod();
Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
});
// 导出Axios类
export default Axios;
utils
是axios
仓库中的全局工具对象,它里面封装了很多自定义的方法。
utils.forEach(list, fn)
这里的utils.forEach
方法的作用是:循环list
参数,将每一项元素作为fn
的参数并调用fn
。
所以这里的作用就是:在导出Axios
类之前,执行两个循环,将get
、post
、put
、delete
等方法都挂载到Axios.prototype
原型对象身上,所以我们在项目中就可以通过使用axios.get
或者axios.post
来便捷的发起http
请求。
并且我们可以看出这些方法的内部都是调用的this.request()
,即axios.request()
。所以在这里我们就可以知道,axios
插件中的所有请求方法,比如axios.get()
、axios.post()
这些等等,其内部都是依赖于axios.request
方法,它才是真正的http
请求方法。
在解析request
方法之前,我们还得先看看拦截器interceptors
的相关实现。
3,拦截器的定义
class InterceptorManager
查看InterceptorManager
类的源码:
# 拦截器class
class InterceptorManager {
constructor() {
// 初始化handlers【存储处理器】
this.handlers = [];
}
# 向handlers数组中添加handler处理器对象
use(fulfilled, rejected, options) {
// 参数:请求成功的钩子,请求失败的钩子,配置对象
this.handlers.push({
fulfilled,
rejected,
synchronous: options ? options.synchronous : false,
runWhen: options ? options.runWhen : null
});
// 返回处理器索引
return this.handlers.length - 1;
}
// 通过索引移除指定的处理器
eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
}
// 清除所有处理器
clear() {
if (this.handlers) {
this.handlers = [];
}
}
# 循环处理所有handler【请求时使用】
forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
}
}
# 导出拦截器class
export default InterceptorManager;
拦截器InterceptorManager
的内容也比较简单,自身只有一个handlers
属性,作用是存储handler
处理对象。拦截器的所有方法都是围绕着handlers
数组进行操作。比如我们常用的use
方法:
# 注册请求拦截器
service.interceptors.request.use(
config => {},
error => {}
)
use
方法的作用:就是将我们传入的成功回调和失败回调包装到一个处理对象中【这样的一个对象可以称为一个处理器】,然后添加到handlers
数组中。
this.handlers.push({fulfilled, rejected})
axios
中只有两种拦截器:请求拦截器和响应拦截器。
但是在一个拦截器中,我们可以定义多个处理器:
# 给请求拦截器 定义两个处理器
// 【处理器只是根据源码方便称呼,也可以说注册多个请求拦截器】
service.interceptors.request.use(
config => {},
error => {}
)
service.interceptors.request.use(
config => {},
error => {}
)
这时service.interceptors.request.handlers
属性中就会存在两个处理器对象。
4,发起http请求
本节我们开始讲解axios.request()
方法。
request
查看request
方法源码:
# 发起请求
request(configOrUrl, config) {
if (typeof configOrUrl === 'string') {
config = config || {};
// 存储url
config.url = configOrUrl;
} else {
config = configOrUrl || {};
}
# 合并config,生成本次http请求的配置 【注意:传入的config可以覆盖默认的全局配置】
config = mergeConfig(this.defaults, config);
const {transitional, paramsSerializer, headers} = config;
if (transitional !== undefined) {
validator.assertOptions(transitional, {
silentJSONParsing: validators.transitional(validators.boolean),
forcedJSONParsing: validators.transitional(validators.boolean),
clarifyTimeoutError: validators.transitional(validators.boolean)
}, false);
}
// 参数处理
if (paramsSerializer != null) {
if (utils.isFunction(paramsSerializer)) {
config.paramsSerializer = {
serialize: paramsSerializer
}
} else {
validator.assertOptions(paramsSerializer, {
encode: validators.function,
serialize: validators.function
}, true);
}
}
# 设置本次请求方式【全部转小写】
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
let contextHeaders;
// Flatten headers
# 格式化请求头
contextHeaders = headers && utils.merge(
headers.common,
headers[config.method]
);
contextHeaders && utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
(method) => {
delete headers[method];
}
);
# 设置请求头
config.headers = AxiosHeaders.concat(contextHeaders, headers);
# 重点:处理拦截器
// 请求拦截器钩子执行链
const requestInterceptorChain = [];
// 是否同步执行:请求拦截器
let synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// runWhen默认null, synchronous默认为false
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
# 默认都是异步执行
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
# 将请求拦截器的所有钩子:添加到执行链列表
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 响应拦截器钩子执行链
const responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
# 将响应拦截器的所有钩子:添加到执行链列表
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
let promise;
let i = 0;
let len;
# 默认异步执行【走这个分支逻辑】
if (!synchronousRequestInterceptors) {
// 创建执行链
const chain = [dispatchRequest.bind(this), undefined];
chain.unshift.apply(chain, requestInterceptorChain);
chain.push.apply(chain, responseInterceptorChain);
len = chain.length;
promise = Promise.resolve(config);
# 循环调用钩子函数,顺序是请求钩子 => 发起真正的http请求 => 响应钩子
while (i < len) {
promise = promise.then(chain[i++], chain[i++]);
}
# 返回响应内容
return promise;
}
# 没有配置请求拦截器,就会走下面的逻辑【较少情况】
len = requestInterceptorChain.length;
let newConfig = config;
i = 0;
while (i < len) {
const onFulfilled = requestInterceptorChain[i++];
const onRejected = requestInterceptorChain[i++];
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected.call(this, error);
break;
}
}
try {
promise = dispatchRequest.call(this, newConfig);
} catch (error) {
return Promise.reject(error);
}
i = 0;
len = responseInterceptorChain.length;
while (i < len) {
promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]);
}
return promise;
}
总结: request
方法里面主要有以下几个重点逻辑:
- 合并
config
,生成本次http
请求的配置。 - 设置本次请求方式
config.method
。 - 设置请求头
config.headers
。 - 处理拦截器,循环执行任务与发起http请求。
- 返回响应结果。
根据这里的mergeConfig
,我们可以印证官网的描述:
【axios官网】配置的优先级:配置将会按优先级进行合并。它的顺序是:在
lib/defaults.js
中找到的库默认值,然后是实例的defaults
属性,最后是请求的config
参数。后面的优先级要高于前面的。
所以我们每一次axios
请求传入的配置参数都拥有【最高的优先级】,比如:
export function login(data) {
return axios({
url: '/api/admin/login',
method: 'post',
data,
# 会覆盖默认的,以及全局的配置
timeout: 10000,
contentType: ''
})
}
这就是aioxs
强大之处,既可以全局封装配置,又可以给独立的请求配置不同的请求参数。
下面我们再继续看拦截器的处理,在分析拦截器之前,我们再回顾一下拦截器的设置:
// 设置拦截器
service.interceptors.request.use(
config => {},
error => {}
)
service.interceptors.response.use(
config => {},
error => {}
)
拦截器的use
方法:
use(fulfilled, rejected, options) {
// 参数:请求成功的钩子,请求失败的钩子,配置对象
this.handlers.push({
fulfilled,
rejected,
synchronous: options ? options.synchronous : false,
runWhen: options ? options.runWhen : null
});
return this.handlers.length - 1;
}
无论是官网案例,还是我们在项目中使用时,定义拦截器都只传递了前面两个钩子函数,所以我们可以得出两个结论:
synchronous
默认都为false
。runWhen
默认都为null
。
我们可以打印查看:
// 是否同步执行请求拦截器
let synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
# runWhen默认null, synchronous默认为false
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
# 默认都是异步执行
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
# 将请求拦截器的所有钩子:添加到钩子列表里面
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
所以synchronousRequestInterceptors
变量会一直为false
,即默认为异步执行请求拦截器。
# 默认异步执行【走这个分支逻辑】
if (!synchronousRequestInterceptors) {
// 执行链
const chain = [dispatchRequest.bind(this), undefined];
// 将请求拦截器的钩子添加到执行链头部
chain.unshift.apply(chain, requestInterceptorChain);
// 将响应拦截器的钩子添加到执行链尾部
chain.push.apply(chain, responseInterceptorChain);
len = chain.length;
promise = Promise.resolve(config);
# 循环调用钩子函数,顺序是请求钩子 => 发起真正的http请求 => 响应钩子
while (i < len) {
# 将钩子添加到微任务,链式调用
promise = promise.then(chain[i++], chain[i++]);
}
# 返回响应内容
return promise;
}
所以request
方法最终会走这个分支进行拦截器执行与发起http
请求。
这里首先定义了一个执行链chain
,存入了一个dispatchRequest
钩子,然后将请求拦截器的所有钩子添加到执行链头部,将响应拦截器的所有钩子添加到执行链的尾部。
dispatchRequest
函数是真正发起http
的方法,后面我们会详细分析它的内容。
所以这里的执行链顺序是:
- 触发请求钩子。
- 触发http请求。
- 触发响应钩子。
我们来打印一下结果印证:
整理好执行链chain
之后:
promise = Promise.resolve(config)
使用Promise.resolve
创建了一个fulfilled
状态的promise
对象,并且是将处理好的config
配置对象作为resolve
参数。
然后开始循环,使用promise.then
依次将执行链chain
中的钩子添加微任务执行,这里的调用结果又赋值给了promise
形成了链式调用,一直到执行完成,返回最后的promise
结果。
这里的链式调用非常重要,我们转变一下它的形式进行更好的理解:
# 链式调用
promise.then(requestResolve(), requestReject())
.then(http(), undefined)
.then(responseResolve(), responseReject())
因为这里的promise
对象是fulfilled
状态,所以在执行then
方法是,默认就会执行requestResolve
,即请求拦截器的config
钩子。根据执行结果,总共可以分为三种场景:
requestResolve
执行成功,则会发起http
请求,请求成功则会进入responseResolve
,最终返回响应结果res
。requestResolve
执行成功,则会发起http
请求,请求失败则会进入responseReject
,返回失败原因error
。requestResolve
执行报错【比如语法错误,设置请求头错误】,则会进入responseReject
,返回失败原因error
。
这就是执行链chain
的完整执行过程,即axios
发起一个http
请求的过程。
拦截器的原理: 就是在axios
每次发起http
请求之前和请求之后触发一些用户定义的钩子函数。
拦截器的原理我们已经知道了,下面我们开始分析axios
中http
具体请求过程。
dispatchRequest
前面我们已经知道了dispatchRequest
函数是真正发起http
请求的方法,下面开始解析它的源码:
# 发起真正的http请求
export default function dispatchRequest(config) {
// 发起真正的请求之前:如果取消请求,则抛出CanceledError
throwIfCancellationRequested(config);
# 继续请求的情况下:
// 设置请求头
config.headers = AxiosHeaders.from(config.headers);
// Transform request data
# 处理请求数据
config.data = transformData.call(
config,
config.transformRequest
);
// 设置请求内容类型
if (['post', 'put', 'patch'].indexOf(config.method) !== -1) {
config.headers.setContentType('application/x-www-form-urlencoded', false);
}
# 获取适配器
// adapter是axios库中负责发送网络请求的适配器(即执行HTTP请求并返回响应数据的功能实现)
const adapter = adapters.getAdapter(config.adapter || defaults.adapter);
# 通过适配器发起真正的http请求
return adapter(config).then(
// 折叠
() => { ... },
() => { ... }
);
}
dispatchRequest
函数的内容可以分为三部分:
- 发起真正的请求之前,可以调用
throwIfCancellationRequested(config)
取消请求,抛出CanceledError。 - 处理请求数据和请求的内容类型
contentType
。 - 获取适配器
adapter
。 - 使用适配器
adapter
发起http
请求。
cancelToken
我们首先查看throwIfCancellationRequested
取消请求方法:
# 取消请求,抛出CanceledError
function throwIfCancellationRequested(config) {
# 取消请求有两种方式
// CancelToken 方式
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
// AbortController 方式
if (config.signal && config.signal.aborted) {
throw new CanceledError(null, config);
}
}
取消请求有两种方式,使用任何一种都可以。
CancelToken
AbortController
【新版本推荐使用】
const controller = new AbortController();
service({
method: 'get',
signal: controller.signal,
url: 'https://www.fastmock.site/mock/43324a7f3da68215e1834d217d35d1c7/api/userInfo',
timeout: 5000,
})
// 取消请求
controller.abort()
controller.abort()
会让signal.aborted
状态为true
,就可以在真正发起请求之前,抛出errer
,取消请求。
throw new CanceledError(null, config);
adapter
我们继续查看适配器adapter
【非常重要】:
- 浏览器环境:依赖于
XMLHttpRequest
,使用xhr
对象发起http请求。 - node环境:依赖于
nodejs
的原生http模块。
// 获取适配器
const adapter = adapters.getAdapter(config.adapter || defaults.adapter);
在项目我们一般不会配置adapter
参数,所以这里都会使用默认的配置defaults.adapter
。
// lib/defaults/index.js
const defaults = {
adapter: ['xhr', 'http'],
}
默认适配器有xhr
对象和http
对象两种,axios
会根据依赖环境选择其中一个。
继续查看adapters.getAdapter
方法:
# adapters对象
export default {
# 获取适配器
getAdapter: (adapters) => {
// 取出适配器列表 ['xhr', 'http']
adapters = utils.isArray(adapters) ? adapters : [adapters];
const {length} = adapters;
let nameOrAdapter;
let adapter;
for (let i = 0; i < length; i++) {
nameOrAdapter = adapters[i];
# 在这里判断时:
// 如果是浏览器环境:第一次循环即可取到xhr请求器,赋值给adapter,然后直接break跳出循环,
// 如果node环境:第一次循环xhr为null,所以需要第二次循环,取出http请求,赋值给adapter
if((adapter = utils.isString(nameOrAdapter) ? knownAdapters[nameOrAdapter.toLowerCase()] : nameOrAdapter)) {
break;
}
}
// 如果没有适配器则报出异常提示
if (!adapter) {
if (adapter === false) {
throw new AxiosError(
`Adapter ${nameOrAdapter} is not supported by the environment`,
'ERR_NOT_SUPPORT'
);
}
throw new Error(
utils.hasOwnProp(knownAdapters, nameOrAdapter) ?
`Adapter '${nameOrAdapter}' is not available in the build` :
`Unknown adapter '${nameOrAdapter}'`
);
}
if (!utils.isFunction(adapter)) {
throw new TypeError('adapter is not a function');
}
# 返回适配器
return adapter;
},
adapters: knownAdapters
}
getAdapter
方法最重要的作用就是循环传入的adapters
列表:
['xhr', 'http']
然后根据环境选择适配器对象adapter
,最后返回适配器。
这里在循环判断时:
- 如果是浏览器环境:第一次循环即可取到
xhr
对象,赋值给adapter
,然后直接break
跳出循环。 - 如果是
node
环境:第一次循环xhr
为null
,所以需要第二次循环,取出http请求,赋值给adapter
。
我们来打印一下印证:
knownAdapters
对象我们还没有提及,它其实存储的就是适配器:
import httpAdapter from './http.js';
import xhrAdapter from './xhr.js';
// 存储适配器
const knownAdapters = {
http: httpAdapter,
xhr: xhrAdapter
}
我们来分别看一下这两个适配器:
xhrAdapter
// lib/adapters/xhr.js
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
export default isXHRAdapterSupported && function (config) {}
在浏览器环境下存在XMLHttpRequest
构造函数,所以能到成功导出xhr
适配器。
httpAdapter
// lib/adapters/http.js
const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process';
export default isHttpAdapterSupported && function httpAdapter(config) {}
在浏览器环境不存在node的process
对象,所以不能成功导出http
适配器,为null
。
回到dispatchRequest
方法:获取到adapter
适配器后,即立刻使用适配器【即xhr
】发起http请求,所以我们要了解具体的发起过程,就要理解xhr
适配器的原理:
xhr
查看xhr
适配器源码:
# xhr适配器
export default function (config) {
// 返回一个Promise对象,传入一个executor执行器函数
return new Promise(function dispatchXhrRequest(resolve, reject) {
let requestData = config.data;
const requestHeaders = AxiosHeaders.from(config.headers).normalize();
const responseType = config.responseType;
let onCanceled;
function done() {}
if (utils.isFormData(requestData) && (platform.isStandardBrowserEnv || platform.isStandardBrowserWebWorkerEnv)) {
requestHeaders.setContentType(false); // Let the browser set it
}
# 创建xhr请求对象
let request = new XMLHttpRequest();
// HTTP basic authentication
# 权限配置
if (config.auth) {
const username = config.auth.username || '';
const password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
requestHeaders.set('Authorization', 'Basic ' + btoa(username + ':' + password));
}
const fullPath = buildFullPath(config.baseURL, config.url);
// 请求配置
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// Set the request timeout in MS
# 本次http请求的超时设置
request.timeout = config.timeout;
function onloadend() {}
# 配置http请求完成的钩子函数
if ('onloadend' in request) {
request.onloadend = onloadend;
} else {
request.onreadystatechange = function handleLoad() {};
}
# 请求取消钩子函数
request.onabort = function handleAbort() {};
# 请求错误处理
request.onerror = function handleError() {};
# 请求超时处理
request.ontimeout = function handleTimeout() {};
// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (platform.isStandardBrowserEnv) {
// Add xsrf header
const xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath))
&& config.xsrfCookieName && cookies.read(config.xsrfCookieName);
if (xsrfValue) {
requestHeaders.set(config.xsrfHeaderName, xsrfValue);
}
}
// Remove Content-Type if data is undefined
requestData === undefined && requestHeaders.setContentType(null);
# 设置http请求头
if ('setRequestHeader' in request) {
utils.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) {
request.setRequestHeader(key, val);
});
}
# 设置withCredentials凭证
if (!utils.isUndefined(config.withCredentials)) {
request.withCredentials = !!config.withCredentials;
}
# 设置响应类型
if (responseType && responseType !== 'json') {
request.responseType = config.responseType;
}
// Handle progress if needed
# 设置下载进度监听
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', progressEventReducer(config.onDownloadProgress, true));
}
// Not all browsers support upload events
# 设置上传进度监听
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress));
}
# 如果配置取消cancel,则取消本次请求
if (config.cancelToken || config.signal) {
// 取消请求:其实就是在abort内部调用reject方法
request.abort();
}
const protocol = parseProtocol(fullPath);
if (protocol && platform.protocols.indexOf(protocol) === -1) {
reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config));
return;
}
# 发起http请求
request.send(requestData || null);
});
}
xhr
适配器的源码虽然看着有点多,但是并不复杂,我们可以分为以下几个部分:
- 使用原生
XMLHttpRequest
构造函数创建request
请求对象。 - 使用
request.open
配置本次http
请求方式,请求地址【默认为异步请求】。 - 使用
request.setRequestHeader
设置本次http的请求头。 - 注册
request
请求成功,请求失败,请求超时,请求取消,请求进度等钩子函数。 - 在发起本次请求之前,如果配置了
cancelToken/signal
,可以取消请求。 - 使用
request.send
发起http请求。
到这里我们也可以进行一个总结:axios
插件底层原理就是基于原生的XMLHttpRequest
构造函数。
// 创建请求对象
const xhr = new XMLHttpRequest()
// 请求配置【默认为异步请求】
xhr.open(method, url, true)
// 发送请求
xhr.send()
扩展:
XMLHttpRequest
有两种执行模式:同步(synchronous)和异步(asynchronous)。如果在open
方法中将第三个参数async
设置为false
,那么请求就会以同步的方式进行。换句话说,JavaScript
执行在send()
处暂停,并在收到响应后恢复执行。这有点儿像alert
或prompt
命令,这会阻塞程序的运行。所以在默认情况下,xhr
都会为异步请求。
我们再回到dispatchRequest
方法的最后:
export default function dispatchRequest(config) {
return adapter(config).then(function onAdapterResolution(response) {
# 请求成功的情况下
// 处理响应数据
response.data = transformData.call(
config,
config.transformResponse,
response
);
// 处理响应头
response.headers = AxiosHeaders.from(response.headers);
// 返回响应式内容
return response;
}, function onAdapterRejection(reason) {
# 请求失败的情况下:返回错误信息
return Promise.reject(reason);
});
}
这里的adapter
就是我们的xhr
适配器,当我们调用adapter(config)
时,就会使用xhr
适配器发起http请求:
- 请求成功:则会执行
onAdapterResolution
,返回最终的响应内容。 - 请求失败:则会执行
onAdapterRejection
,返回错误信息。
以上就是使用axios
来发起一个http
请求的完整流程。
下面我们可以根据几个常见的情况来进行验证:
(一)请求超时
request.ontimeout = function handleTimeout() {
# 调用rejtct方法,返回错误信息对象
reject(new AxiosError(
timeoutErrorMessage,
transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
config,
request));
// 清除request对象
request = null;
}
请求超时:会调用rejtct
方法,返回错误信息对象,即会执行onAdapterRejection
方法,返回最终的错误信息。
(二)请求错误
// 请求错误处理
request.onerror = function handleError() {
reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));
// Clean up request
request = null;
};
请求错误:同样会调用rejtct
方法,返回错误信息对象,即会执行onAdapterRejection
方法,返回最终的错误信息。
(三)取消请求
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
// Clean up request
request = null;
};
取消请求:调用rejtct
方法,返回错误信息对象,即会执行onAdapterRejection
方法,返回最终的错误信息。
(四)请求成功
# 请求完成
function onloadend() {
if (!request) {
return;
}
// Prepare the response
const responseHeaders = AxiosHeaders.from(
'getAllResponseHeaders' in request && request.getAllResponseHeaders()
);
const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
request.responseText : request.response;
# 组装响应数据
const response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
};
settle(function _resolve(value) {
# 请求成功,调用resolve,返回响应数据
resolve(value);
// 取消一些订阅
done();
}, function _reject(err) {
// 出现异常,调用reject
reject(err);
done();
}, response);
// Clean up request
request = null;
}
请求成功:调用resolve
方法,返回响应数据,即会执行onAdapterResolution
方法,返回最终的响应内容。
最后再看一下http
请求进度的监听:
- 下载进度:
// 下载进度监听
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', progressEventReducer(config.onDownloadProgress, true));
}
我们只需要在本次请求的config
中配置onDownloadProgress
函数即可:
export function myDownload(data, onDownloadProgress) {
return axios({
url: '/api/download',
method: 'post',
data,
onDownloadProgress,
})
}
// 调用
myDownload(data, (e) => {})
- 上传进度:
// 上传进度监听
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress));
}
我们只需要在本次请求的config
中配置onUploadProgress
函数即可:
export function myUpload(data, onUploadProgress) {
return axios({
url: '/api/download',
method: 'post',
data,
onUploadProgress,
})
}
// 调用
myUpload(data, (e) => {})
5,完整请求流程
在axios
源码有了基本的了解后,我们使用一个简单的案例来过一遍完整的请求流程。
import axios from "axios";
// 创建请求实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 10000
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 每个请求接口的公共参数
config.headers.Authorization = "myToken";
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(res) => {
console.log(res)
return res;
},
(error) => {
// 可以提示请求出错
console.log('请求失败', error)
return Promise.reject(error);
}
);
// 发起请求
service({
method: 'get',
url: 'https://www.fastmock.site/mock/324a7f3da68215e1834d217d35d1c7/api/userInfo',
timeout: 5000,
onDownloadProgress(e) {
console.log(e)
}
})
创建请求实例
进入断点,首先调用axios.create
创建请求实例service
:
返回的是createInstance
方法,首先调用了mergeConfig
函数来合并生成全局config
:
这里的context
就是创建完成的axios
实例,在创建完成后,立即执行一个bind
方法,将请求实例包装成了一个函数实例。
设置拦截器
下面开始设置axios
拦截器:
这里我们没有传递第三个参数options
,所以后面两个为false
和null
。
响应拦截器的添加同理,就不重复贴图了。
发起请求
进入request
方法,开始发起请求:
当然:实际情况是将执行链钩子添加到了微任务队列,等待异步执行,我们这里为了方便阅读直接进入执行阶段。
触发请求拦截器
然后执行dispatchRequest
,发起http
请求: