我们知道,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其实不算难,难点在于对各种默认值、参数、请求形式等的处理。
最终附上完整代码:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。