学习目的
- 了解axios的实现原理、学习温故js基础知识
- 了解axios平时不常用的配置与功能
- 有利于解决前后端交互遇到的问题
- 提高阅读源码的能力,沉淀一些好用的方法
- 扩展自己想要的功能-如果自己要写一个包或开源库,有利于提供思路
源码目录分析
下载地址:git clone https://github.com/axios/axios.git
└─ examples // demo
└─ lib
└─ adapters
├─ http.js // node环境下利用 http 模块发起请求
├─ xhr.js // 浏览器环境下利用 xhr 发起请求
└─ cancel
├─ Cancel.js // Cancel构造函数
├─ CancelToken.js // 取消请求构造函数
├─ isCancel.js // 是否取消boolean
└─ core
├─ Axios.js // 生成 Axios 实例
├─ InterceptorManager.js // 拦截器
├─ dispatchRequest.js // 调用适配器发起请求
├─ mergeConfig.js // 合并配置
├─ transformData // 数据转化
...
└─ helpers
├─ bind.js // bind函数
├─ buildURL.js // 根据类型转换为最终的url
├─ spread.js // sprend扩展
├─ validator.js // 校验函数
├─ cookies.js // cookie的读写方法
├─ ...
├─ axios.js // 入口文件
├─ defaults.js // axios 默认配置项
├─ utils.js // 工具函数
└─ sandbox // 沙箱测试
client.html // 前端页面
server.js // 后端服务
└─ webpack.config.js // webpack配置
基本功能分析
入口文件
查看webpack.config.js , 入口文件为index.js
var config = {
entry: './index.js',
output: {
path: __dirname + '/dist/',
filename: name + '.js',
sourceMapFilename: name + '.map',
library: 'axios',
libraryTarget: 'umd',
globalObject: 'this'
}
查看根目录下的index.js
module.exports = require('./lib/axios');
顺藤摸瓜,查看./lib/axios.js 文件
我们来分析下真正的入口文件./lib/axios.js
导入依赖模块
'use strict';
var utils = require('./utils'); // 工具函数
var bind = require('./helpers/bind'); // bind方法
var Axios = require('./core/Axios'); // 核心Axio方法
var mergeConfig = require('./core/mergeConfig'); // 配置参数的处理
var defaults = require('./defaults'); // 默认配置参数
创建实例
/**
* Create an instance of Axios
*
* @param {Object} defaultConfig The default config for the instance
* @return {Axios} A new instance of Axios
*/
function createInstance(defaultConfig) {
// context实例自身拥有2个属性:defaults、interceptors
// 原型对象上有一些方法:request、getUri、delete、get、post、head、options、put、patch
var context = new Axios(defaultConfig);
// bind返回一个新函数,这个新函数内部会调用这个request函数,改变Axios.prototype.request中的this指向
var instance = bind(Axios.prototype.request, context);
// Axios原型上的方法拷贝到instance上:request()/get()/post()/put()/delete()
utils.extend(instance, Axios.prototype, context);
// 将Axios实例对象(context)上的属性拷贝到instance上:defaults和interceptors属性
utils.extend(instance, context);
// 工厂模式,用于创建新的实例
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
return instance;
}
// 创建实例
var axios = createInstance(defaults);
// 往外暴漏
axios.Axios = Axios;
在实例上挂着其他方法和属性
// 暴漏取消请求相关实例
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
axios.VERSION = require('./env/data').version;
// 暴漏all方法
axios.all = function all(promises) {
return Promise.all(promises);
};
// 暴漏spread方法
axios.spread = require('./helpers/spread');
// 暴漏isAxiosError
axios.isAxiosError = require('./helpers/isAxiosError');
// 导出
module.exports = axios;
// TS方式下导出
module.exports.default = axios;
axios、axios.Axios与axios.instance三种方式创建实例的区别是什么?
- 配置可能会有所不同
- axios 全部采用默认参数的形式创建实例
- axios.Axios 全部采用开发者传入的形式创建实例
- axios.instance 采用开发者传入与默认合并的形式创建实例
- 挂载的属性会有不同
- axios 上挂载了更多的属性 (Cancel、 CancelToken、 isCancel、 all、 spread )
axios.spread和axios.all配合使用 axios.spread作为回调函数使用
// 源码
/*
* @param {Function} callback
* @returns {Function}
*/
module.exports = function spread(callback) {
return function wrap(arr) {
return callback.apply(null, arr);
};
};
// 使用
axios.all([
axios.get('https://api.github.com/users/mzabriskie'),
axios.get('https://api.github.com/users/mzabriskie/orgs')
]).then(axios.spread(function (user, orgs) {})
参数配置处理
默认参数
默认参数在/lib/defaults.js
var defaults = {
// 对transitional 属性进行版本校验,会提示自某个版本之后移除该属性
// transitional包含了一些过度属性配置项,这些项对开发者不可见,主要是为了兼容之前的版本,经过配置可在较新版本中删除向后兼容的某些性质
transitional: {},
adapter: getDefaultAdapter(),
transformRequest: [function transformRequest(data, headers) {}],
transformResponse: [function transformResponse(data) {}],
// `params` 是即将与请求一起发送的 URL 参数
// 必须是一个无格式对象(plain object)或 URLSearchParams 对象
params: {
ID: 12345
},
// `paramsSerializer` 是一个负责 `params` 序列化的函数
paramsSerializer: function(params) {
return Qs.stringify(params, {arrayFormat: 'brackets'})
},
timeout: 0,
// xsrfCookieName 是用作 xsrf token 值的 cookie 名称
xsrfCookieName: 'XSRF-TOKEN',
// xsrfHeaderName 是 xsrf token 值的 http 头名称
xsrfHeaderName: 'X-XSRF-TOKEN',
// maxContentLength 限制http响应的最大字节数
maxContentLength: -1,
// maxContentLength 限制请求体的最大字节数
maxBodyLength: -1,
// 提前拿到服务器相应过来的状态信息。如果该函数返会True或者null或者undefined, 则axios返回的promise状态则会被更改为resolved。 如果该函数返会false, 则该promise状态变为rejected
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
},
// headers 是即将被发送的自定义请求头
headers: {
common: {
'Accept': 'application/json, text/plain, */*'
}
}
}
adapter适配器,主要区分是浏览器环境还是node环境
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;
}
transformRequest, 允许在向服务器发送前,修改请求数据, 只能用在 ‘PUT’, ‘POST’ 和 ‘PATCH’ 这几个请求方法, 后面数组中的函数必须返回一个String,或 ArrayBuffer,或 Stream
transformResponse, 在传递给 then/catch 前,允许修改响应数据
xsrfCookieName 和xsrfHeaderName是用来预防xsrf(跨站请求伪造)攻击
小插曲:登录认证一般有session认证,JWT(token)认证, session认证有一些缺点,最好的认证方式是token,token在前后端通信中一般有三种方式
1. get放在查询字符串中,URL中拼接(post可以放body中) 2. 放到Authorization/或者自定义headers中 3. 放到cookie中
xsrfCookieName && xsrfHeaderName主要就是对放到cookie中的token进行处理的键值
if (utils.isStandardBrowserEnv()) {
// Add xsrf header
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
没有默认处理的配置
- withCredentials
- auth
- responseEncoding
- onUploadProgress
- onDownloadProgress
- maxRedirects
- socketPath //
socketPath
定义了一个在 node.js 中使用的 UNIX Socket。 例如 ‘/var/run/docker.sock’ 向 docker 守护进程发送请求。只能指定socketPath
或proxy
。如果两者都指定,则使用socketPath
。 - httpAgent // node.js 中用于自定义代理
- httpsAgent
- proxy
- cancelToken
- signal: new AbortController().signal, // an alternative way to cancel Axios requests using AbortController
除此之外还有一个配置项auth (密码式授权)
// HTTP basic authentication
if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
onUplodProgress, 方便文件上传时,进度的获取
var config = {
onUploadProgress: function(progressEvent) {
var percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total );
console.log('percentCompleted', percentCompleted)
}
};
axios.put('/upload/server', data, config)
.then(function (res) {
output.className = 'container';
output.innerHTML = res.data;
})
.catch(function (err) {
output.className = 'container text-danger';
output.innerHTML = err.message;
});
调取适配器发起请求
function dispatchRequest(config) {
// ....config参数的处理
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
// 成功
},function onAdapterRejection(reason) {
// 失败
})
}
浏览器环境的 xhr请求处理
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// ...定义一些变量
function done() {
// 取消一些监听
}
var request = new XMLHttpRequest();
// HTTP basic authentication
// url的处理
// Set the request timeout
// 定义loadend函数
function onloadend() {
// 解析响应
// 根据校验状态去处理promise
settle(function _resolve(value) {
resolve(value);
done();
}, function _reject(err) {
reject(err);
done();
}, response);
}
if ('onloadend' in request) {
// Use onloadend if available
request.onloadend = onloadend;
} else {
// 使用 onreadystatechange 模拟 onload 事件
}
request.onabort = function () {}
request.onerror = function () {}
request.ontimeout = function () {}
// Add xsrf header
// Add headers to the request
// Add withCredentials to request if needed
// Add responseType to request if needed
// Handle progress if needed 下载
// Not all browsers support upload events
// 。。。
// 取消请求对处理
if (config.cancelToken || config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = function(cancel) {
...
}
}
if (!requestData) {
requestData = null;
}
// Send the request
request.send(requestData);
})
node环境下的适配器处理(lib/adapters/http.js),可以自行去学习了解
第二节:重要功能的实现
cancelToken实现
执行CancelToken
函数,
1.创建一个promise对象,把promise对象赋值给promise属性,把resolve执行权暴漏出来
2.执行executor方法,把cancel作为executor的参数,在内部调用resolve,来取消请求
3.定义promise的.then方法和then的回调函数,.then方法中执行了订阅和取消订阅的监听方法,回调函数中执行取消方法的具体动作
实例上挂载source方法,方便调用,返回两个方法,token是创建实例,cancel是触发取消请求的方法
function CancelToken(executor) {
// 取promise的resolve方法的控制权给resolvePromise
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
this.promise.then(cancel) // token.reason 传递过来的cancel实例
this.promise.then = function (onfulfilled) {}
......
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
......
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
module.exports = CancelToken;
拦截器与request处理
Axios原型对象,实例上定义了一个默认配置defaults,两个拦截器
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
原型方法request
Axios.prototype.request = function request(configOrUrl, config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
console.log('request', configOrUrl, config)
// 1. 配置项处理
if (typeof configOrUrl === 'string') {
config = config || {};
config.url = configOrUrl;
} else {
config = configOrUrl || {};
}
config = mergeConfig(this.defaults, config);
// 2. 请求方式处理
// Set config.method
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
// 拦截器处理
// filter out skipped interceptors
// 请求拦截器储存数组
var requestInterceptorChain = [];
// 默认所有请求拦截器的变量
var synchronousRequestInterceptors = true;
// 遍历注册好的请求拦截器数组
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 这里interceptor是注册的每一个拦截器对象,axios请求拦截器向外暴露了runWhen配置
// 来针对一些需要运行时检测来执行的拦截器
// 如果配置了该函数,并且返回结果为true,则记录到拦截器链中,反之则直接结束该层循环
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
// interceptor.synchronous 是对外提供的配置,可标识该拦截器是异步还是同步 默认为false(异步)
// 这里是来同步整个执行链的执行方式的,如果有一个请求拦截器为异步,
// 那么下面的promise执行链则会有不同的执行方式
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
// 塞到请求拦截器数组中
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 响应拦截器存储数组
var responseInterceptorChain = [];
// 遍历按序push到拦截器存储数组中
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
var promise;
// ***** 如果为异步 其实也是默认情况 ******//
if (!synchronousRequestInterceptors) {
// 创建存储链式调用的数组 首位是核心调用方法dispatchRequest,第二位是空
var chain = [dispatchRequest, undefined];
// 请求拦截器塞到前面
Array.prototype.unshift.apply(chain, requestInterceptorChain);
// 响应拦截器塞到后面
chain = chain.concat(responseInterceptorChain);
// 传参数config给dispatchRequest方法
promise = Promise.resolve(config);
// 循环 每次取两个出来组成promise链.then执行[]
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
// ***** 这里则是同步的逻辑 ******//
// 请求拦截器一个一个的走 返回 请求前最新的config
var newConfig = config;
// 循环 每次取两个出来组成promise链.then执行
while (requestInterceptorChain.length) {
var onFulfilled = requestInterceptorChain.shift();
var onRejected = requestInterceptorChain.shift();
// 做异常捕获 有错直接抛出
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected(error);
break;
}
}
// 请求
try {
promise = dispatchRequest(newConfig)
} catch (error) {
return Promise.reject(error);
}
// 响应拦截器执行
while (responseInterceptorChain.length) {
promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
}
return promise;
};
从源码中可以看出来,请求拦截器可以设置多个,并且请求拦截器是unshift方式存入数组,后定义的请求拦截器会先执行,因此同一个处理先定义的拦截器会覆盖后定义的拦截器
基于request 实现 其他方法
// Provide aliases for supported request methods
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: method,
url: url,
data: (config || {}).data
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, data, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: data
}));
};
});
在源码中自己实现一个节流逻辑
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
// 添加一个对象,来存储请求参数
this.throttleParamsConfig = {}
}
// 节流的实现
function throttle(handle, wait) {
let lastTime = new Date().getTime();
return function (configOrUrl, config) {
let nowTime = new Date().getTime();
// 超过wait时间,清空缓存参数
if (nowTime - lastTime > wait) {
this.throttleParamsConfig = {}
}
// 处理当前请求的参数
config = handleConfig(configOrUrl, config, this.defaults)
// 不同的请求
const nextFlag = this.saveRequestConfig(config)
if(nextFlag) {
lastTime = nowTime
const promise = handle.apply(this, arguments);
return promise;
}
}
}
// 存储请求配置
Axios.prototype.saveRequestConfig = function saveRequestConfig (config) {
// 节流处理
const prevConfigArray = this.throttleParamsConfig[config.url]
if (prevConfigArray) {
console.log(2)
if (prevConfigArray.includes(JSON.stringify(config))) {
console.log(prevConfigArray)
return false;
} else {
console.log(3, this.throttleParamsConfig[config.url])
this.throttleParamsConfig[config.url] = prevConfigArray.concat([JSON.stringify(config)])
}
} else {
console.log(1)
this.throttleParamsConfig[config.url] = [JSON.stringify(config)]
}
return true;
}
// throttle包裹request,时间可以自定义,暴漏出去
Axios.prototype.request = throttle(function (configOrUrl, config) {
// 1. 配置项处理
config = handleConfig(configOrUrl, config, this.defaults)
// 改变this指针
bind(Axios.prototype.saveRequestConfig, this);
// 以下为原有逻辑处理
....
}, 1000)
存储方式
思考->其他扩展
- jsonp
- 钩子函数(处理数据)
- 队列的处理 (await的实现)
- 防抖
- …
总结
重点概念:
axios内部核心代码主要由适配器,取消请求类,request 方法实现,拦截器处理。
一些配置:
url处理,数据转换,上传下载的进度,auth授权,防xsrf攻击、代理等配置的支持
一些方法:
判断类型和环境 如:isPlainObject isStream isStandardBrowserEnv…
绑定类的 如:bind、extend
设计方法:
发布订阅者模式(取消请求类)
自定义实现:
节流实现、可尝试其他功能实现