做为一名前端er,对于数据请求的第三方工具axios
,必定不会陌生,若是仍是有没有用过,或者不了解的小伙伴,这里给大家准备了贴心的中文文档 ,聪明的大家一看就会~html
唔,为了更好的了解和学习 axios
封装思想和实现原理,咱们一块儿来动手来实现一个简版的 axios
~前端
工欲善其事,必先利其器,咱们在开始咱们的项目以前,必定要作好其相关的准备工做,咱们须要准备的也很简单,一个 客户端(client) 方便咱们调试,一个 服务端(server) 作接口测试~node
服务端webpack
服务端我这里为了方便调试,直接使用基于 nodejs
实现的 koa
框架,经过 koa-router
来实现接口,参考代码以下:ios
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~git
客户端es6
客户端这块的话,emm,咱们须要准备一个 html
文件,和 一个 js
文件夹,主要存放咱们要实现的核心代码~github
html
文件很是简单,以下web
<!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
文件~npm
要注意的是,因为咱们的代码是基于 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
地址 感兴趣的小伙伴能够参考参考~