自己动手实现一个 axios
前言
作为一名前端er,对于数据请求的第三方工具axios
,一定不会陌生,如果还是有没有用过,或者不了解的小伙伴,这里给你们准备了贴心的中文文档 ,聪明的你们一看就会~
唔,为了更好的了解和学习 axios
封装思想和实现原理,我们一起来动手来实现一个简版的 axios
~
前期准备
工欲善其事,必先利其器,我们在开始我们的项目之前,一定要做好其相关的准备工作,我们需要准备的也很简单,一个 客户端(client) 方便我们调试,一个 服务端(server) 做接口测试~
服务端
服务端我这里为了方便调试,直接使用基于 nodejs
实现的 koa
框架,通过 koa-router
来实现接口,参考代码如下:
const Koa = require('koa');
const KoaRouter = require('koa-router')
//app 实例
const app = new Koa();
//router 实例
const router = new KoaRouter();
//请求中间件,解决跨域
app.use(async (ctx,next)=>{
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', 'content-type,token,accept');
ctx.set('Access-Control-Allow-Methods', 'POST,GET,OPTIONS');
ctx.set("Content-Type", "application/json")
ctx.set('Access-Control-Max-Age', 10)
//处理 options
if (ctx.request.method.toLowerCase() === 'options'){
ctx.response.status = 200;
ctx.body = '';
} else await next();
})
//接口测试地址
router.get('/',async ctx=>{
ctx.body = {
data : 'Hello World'
}
})
router.get('/user/info',async ctx =>{
ctx.body = {
name : 'Chris' ,
msg : 'Hello World'
}
})
app.use(router.routes());
//启动服务
app.listen(3000,function () {
console.log('app is running ~')
})
这里我们通过 node app.js
就可以启动我们的服务,如果你在服务端控制台看到 app is running ~
说明你的服务已经启动成功,此时你打开浏览器访问 http://localhost:3000/
,不出意外你能看到 Hello World
的返回信息,说明服务端这一块就 配置 ok 了,是不是 so easy~
客户端
客户端这块的话,emm,我们需要准备一个 html
文件,和 一个 js
文件夹,主要存放我们要实现的核心代码~
html
文件非常简单,如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>axios-demo</title>
</head>
<body>
<div class="">
<h1>axios 的简版实现</h1>
</div>
<script src="./js/main.js"></script>
</body>
</html>
其中 main.js
是我们的要使用的js
文件~
要注意的是,由于我们的代码是基于 es6
模块化开发的,如果直接丢到浏览器里,是无法识别的,会报错,不过也没关系,我们可以借助第三方的打包工具帮我们搞定这些事~
打包不是我们主要关注的问题,这里我就不采用webpack
这种工具,给大家推荐一个零配置的打包工具 Parcel ,使用方式也很简单,在你的客户端目录下通过 npm init -y
初始化,通过 npm install parcel-bundler --save-dev
安装 Parcel
,然后在你的 package.json
文件中添加如下脚本:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "parcel ./*.html",
"build": "parcel build ./*.html"
},
这样,我们可以通过 npm run dev
脚本打开我们的 html
文件,如果你们跟我们配置一样,那么你在浏览器的 http://localhost:1234/
地址会看到 axios 的简版实现 这几个字,并且控制台不会报错,就证明一切准备 ok 了!!
具体实现
雏形
我们首先在客户端的 js
文件夹下创建一个 axios
的文件夹,里面存放我们自己实现的 axios
相关代码。
在 axios
文件夹下新建 index.js
入口文件 和 axios.js
核心js
文件~
axios
的本质是一个类,这里我们通过 class
实现,即:
axios.js
class Axios {
constructor(){
}
}
export default Axios;
通过 index.js
进行 new
初始化,导出 axios
实例,这也是我们在使用axios
中 不需要 new
的原因~
index.js
import Axios from './Axios'
const axios = new Axios();
export default axios;
此时,我们只需要在 main.js
通过 import
导入即可
main.js
import axios from './axios'
console.log(axios)
此时整个 axios
雏形就已经完成了~
一个简单的get请求
我们先实现一个简单 axios.get
方法,即通过 axios.get
获取我们服务端的响应~
我们回忆一下我们平时使用 axios.get
的时候,通常是 axios.get().then
的方式,那么我们首先就确定了我们的 axios.get
方法返回的是一个 Promise
对象,我们在 axios.js
中添加这个方法~
get(url){
return new Promise((resolve => {
let xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve({
data: JSON.parse(xhr.responseText),
status: xhr.status,
statusText: xhr.statusText
});
}
xhr.open( 'get', url , true );
xhr.send();
}))
}
此时我们在 main.js
调用 get
方法 ,
axios.get('http://127.0.0.1:3000/user/info').then(res=>{
console.log(res);
})
控制台输出如下:
对比官方的 axios
,我们少了比如 header
之类的信息,因为官方对请求返回做了二次包装,这里我们只是简单的json
处理,具体的要根据返回的数据类型做不同的处理~
默认配置
我们在使用官方 axios
的,会有很多配置项,包括全局配置,实例配置和请求配置,因此我们就来看看配置信息这一块。
我们在 axios
文件夹下新建一个 config.js
,用于 axios
的默认配置,为了方便,我们的默认配置如下:
config.js
export default {
baseURL : '' ,
method : 'get' ,
headers : {
'content-type' : 'application/json'
}
}
我们将默认的配置传入到我们的构造函数中,如下:
index.js
import Axios from './Axios'
import config from './config'
const axios = new Axios(config);
export default axios;
所以,我们需要在构造函数中接收一个 config
参数进行处理,即将默认配置写入到实例中,即:
axios.js
constructor(config){
//配置
this.defaults = config;
}
这样我们的 get
方法里请求的 url
就可以改写成 :
this.defaults.baseURL += url
......
xhr.open( 'get', this.defaults.baseURL , true );
//添加header头
for(let key in configs.headers){
xhr.setRequestHeader(key,configs.headers[key])
}
......
如果你此时在config.js
中配置 baseURL
那么,你在axios.get
中就可以省略前面的 baseURL
, 因为在请求之前已经帮你拼接完成了~
当然,你也可以通过 axios.defaults.baseURL = xxx
这种方式修改默认配置,都是没问题的~
实例配置
在使用官方 axios
的时候,我们可以通过一个create
方法创建一个axios
实例,并传入配置信息即可,我们只需要在 index.js
中创建的 axios
添加一个 create
方法即可 。
index.js
axios.create = function (config) {
return new Axios(config);
}
这样我们也可以通过 create
方法构建一个 axios
实例,它也拥有相应的方法~
但是这么做存在一个问题,如果我们创建多个实例,传入不同的 config
,由于我们直接在构建的时候 通过 this.defaults = config;
这种方式复制,并没有切断对象的引用关系,因此会导致配置对象会被相互引用,出问题~
因此,我们需要对其进行 深拷贝 赋值,即 this.defaults = deepClone(config)
, 其中 deepClone
时深拷贝函数,这里不再赘述~
请求配置
我们发现官方的 axios
的get
、post
等请求会有第二个可选参数,也是 config
,即单独本次请求的配置,如果存在,我们需要进行配置合并,对于简单的 baseURL
、method
等这种简单的配置直接覆盖,对于headers
这种复杂的对象配置,进行对象合并,有点类似 Object.assign
方法~
所以,我们更改我们的 get
方法如下:
get(url,config){
let configs = mergeConfig(this.defaults,config);
return new Promise((resolve => {
let xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve({
data: JSON.parse(xhr.responseText),
status: xhr.status,
statusText: xhr.statusText
});
}
xhr.open( 'get', configs.baseURL + url , true );
//添加header头
for(let key in configs.headers){
xhr.setRequestHeader(key,configs.headers[key])
}
xhr.send();
}))
}
其中 mergeConfig
是合并两配置对象的方法,具体实现参考如下:
function mergeConfig (obj1, obj2) {
let target = deepClone(obj1),
source = deepClone(obj2);
return Object.keys(source).reduce((t,k)=>{
if(['url','baseURL','method'].includes(k)){
t[k] = source[k]
}
if(['headers'].includes(k)){
t[k] = Object.assign({},source[k],t[k])
}
return t;
},target)
}
ok~ 现在我们就可以通过如下方式进行请求了:
axios.get('/user/info',{
baseURL : 'http://127.0.0.1:3000' ,
headers : {
token : 'x-token-123456'
}
}).then(res=>{
console.log(res);
})
可以看到控制台输出跟之前的是一样的~
细心的小伙伴可以看到 header
头已经添加了 token
信息~
拦截器
拦截器主要用于在请求之前或者请求之后可自定义对配置或者响应结果做一系列的处理,axios
官方给我们提供了 use
方法,可以添加多个拦截器,使用方式如下:
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Do something with response data
return response;
}, function (error) {
// Do something with response error
return Promise.reject(error);
});
那么,接下来我们自己来实现这么一个 use
方法~
首先我们需要在我们的 axios
实例上添加一个 interceptors
对象,该对象有 request
和 response
两个属性,他们都拥有 use
方法,我们发现 use
方法的结构都相同,入参为两个函数,其实他们是同一个 Interceptor
类的不同实例而已。
我们先来构建 Interceptor
这个类,首先在 axios
文件夹下新建 Interceptor.js
文件,并定义如下:
Interceptor.js
export default class Interceptor {
constructor() {
this.handlers = [];
}
use( resolvedHandler, rejectedHandler ) {
this.handlers.push({
resolvedHandler,
rejectedHandler
});
}
}
这里,我们 new
出来的的实例都会拥有 use
方法,并且我们通过一个 handlers
数组来保存,这样可以保证我们可以多调用 use
方法,添加多个拦截器~
我们只需在 Axios.js
中的 constructor
构造函数中初始化即可。
Axios.js
constructor(config){
//默认配置
this.defaults = deepClone(config);
//拦截器
this.interceptors = {
request : new Interceptor() ,
response : new Interceptor()
}
}
这样尽管我们已经可以在我们的 main.js
中使用 use
方法添加拦截器了,但是还是无法正确使用,因为请求这一块还未进行处理,接下来,我们需要对我们之前的 Axios.js
进行改造~
首先,我们统一封装一个 request
函数,往后所有的请求都会调用这个方法,入参需要一个 config
,返回一个 Promise
对象,我们在这里对拦截器进行操作,定义如下:
//request请求
request (config) {
//配置合并
let configs = mergeConfig(this.defaults, config);
//将配置转成 Promise 对象,链式调用和返回 Promise 对象
let promise = Promise.resolve(configs);
//请求拦截器,遍历 interceptors.request 里的处理函数
let requestHandlers = this.interceptors.request.handlers;
requestHandlers.forEach(handler => {
promise = promise.then(handler.resolvedHandler, handler.rejectedHandler)
});
//数据请求
promise = promise.then(this.send)
//相应拦截器,遍历 interceptors.response 里的处理函数
let responseHandlers = this.interceptors.response.handlers;
responseHandlers.forEach(handler => {
promise = promise.then(handler.resolvedHandler, handler.rejectedHandler)
})
//返回响应信息
return promise;
}
上面,为了代码简洁,我又将 send
方法提出来,定义跟之前基本一致:
//发送请求
send (configs) {
return new Promise((resolve => {
let xhr = new XMLHttpRequest();
xhr.onload = function () {
resolve({
data: JSON.parse(xhr.responseText),
status: xhr.status,
statusText: xhr.statusText
});
}
xhr.open(configs.method, configs.baseURL + configs.url, true);
//添加header头
for ( let key in configs.headers ) {
xhr.setRequestHeader(key, configs.headers[key])
}
xhr.send();
}))
}
哦对啦,我们之前的 get
方法也有一点点的不同,主要是加入了请求拦截器~
// 发送get请求
get (url, config) {
config.method = 'get';
config.url = url;
return this.request(config);
}
趁热打铁,我们来试试~
这里我在 main.js
中分别添加了 2 个响应拦截器和请求拦截器:
//请求拦截器
axios.interceptors.request.use(config=>{
console.log('请求配置信息:',config);
return config
})
axios.interceptors.request.use(config=>{
config.headers.token = 'x-token-654321';
return config
})
//响应拦截器
axios.interceptors.response.use(res=>{
console.log('请求响应信息',res)
return res;
})
axios.interceptors.response.use(res=>{
res.msg = 'request is ok ~';
return res;
})
请求拦截器分别打印了请求的配置并将请求的 token
值经行了修改,响应拦截器分别打印了响应信息并将响应添加了 msg
的属性~
不出意外,你在控制台可以看到如下信息,在请求 header
里看到 token
已经被更改~
大功告成!
总算是有点样子啦~
结语
至此,我们自己封装了一个非常简单的 axios
的请求库,由于篇幅有限,这里我只是用了最简单的 get
请求示例,axios
源码中远不止这些,像一些异常处理、取消请求等的一系列的东西都还没有实现,这里主要是借鉴其一些思想和实现的思路,我这里只是牵个头,剩下的靠你们自己不断的去完善,动动手总是好的~
文末,附上 git
地址 感兴趣的小伙伴可以参考参考~