Axiso源码目录及核心功能的梳理
前言:此文是个人对axios源码的一个整理,没有细入到每个api功能,主要是把文件目录结构及对应的功能,和几个核心功能的原理解析了一遍。目的是领入源码的入门,以及了解核心流程
看之前需要?
需要充分了解promise相关用法,比如.then()接收2个参数都是函数,分别作用是什么?这2个函数的返回值对后面的链式调用有什么影响?如果函数返回值也是个promise 后面会怎么执行?
需要充分了解axios的基本使用和配置: https://www.kancloud.cn/yunye/axios/234845
比如:
- axios既可以像函数一样执行,又类似对象一样,可以访问属性
// axios作为函数执行
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});
// axios类似对象一样访问属性 去执行
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
- 可以使用自定义配置新建一个 axios 实例
var instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
// 了解配置项
{
// `url` 是用于请求的服务器 URL
url: '/user',
// `method` 是创建请求时使用的方法
method: 'get', // 默认是 get
// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
baseURL: 'https://some-domain.com/api/',
// `transformRequest` 允许在向服务器发送前,修改请求数据
// 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
// 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
transformRequest: [function (data) {
// 对 data 进行任意转换处理
return data;
}],
...
...
...
...
...
}
- 拦截器
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
开始:先解析目录结构
- 打开package.json找到main字段,就是源代码dev模式的入口(打包后的文件另算)
"main": "index.js",
- 我们主要关注lib目录,axios核心功能代码都在这里面(源码的目录结构和功能拆分的非常清晰,非常有美感,很值得学习)
./lib
├── adapters 适配器(当前环境是 浏览器则用xhr发请求,node环境则用http模块发请求)
│ ├── README.md
│ ├── http.js
│ └── xhr.js
├── axios.js 主入口(聚合所有的功能)
├── cancel 取消请求 相关的
│ ├── Cancel.js
│ ├── CancelToken.js
│ └── isCancel.js
├── core
│ ├── Axios.js 初始化axios实例(主要是请求相关的功能,单独拆出来的,利于维护。属于axios.js的子集)
│ ├── InterceptorManager.js 拦截器管理
│ ├── README.md
│ ├── buildFullPath.js 合并成完整的path,比如baseURL: 'https://some-domain.com/api/', 请求path是 someModule/getList。 结果合成https://some-domain.com/api/someModule/getList
│ ├── createError.js 封装报错的功能
│ ├── dispatchRequest.js 管理适配器adapters,类似小老板 代理管 适配器工人(适配器工人有3种,用户自定义,浏览器的xhr,node的http模块)
│ ├── enhanceError.js 增强错误提示,更具体
│ ├── mergeConfig.js 合并配置,axios有默认配置,用户可以axios.create自定义实例,用户自定义的配置,和 axios的默认配置的合并
│ ├── settle.js promise处理的一个代理。根据响应状态解析或拒绝 Promise。
│ └── transformData.js 负责执行转换数据的函数
├── defaults.js 默认配置
├── env 当前axios版本
│ ├── README.md
│ └── data.js 当前axios版本
├── helpers 工具函数,几乎见名知意
│ ├── README.md
│ ├── bind.js 和现在的bind一样,此处是兼容老浏览器
│ ├── buildURL.js 往url后面添加参数,比如 xxx?id=1&page=2
│ ├── combineURLs.js 通过组合指定的 URL 创建一个新的 URL
│ ├── cookies.js 操作cookies
│ ├── deprecatedMethod.js 废弃的写法报错提示
│ ├── isAbsoluteURL.js 判断url是否是绝对路径
│ ├── isAxiosError.js 判断是否是axios内部错误
│ ├── isURLSameOrigin.js 判断url是否同源
│ ├── normalizeHeaderName.js 标准化请求头字段名(兼容用户的不规范写法)
│ ├── parseHeaders.js 解析http头 成js对象
│ ├── spread.js 类似apply的功能(兼容老浏览器)
│ └── validator.js 校验(如一些配置项等等)
└── utils.js
拆解一下主要几个流程
主要讲解:
- 创建实例的流程?
- 执行请求的流程?
- 适配器是什么?
- 多个拦截器的执行顺序是什么?
- 如何转换请求数据?
- 如何取消请求?
- 最终返回响应结果
1. 创建实例的流程?
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
Axios.prototype.request = function request(config) {...}
// Axios.prototype.xx 省略很多方法
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context); // 注意实例是一个函数
// 此处的.extend类似 Object.assign(), 把Axios.prototype和context的属性 方法,copy的实例instance身上去。 此时实例拥有类似对象的属性和方法(之前只是一个函数)
utils.extend(instance, Axios.prototype, context);
// 让实例instance的this指向context
utils.extend(instance, context);
// 用户可以自定义实例,配置项通过mergeConfig(defaultConfig, instanceConfig) 合并
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
return instance;
}
var axios = createInstance(defaults);
总结:
- 实例instance最开始是一个函数,后面通过类似 Object.assign(),把Axios.prototype和context的属性 方法,copy的实例instance身上去。 此时实例拥有类似对象的属性和方法。可以完成
axios({method: 'get'}) 也可以 axios.get('')
2种灵活调用 - 可以用默认axios配置,也可以自定义,自定义用axios.create,可以自定义一些配置。配置项通过mergeConfig(defaultConfig, instanceConfig) 合并
var instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
2. 执行请求的流程?
流程: Axios.prototype.request -> 请求拦截器 -> dispatchRequest(处理请求参数,调用适配器,小老板 代理) -> adapter适配器 发起请求 -> 报错/取消请求 -> 响应拦截器 -> 返回结果
从axios官网上copy下来 特性展示:
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
- 首先理解适配器,针对特性1,2
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
axios默认会根据当前环境,是否存在 XMLHttpRequest,确定是浏览器环境还是node环境,浏览器环境用XHR对象发请求,node环境用http模块发请求
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}
也可以支持用户自定义请求处理,在自定义实例的配置项里设置adapter(此功能可以写mock,axios-mock-adapter库就是基于这个公共)
var adapter = config.adapter || defaults.adapter;
- 多个拦截器的执行顺序?
多个拦截器的话,是先 外到里,在里到外。
比如按顺序写的,
请求拦截器1
请求拦截器2
响应拦截器1
响应拦截器2
执行顺序是 请求拦截器2 - 请求拦截器1 - 发起请求/等待结果 - 响应拦截器1 - 响应拦截器2
// 因为请求拦截器是unshift插入数组的,响应拦截器是按顺序push进数组的
if (!synchronousRequestInterceptors) {
var chain = [dispatchRequest, undefined];
Array.prototype.unshift.apply(chain, requestInterceptorChain); // 请求拦截器是unshift插入数组的
chain = chain.concat(responseInterceptorChain); // 响应拦截器是按顺序push进数组的
promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
- 如何转换请求数据?
在 dispatchRequest 阶段执行的
- dispatchRequest阶段:处理请求参数,调用适配器,类似小老板 代理
以下就是有一些默认配置,比如
- 数据是 ArrayBuffer,Blob,File,stream,Formdata等就直接return
- 数据是json格式则JSON.stringify转成字符串
- 如果content-type是application/x-www-form-urlencoded,则return data.toString() 等等 可以自行看代码
var defaults = {
...
transformRequest: [function transformRequest(data, headers) {
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
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;
}
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {
setContentTypeIfUnset(headers, 'application/json');
return stringifySafely(data);
}
return data;
}],
transformResponse: [function transformResponse(data) {
var transitional = this.transitional || defaults.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) {
if (strictJSONParsing) {
if (e.name === 'SyntaxError') {
throw enhanceError(e, this, 'E_JSON_PARSE');
}
throw e;
}
}
}
return data;
}],
...
};
3. 取消请求
首先看一个使用案例:
- 配置 cancelToken 对象
- 缓存用于取消请求的 cancel 函数,在后面特定时机调用 cancel 函数取消请求
- 在错误回调中判断如果 error 是 cancel, 做相应处理
- 实现功能 点击按钮, 取消某个正在请求中的请求(主动触发的)
<script>
//获取按钮
const btns = document.querySelectorAll('button');
//2.声明全局变量
let cancel = null;
//发送请求
btns[0].onclick = function () {
//检测上一次的请求是否已经完成
if (cancel !== null) {
//取消上一次的请求
cancel();
}
axios({
method: 'GET',
url: 'http://localhost:3000/posts',
//1. 添加配置对象的属性
cancelToken: new axios.CancelToken(function (c) {
//3. 将 c 的值赋值给 cancel
cancel = c;
})
}).then(response => {
console.log(response);
//将 cancel 的值初始化
cancel = null;
})
}
//绑定第二个事件取消请求
btns[1].onclick = function () {cancel(); }
</script>
源码细节在cancel文件内 和 xhr.js内
- 主要的方法是:XMLHttpRequest对象的 abort 方法,才能取消请求 (node内的http模块也复用了abort这个名字)
if (config.cancelToken || config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = function(cancel) {
if (!request) {
return;
}
reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
request.abort();
request = null;
};
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
最终返回响应结果
监听xhr的onreadystatechange方法,当readyState === 4 时,才正常得到响应结果
最终会由settle,对结果进行一下校验,在resolve或reject对外返回响应结果
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// 请求出错,没有得到响应,会由 onerror 处理,通过promise的 reject 抛出去
// 有一个例外:请求使用 file: 协议,大多数浏览器 即使请求成功,也会返回状态为 0
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
// readystate handler is calling before onerror or ontimeout handlers,
// so we should call onloadend on the next 'tick'
setTimeout(onloadend);
}
function onloadend() {
if (!request) {
return;
}
// Prepare the response
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
var responseData = !responseType || responseType === 'text' || responseType === 'json' ?
request.responseText : request.response;
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
};
settle(function _resolve(value) {
resolve(value);
done();
}, function _reject(err) {
reject(err);
done();
}, response);
// Clean up request
request = null;
}
// 最终会由settle,对结果进行一下校验,在resolve或reject,对外返回
function settle(resolve, reject, response) {
var validateStatus = response.config.validateStatus;
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
reject(createError(
'Request failed with status code ' + response.status,
response.config,
null,
response.request,
response
));
}
};
感受: 源码的目录结构和功能拆分的非常清晰,非常有美感,很值得学习!
码字不易,点赞鼓励!