共计 8782 个字符,预计需要花费 22 分钟才能阅读完成。
我们知道,axios 是前端一个非常优秀的对于网络请求的框架,其特点主要是请求方便、功能多(如拦截器)。那么作为一枚前端开发人员,了解并能够使用 axios 其实是基础,深入了解其实现原理才是比较重要的,当然,如果能徒手撸一个 axios 类似的框架出来,那就是相当的不错了。
这篇文章会从以下几个大的点来实现一个 axios 框架:
- axios 的本质是什么?
- axios 默认值、参数的实现
- 常见请求方式:get、post、delete 在 axios 中的实现
- 真正请求 (XMLHttpRequest) 的实现
- axios 拦截器的实现
- 打包发布
同时希望你了解以下的一些知识点:
- webpack
- axios 框架的基本使用
- Es6 的 Proxy、Class 等
- XMLHttpRequest
- http 协议 … 等
axios 的本质是什么
使用 axios 框架的时候,我们大部分情况都是以模块的形式引入进行使用,如:
import axios from 'axios'
发出一个 get 请求,如:
axios.get('/user')或 axios('/user')
从中可以看出,axios 的本质就是一个函数,那么就先来实现一个 axios 的雏形。(本篇文章实现利用 es6 的 calss 去实现)。
axios.js
class Axios{constructor(){}}
export default new Axios();
index.js
import axios from './axios'
这里毫无疑问可以看出,axios 现在只是一个 Axios 的实例,并不是一个函数,那么要怎样将 axios 变成一个函数?这就需要两个知识点:Axios 的构造函数是可以进行返回值操作、利用 ES6 的 Proxy。更进一步的 Axios 类的代码如下:
class Axios{constructor(){return new Proxy(function(){},{apply(fn,thisArg,agrs){}})
}
}
export default new Axios();
这样我们得到的一个 Axios 实例 axios 就是一个函数了。这里简单提一下,Proxy 在进行函数处理的使用,apply 是很有用的,使用它我们就可以对一个函数进行代理处理了。
看看打印出来的内容:
接下来,要怎样才能够实现 axios(‘/user’)或 axios.get(‘/user’)这样的效果呢?, 我们知道了 axios 是一个函数,那么给一个函数增加一个属性(也是函数),就能够解决这个问题了。先来简单看下函数式的写法:
继续完善 Axios 类中的代码:
class Axios{constructor(){
const _this = this;
return new Proxy(function(){},{apply(fn,thisArg,agrs){ },
get(fn,key){return _this[key];
},
set(fn,key,val){_this[key] = val;
return true;
}
})
}
get(){console.log('get request');
}
}
export default new Axios();
这样就实现了 axios(‘/user’)或 axios.get(‘/user’)这样的效果了。
axios 默认值、默认参数的实现
我们知道,在使用 axios 的时候,可以有如下的使用情况:
import Axios from 'axios'
let axios1 = Axios.create({
baseUrl:'http://www.xxx.com',
headers:{
common:{
a:'12',
b:{c:{}
}
}
}
})
let axios2 = Axios.create({baseUrl:"http://www.yyy.com",})
或
axios1.default.baseUrl = 'http://www.xxx.com';
axios2.default.baseUrl= "http://www.yyy.com";
Axios.default.headers.common.a = '123';
也就是 通过 axios 的 create 方法来创建不同的 axios 实例,那接下来就来实现一下这个 create 方法
首先需要一个默认配置项,比如 我们在使用 axios(‘/user’)来进行请求的时候,其实默认的请求方式是 get、默认配置里面还有 baseUrl、headers 等基本默认信息。那么就需要抽离出来成为一个独立的模块(设计模式中的单一职责)default.js:
export default {
method:'get',
bserUrl:'',
headers:{
common:{'X-Request-By':'XMLHttpRequest'},
get:{ },
post:{}}
}
并在 axios.js 中引入
import defaultOptions from './default'
接下来就是实现 create 这个方法以及类 Axios 和实例 aixos 的 default 配置属性。
let axios1 = Axios.create({
baseUrl:'http://www.xxx.com',
headers:{
common:{
a:'12',
b:{c:{}
}
}
}
})
该方法的参数是一个对象。当我们像上面这样配置的时候,就需要将默认配置项中的部分进行覆盖,也就是需要对对象进行合并操作。首先抽取一个模块,名字叫utils.js,主要主要是封装各种工具子模块(参数类型判断、对象合并、对象克隆)。部分代码如下:
并在类 Axios 所在模块中引入,便于后续使用。
最终 create 方法实现如下:
Axios.create = Axios.prototype.create = function(options){let axios = new Axios();
// 拷贝一份默认配置
let res = cloneObj(defaultOptions);
merge(res,options);
console.log('res:',res);
axios.default = res;
return axios;
}
export default Axios.create();
当我们通过下面代码去使用的时候。
import Axios from './axios'
let axios1 = Axios.create({
baseUrl:'http://www.xxx.com',
headers:{
common:{
a:'12',
b:{c:{}
}
}
}
})
或
let axios2 = Axios.create({})
axios2.default.headers.common.test = 'test';
最终得到我们想要的:
常见请求方式:get、post、delete 在 axios 中的实现
在 axios 中,这三种常见请求的格式大致如下:
get 请求:
- axios.get(url)
- axios.get(url,{params:{},headers:{}})
- axios.get({url,params:{},headers:{}})
post 请求:
- axios.post(url)
- axios.post(url,{a:12,b:13})
- axios.post(url,{a:12,b:13},{params:{},headers:{}})
- axios.post({url,params:{},headers:{},data})
delete 请求:
- axios.delete(url)
- axios.delete(url,{headers:{},params:{}})
- axios.delete({url,headers:{},params:{}})
分析三者请求参数的相同点:
-
都只有一个参数,其参数是字符串类型
- axois.get(url)
- axios.post(url)
- axios.delete(url)
-
都只有一个参数,其参数类型是 object
- axios.get({url,params:{},headers:{}})
- axios.post({url,params:{},headers:{},data})
- axios.delete({url,headers:{},params:{}})
接下来就按照这样的一个规律去实现这三种请求。先来处理 get 请求的方式:
get(...agrs){
let options;
if(agrs.length===1 && typeof agrs[0] ==='string'){//axois.get(url)
options = {
method:'get',
url:agrs[0]
}
}else if(agrs.length === 1 && agrs[0] instanceof Object){//axios.get({url,params:{},headers:{}})
options = {...agrs[0],
method:'get'
}
}else if(agrs.length === 2 && typeof agrs[0] ==='string'){//axios.get(url,{params:{},headers:{}})
options={
method:'get',
url:agrs[0],
...agrs[1]
}
}else{assert(false,`arguments invalidate!`)
}
console.log('get options:',options);
}
测试:
那么 post 与 delete 的处理方式也类似,它们由很多相同点,所以提取到同一个方法中进行处理。
/**
* 预处理方法参数
* @param {*} methdoType
* @param {*} args
*/
_preprocessArgs(methdoType,args){
let options;
if(args.length===1 && typeof args[0] === 'string'){
options = {
method:methdoType,
url:args[0]
}
}else if(args.length===1 && args[0].constructor === Object) {
options = {...args[0],
method:methdoType
}
}else{return undefined}
return options;
}
最终实现的 get、post、delete 部分代码如下:
这里还有一种特殊需要处理的,axios(‘/user’)、axios(‘/user’,{})、axios({url,xxx})这三种情况,这就需要借助 Proxy 第二个参数中的 apply 方法。该方法的第一个参数就是 axios 这个方法,第三个参数就是 axios 方法中传递的参数。
真正请求 (XMLHttpRequest) 的实现
这里真正的网络数据请求模块与 axios 模块是独立开来的,请求模块只负责数据的请求,而 axios 模块需要负责对数据请求进行处理。
从上面实现的 get、post、delete 方法中,最后调用的实例的 request 方法,该方法的主要作用有:
- 请求头的处理
- 参数的检测
- 正式调用请求数据
- 变换请求 transformRequest、transformResponse 的处理
请求头的处理:这里有三个地方涉及到请求头,默认的配置项 defaultOptions(axios(this).default)、get/post/delete 请求中配置的、options 中配置的,其优先级是 defaultOptions(axios(this).default).default<get/post/delete<options。
参数的检测:
// 参数检测
checkOptions(options);
function checkOptions(options){assert(options,'options is required!');
assert(options.url,`not found url!`);
assert(typeof options.url === 'string',`the type of url must be string!`);
assert(options.url,`not found method!`);
assert(typeof options.method === 'string',`the type of method must be string!`);
}
正式调用请求数据:这里需要实现 XMLHttpRequest 模块request.js,并准备些 Mock 数据进行测试。
request.js 模块代码如下:
export default function request(options) {let xhr = new XMLHttpRequest();
xhr.open(options.method,options.url,true);
for(let key in options.headers){xhr.setRequestHeader(encodeURIComponent(key),encodeURIComponent(options.headers[key]));
}
xhr.send(options.data);
return new Promise((resolve,reject) => {xhr.onreadystatechange = function () {if(xhr.readyState===4){if(xhr.status>=200 && xhr.status<300){resolve(xhr);
}else{reject(xhr);
}
}
}
})
}
调用情况如下:
Mock 数据如下:
src/data/test.json:
{
"skill":"javascript",
"name":"Darkcode"
}
请求方式如下:
import axios from './axios'
axios.get('../datas/test.json').then((res) => {console.log('返回的数据是:',res);
})
发现 url 这里有问题:
也就上图中对 url 拼接这里出了问题。这里有了 nodejs 内置 url 模块来解决这个问题:
options.url = urlLibrary.resolve(options.baseUrl,options.url);
axios.get(url).then(),可以知道 get 方法返回的是一个 promise,所以在最开始处理实现的 get、post、delete 等地方,需要进行类 Axios request 方法,该方法返回一个 promise。
request 方法的完善代码如下:
这时候,请求就成功了,可以看到代码:
axios.get('../datas/test.json').then((res) => {console.log('返回的数据是:',res);
})
打印出来的就是一个 xhr 对象。
发现这返回来的数据与使用真正的 axios 框架中的返回值不填一样,真正使用 axios 返回的数据应该是如下:
{
status:200,
statusText:'ok',
data:{},
...
}
这时候就需要对请求到的数据进行进一步处理了,鉴于请求返回的模式:返回成功、返回失败,抽取成单独的模块进行处理。
请求成功模块:response.js,对数据进行封装
export default function (xhr) {let arr = xhr.getAllResponseHeaders().split('\r\n')
let headers = {};
arr.forEach((str) =>{if(!str){return;}
const [name,value] = str.split(":");
headers[name] = value;
})
return{
ok:true,
status:xhr.status,
statusText:xhr.statusText,
data:xhr.response,
xhr,
headers
}
}
请求失败的模块:error.js
export default function(xhr){
return {
ok:false,
status:xhr.status,
statusText:xhr.statusText,
data:xhr.response,
}
}
接下来在 axios.js 模块中引入这两个模块,并针对请求 request 模块部分的代码进行处理。
import response from './response'
import err from './error'
// 发出真正的请求
return new Promise((resolve,reject) => {request(options).then((xhr) => {let res = response(xhr);
resolve(res)
},(xhr) => {let error = err(xhr);
reject(error);
});
})
在看一下得到的数据情况:
是不是发现 data 里面是字符串形式,而不是我们常见的 json 对象形式,????,接着搞。
变换请求 transformRequest、transformResponse 的处理
在 axios 中。
- transformRequest:负责向服务器发送请求前针对数据进行处理
- transformResponse:负责服务器返回数据后针对数据进行处理。
这两个配置对象属性是一个函数。简单用法如下:
axios.create({transformRequest:function (config) {
//do something
return config;
},
transformResponse:function (res) {
////do something
return JSON.parse(res);
}
})
那么要解决上面返回的 data 的值是字符串的问题,就很简单了。直接在默认配置模块中进行配置:
并在真正请求之前,和请求之后对数据做处理:
const {transformRequest,transformResponse} = options;
options = transformRequest(options);
checkOptions(options);
// 发出真正的请求
return new Promise((resolve,reject) => {request(options).then((xhr) => {let res = response(xhr);
res.data = transformResponse(res.data);
resolve(res)
},(xhr) => {let error = err(xhr);
reject(error);
});
})
这下数据就完全正确了。
变换一下请求方式:
import axios from './axios'
axios.default.headers.common.auth = 'xxxx';
axios('../datas/test.json').then((res) => {console.log('返回的数据是:',res);
})
也是很 OK 的。
axios 拦截器的实现
axios 拦截器:axios.interceptors
- axios.interceptors.request.use(config => {config})
- axios.interceptors.response.use(response => { response})
axios 的拦截器的功能有点类似上面提到的 transformRequest、transformResponse 的功能,但又有区别,拦截器的功能更加强大,不仅可以针对数据进行处理,还可以针对实际业务进行功能的处理等。
从写法上来看,用法大致一样。分别针对请求前数据、请求后的数据进行拦截处理。这里也是需要抽取成一个独立模块:interceptors.js。
export default class Interceptors{constructor(){this._list =[]
}
use(fn){this._list.push(fn);
}
list(){return this._list;}
}
然后在 Axios 构造函数中初始化 interceptors 对象属性,包含 require、response 两个属性:
接下来在 request 方法中进行处理。
测试一下使用情况:
import axios from './axios'
axios.default.headers.common.auth = 'xxxx';
axios.interceptors.request.use(function(config){
config.headers.abc = '11'
return config;
})
axios('../datas/test.json').then((res) => {console.log('response info is:',res);
})
再测试一个错误的请求:
axios('../datas/test1.json').then((res) => {console.log('response info is:',res);
},(err) => {console.log('error info:',err);
})
打包发布
往往在一个库或者框架开发测试完后,需要打包发布给他人使用,接下来就是对已完成的 axios 进行打包。
npm run build
至于发布操作,通常都会选择发布到 npmjs 上,这里就不做一一的操作了。很简单。
到此。从 0 实现一个 axios 其实不算难,难点在于对各种默认值、参数、请求形式等的处理。
最终附上完整代码:
https://github.com/huangche00…