axios核心重写
框架与库皆是工具,原生为王
# 前言:
通读本文,学会以逆向思维拆解现在市面上比较流向的js库的封装方式
想要彻底弄懂ajax请求,彻底弄懂Promise ,重写axios 库无疑是最完美的选择
要想进行axios的核心部分重写,需要对ajax前后端数据联调,Promise 前端微任务,都有一定的了解才可以进行。
axios核心库能够支持服务端请求与前端请求,本文只针对axios前端常见方法进行重写。
# axios 是什么 怎么用
axios 是基于Promise 封装了HTTTP 请求的 库。
axios 的使用方法比较简单,能够代替原生js 进行前后端数据请求交互。
# axios 引入后使用方式
- 方式一:通过axios 函数传入请求信息对象
axios({
method:"get",
url:"url地址",
params:{key:value}
}).then()
axios({
method:"post",
url:"url地址",
data:{key:value}
}).then()
2
3
4
5
6
7
8
9
10
- 方式二:通过axios 下方法别名调用
axios.get('url地址?key=value').then()
axios.post('url地址',{key:value}).then()
2
- 方式三:通过内置方法实例化后调用
const instance = axios.create({
baseURL: '服务器全局默认地址',
timeout: 1000,
headers: {请求头对象}
});
instance.get('请求地址',{params:{key:value}}).then()
instance.post('请求地址',{key,value}).then()
2
3
4
5
6
7
# axios 返回值
axios 的返回值是一个对象,其中 data属性才是后端真正的返回数据
{
config: {...},
data: {code: 0, msg: '欢迎你,李四同学,get请求成功啦~你离前端大神更近一步啦~'},
headers: {content-length: '97', content-type: 'application/json; charset=utf-8'},
request:...},
status: 200,
statusText: "OK"
}
2
3
4
5
6
7
8
以上就是关于axios 基本请求的使用方式了,特殊的方法放到后面再进行介绍,下面我们开始核心库的重写~
# 源码分析
axios 即是一个函数,又是一个对象。
两个大层次划分:
第一层: 生成axios 特殊的函数/对象,关联这个函数和Axios类的属性及方法,并暴露这个函数
第二层:定义Axios 类,定义类的属性(全局配置,拦截器..),核心方法:request 批量定义类的原型方法 (get,post,put..)
其他细节:数据转换工具函数:根据传入的配置数据,获取各大原型方法所需的数据格式。 全局默认配置的转换工具函数...
# 逆向拆解实现axios的核心
# 1.实现三种不同的调用方式
- axios.js
// 生成axios 函数
function createAxios() {
// 创建一个函数
const instance = function () {
return 'axios调用中'
}
// 给这个函数添加get post 等属性
instance.get = function () {
return 'get调用成功'
}
instance.post = function () {
return 'post调用成功'
}
// 初始化方法 返回当前这个函数
instance.create = function () {
return createAxios();
}
// 返回这个函数
return instance;
}
// 生成axios 并暴露[因为直接在 原生html/js 环境测试 暂时不使用模块化语法暴露]
const axios = createAxios();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- test.html
<script src="./axios.js"></script>
<script>
// 方式一: axios(config)
console.log( axios() );
// 方式二: axios.get()
console.log(axios.get());
// 方式三:实例初始化
const instance = axios.create();
console.log( instance.post() );
</script>
2
3
4
5
6
7
8
9
10
问题:
能够看到 目前三种调用方式都已支持
1.配置参数未传递过去 config
2.数据接收 .then() .catch() 还没有
# 2.接收请求参数,并发送请求,返回promise
- 实现get请求三种请求方式,并返回数据
/**
* createAxios 生成axios 函数/对象
* @returns function / Object
*/
function createAxios() {
// 创建一个函数
const instance = function (config) {
// 根据不同请求类型-调用instance 对象上的方法
const { method } = config;
// 例:method=='get' 实现:this.get(url,config)
return instance[method](config)
}
// 给这个函数添加get post 等属性
instance.get = function (config) {
// 为了满足两种方式必须写入 url 和 config
// 有config 就是通过方式1调用 没有config就是方式2/3调用
// 方式2/3调用没有config 需要组装一个config
let newConfig = {};
if (typeof config == 'string') {
//方式2 和方式3
//第一个参数都是url
//如果后面继续跟参数对象,必是 包含params对象数据
let arg = [...arguments];
let paramsData = arg.length > 1 ? arg[1] : {};
newConfig = { method: 'get', url: config, ...paramsData }
} else {
newConfig = { ...config, method: 'get' }
}
return request(newConfig)
}
instance.post = function () {
return 'post调用成功'
}
// 初始化方法 返回当前这个函数
instance.create = function () {
return createAxios();
}
// 返回这个函数
return instance;
}
/**
* request 发送axios
* @return Promise
*/
function request(config) {
// 获取config中的数据
const { url, method, params, data, headers } = config;
return new Promise((resolve, reject) => {
// 初始化 ajax
let xhr = new XMLHttpRequest();
// open send
if (method.toLowerCase() == 'get') {
xhr.open(method, url + paramsFormat(params));
xhr.send()
}
if (method == 'post') {
// 默认请求头 json
let header = { 'Content-type': 'application/json' }
// 设置请求头
xhr.setRequestHeader({ ...header, ...headers })
xhr.open(method, url);
xhr.send()
}
xhr.onreadystatechange = function () {
if (xhr.status == 200 && xhr.readyState == 4) {
resolve(JSON.parse(xhr.responseText))
} else if (xhr.readyState == 4) {
reject(new Error('axios 错误'))
}
}
})
}
/**
* paramsFormat 格式化get请求数据
* @params Object {key:value,key1:value1}
* @return String '?key=value&key1=value1'
*/
function paramsFormat(params) {
let str = '';
//有数据,在进行处理
if (params) {
for (key in params) {
str += `&${key}=${params[key]}`;
}
str = '?' + str.slice(1);
}
return str;
}
// 生成axios 并暴露[因为直接在 原生html/js 环境测试 暂时不使用模块化语法暴露]
const axios = createAxios();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
- 测试三种get请求
注:以下接口只是用来进行测试时使用,请勿恶意攻击服务器
//1
axios({
method: 'get',
url: 'http://115.159.153.85:8001/getTest',
params: { name: "张三" }
}).then(res => {
console.log(res);
})
//2
axios.get('http://115.159.153.85:8001/getTest?name=李四').then(res => {
console.log(res);
})
//3
//这里的初始化 请求头、拦截器参数这部分还未完善
let instance = axios.create();
instance.get('http://115.159.153.85:8001/getTest', { params: { name: '黄四郎' } }).then(res => {
console.log(res);
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 3.完善request 函数,支持post请求
/**
* createAxios 生成axios 函数/对象
* @returns function / Object
*/
function createAxios() {
// 创建一个函数
const instance = function (config) {
// 根据不同请求类型-调用instance 对象上的方法
const { method } = config;
// 例:method=='get' 实现:this.get(url,config)
return instance[method](config)
}
// 给这个函数添加get post 等属性
instance.get = function (config) {
// 为了满足两种方式必须写入 url 和 config
// 有config 就是通过方式1调用 没有config就是方式2/3调用
// 方式2/3调用没有config 需要组装一个config
let newConfig = {};
if (typeof config == 'string') {
//方式2 和方式3
//第一个参数都是url
//如果后面继续跟参数对象,必是 包含params对象数据
let arg = [...arguments];
let paramsData = arg.length > 1 ? arg[1] : {};
newConfig = { method: 'get', url: config, ...paramsData }
} else {
newConfig = { ...config, method: 'get' }
}
return request(newConfig)
}
instance.post = function (config) {
// 重新组装config 思路与get请求一致
//暂时不考虑 config.headers 请求头相关
let newConfig = {};
if (typeof config == 'string') {
//2/3
let arg = [...arguments];
let data = arg.length > 1 ? arg[1] : {};
newConfig = { method: 'post', url: config, data }
} else {
//1
newConfig = { ...config, method: 'post' }
}
return request(newConfig);
}
// 初始化方法 返回当前这个函数
instance.create = function () {
return createAxios();
}
// 返回这个函数
return instance;
}
/**
* request 发送axios
* @return Promise
*/
function request(config) {
// 获取config中的数据
const { url, method, params, data, headers } = config;
return new Promise((resolve, reject) => {
// 初始化 ajax
let xhr = new XMLHttpRequest();
// open send
if (method.toLowerCase() == 'get') {
xhr.open(method, url + paramsFormat(params));
xhr.send()
}
if (method == 'post') {
xhr.open(method, url);
xhr.setRequestHeader('Content-Type', 'application/json');
// 默认请求头 json ,后续通过其他函数统一处理 请求头和参数
let sendData = JSON.stringify(data);
xhr.send(sendData)
}
xhr.onreadystatechange = function () {
if (xhr.status == 200 && xhr.readyState == 4) {
resolve(JSON.parse(xhr.responseText))
} else if (xhr.readyState == 4) {
reject(new Error('axios 错误'))
}
}
})
}
/**
* paramsFormat 格式化get请求数据
* @params Object {key:value,key1:value1}
* @return String '?key=value&key1=value1'
*/
function paramsFormat(params) {
let str = '';
//有数据,在进行处理
if (params) {
for (key in params) {
str += `&${key}=${params[key]}`;
}
str = '?' + str.slice(1);
}
return str;
}
// 生成axios 并暴露[因为直接在 原生html/js 环境测试 暂时不使用模块化语法暴露]
const axios = createAxios();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
- 测试
//1
axios({
method: 'post',
url: 'http://115.159.153.85:8001/postTest',
data: { name: "张三1" }
}).then(res => {
console.log(res);
})
//2
axios.post('http://115.159.153.85:8001/postTest', { name: "张三2" }).then(res => {
console.log(res);
})
//3
const instance = axios.create();
instance.post('http://115.159.153.85:8001/postTest', { name: '张三3' }).then(res => {
console.log(res);
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
剩余问题:
1.通过axios.create() 方法初始化axios时 支持 headers baseUrl timeOut 等属性 的设置
2.不同的请求头 的请求参数的格式不同
3.目前只支持了 get 请求 post 请求 ,应该支持更多的请求方式: put patch delete ...
# 按照axios 源码思路复现
# 1.通读源码-拆解axios 源码
源码地址: https://github.com/axios/axios/tree/v1.x/lib/core
# 2.源码注释
- Axios.js
定义Axios核心库,实现所有的请求方式,requestajax 核心方法的封装
https://github.com/axios/axios/blob/v1.x/lib/core/Axios.js
'use strict';
import utils from './../utils.js';
import buildURL from '../helpers/buildURL.js';
import InterceptorManager from './InterceptorManager.js';
import dispatchRequest from './dispatchRequest.js';
import mergeConfig from './mergeConfig.js';
import buildFullPath from './buildFullPath.js';
import validator from '../helpers/validator.js';
import AxiosHeaders from './AxiosHeaders.js';
const validators = validator.validators;
/**
* Axios 类的创建
* instanceConfig :获取到全局默认配置的参数对象{headers:....}
*/
class Axios {
constructor(instanceConfig) {
// 全局默认配置获取出来,放到实例对象的属性上
this.defaults = instanceConfig;
// 这里是拦截器的初始化,放到之后实例对象的属性上【请求拦截器和响应拦截器】
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
/**
* request 方法是封装了ajax 请求的核心方法,主要封装的是关于ajax的请求头,参数格式化,各种请求方式的原型方法挂载
*
* 获取到请求时的第一个参数:可能是一个请求地址url,也有可能是一个对象【对应不同的调用方式】
* @param {String|Object} configOrUrl
* 有的调用方式,需要传递两个数据,第一个是url 和二个参数是一个对象【请求传递的数据,请求头等等,对应不同的调用方式】
* @param {?Object} config
*
* 返回一个Promise 状态机,这个Promise就是可以在 调用时写 .then() .catch()接收数据的原因
* @returns {Promise}
*/
request(configOrUrl, config) {
/**处理不同调佣方式传递的数据,只传递了一个url过来的重新组装一个config 对象,传递了第2个参数,config对象加一个url属性 */
if (typeof configOrUrl === 'string') {
config = config || {};
config.url = configOrUrl;
} else {
config = configOrUrl || {};
}
/** 将config 对象进行格式化处理:和全局默认配置进行合并,如果都有请求头这种,需要做合并处理 */
config = mergeConfig(this.defaults, config);
const {transitional, paramsSerializer} = 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 !== undefined) {
validator.assertOptions(paramsSerializer, {
encode: validators.function,
serialize: validators.function
}, true);
}
// 处理多种调用方式,axios.get() axios({method:'get'}) 重新组装config 添加method属性
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
// Flatten headers
const defaultHeaders = config.headers && utils.merge(
config.headers.common,
config.headers[config.method]
);
// 通过定义好的所有的请求方式,匹配调用时传递进来的方法 重新组装 config的 method属性
defaultHeaders && utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);
config.headers = new AxiosHeaders(config.headers, defaultHeaders);
// 拿到所有的请求拦截器 和响应拦截器的函数代码
const requestInterceptorChain = [];
let synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
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;
//拦截器验证,给一个promise通过了拦截器之后 在进行下一步
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);
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;
}
// 再次针对config.url进行格式化(防止参数里面有特殊字符和中文汉字等等)
getUri(config) {
config = mergeConfig(this.defaults, config);
const fullPath = buildFullPath(config.baseURL, config.url);
return buildURL(fullPath, config.params, config.paramsSerializer);
}
}
// 支持 axios.get axios.delete 方式的调用,将这个请求方式全部挂载到 原型方法,并针对这四个方法进行请求头的处理
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
}));
};
});
// 支持 axios.post axios.put 方式的调用,将这个请求方式全部挂载到 原型方法,并针对这四个方法进行请求头的处理
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
//封装函数 实现 请求头 相关的属性的格式化
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 类的原型上挂载 所有的方法 get put patch delete get head options
Axios.prototype[method] = generateHTTPMethod();
Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
});
//暴露Axios 类,通过另外一个js文件接收,并实例化
export default Axios;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
- axios.js
将Axios 类进行实例化, 生成一个函数 ,将Axios 生成的实例对象额原型方法 都挂载
https://github.com/axios/axios/blob/v1.1.2/lib/axios.js
'use strict';
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';
/**
* createInstance 函数创建一个 instance 函数对象
*/
function createInstance(defaultConfig) {
// 将全局默认配置传入Axios 类并完成 Axios 实例对象 context的返回
const context = new Axios(defaultConfig);
// 将Axios 类中的原型方法 request 的this指向 context这个实例对象,
// 并返回一个instance 函数 instance()
const instance = bind(Axios.prototype.request, context);
// 将Axios 原型上的方法(get post...) 复制一份 放到 instance() 函数上, 支持: instance.get()
utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
// 将Axios实例对象 context 合并到 instance 对象上
utils.extend(instance, context, null, {allOwnKeys: true});
// 支持实例化方法的调用 const instance = axios.create();
// 返回的 仍然是instacne这个函数对象
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
// 返回instance 这个instance 既是函数 也是对象
return instance;
}
// 调用上面函数封装 实现的axios 对象的创建--【这也是axios库 最终暴露的对象,也就是我们使用的axios 对象】
const axios = createInstance(defaults);
// Expose Axios class to allow class inheritance
axios.Axios = Axios;
// Expose Cancel & CancelToken
axios.CanceledError = CanceledError;
axios.CancelToken = CancelToken;
axios.isCancel = isCancel;
axios.VERSION = VERSION;
axios.toFormData = toFormData;
// Expose AxiosError class
axios.AxiosError = AxiosError;
// alias for CanceledError for backward compatibility
axios.Cancel = axios.CanceledError;
// Expose all/spread
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = spread;
// Expose isAxiosError
axios.isAxiosError = isAxiosError;
axios.formToJSON = thing => {
return formDataToJSON(utils.isHTMLForm(thing) ? new FormData(thing) : thing);
};
export default axios
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76