重磅从0实现一个axios网络请求框架百分之七十的功能

32次阅读

共计 8782 个字符,预计需要花费 22 分钟才能阅读完成。

我们知道,axios 是前端一个非常优秀的对于网络请求的框架,其特点主要是请求方便、功能多(如拦截器)。那么作为一枚前端开发人员,了解并能够使用 axios 其实是基础,深入了解其实现原理才是比较重要的,当然,如果能徒手撸一个 axios 类似的框架出来,那就是相当的不错了。

这篇文章会从以下几个大的点来实现一个 axios 框架:

  1. axios 的本质是什么?
  2. axios 默认值、参数的实现
  3. 常见请求方式:get、post、delete 在 axios 中的实现
  4. 真正请求 (XMLHttpRequest) 的实现
  5. axios 拦截器的实现
  6. 打包发布

同时希望你了解以下的一些知识点:

  • 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:{}})

分析三者请求参数的相同点

  1. 都只有一个参数,其参数是字符串类型

    • axois.get(url)
    • axios.post(url)
    • axios.delete(url)
  2. 都只有一个参数,其参数类型是 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…

正文完
 0