什么是 axios
关于 axios 是什么,使用 Vue 开发的同学应该不会陌生,具体的 API 也可以参考axios 中文文档
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
(这里只针对浏览器的原理进行阐述)
手写实现
创建本地文件
demo.html
<button id="btnId">请求数据</button>
node 服务模拟数据
// httpServer.js
let express = require('express')
let app = express()
app.all('*', (req, res, next) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Content-Type', 'application/json;charset=utf-8')
next()
})
app.get('/', (req, res) => {
res.send('首页')
})
app.get('/get/info', (req, res) => {
res.send({
code: 200,
data: {
sunny: '阳光'
}
})
})
app.listen(3000, () => {
console.log('服务器启动成功...')
})
axios实现
axios({})
axios 的原理还是创建 XMLHttpRequest
// axios.js
class Axios {
constructor() {
}
request(config) {
return new Promise(resolve => {
const {url = '', method = 'get', data = {}} = config
const xhr = new XMLHttpRequest()
xhr.open(method, url, true)
xhr.onload = function() {
resolve(xhr.responseText)
}
xhr.send()
})
}
}
function createInstance() {
let axios = new Axios()
let req = axios.request.bind(axios)
return req
}
let axios = CreateAxiosFn() // demo.html中调用的就是这个axios 全局变量
说明
- 这里创建一个
axios
实例,原型上有一个request
方法,使用axios({})
请求接口,我们需要调用的其实是request
,所以需要返回request
这个方法 bind
方法返回的是一个函数,fun.bind(context)
返回的其实还是这个 fun,只是改变了它内部的作用到 context上而已- 有个疑问:为什么不直接返回实例的方法
axios.request
,而非要用bind
来?
现在我们点击按钮可以返回数据了。
// demo.html
<button id="btnId">请求数据</button>
<script src="axios.js"></script>
<script>
document.querySelector('#btnId').onclick = () => {
axios({
method: 'get',
url: 'http://localhost:3000/get/info'
}).then(res => {
console.log('响应数据,', res)
})
}
</script>
到此完成了 axios({})
形式调用。
axios.get()
const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post']
methodsArr.forEach(met => {
Axios.prototype[met] = function() {
if (['get', 'delete', 'head', 'options'].includes(met)) { // 2个参数(url[, config])
return this.request({
method: met,
url: arguments[0],
...arguments[1] || {}
})
} else { // 3个参数(url[,data[,config]])
return this.request({
method: met,
url: arguments[0],
data: arguments[1] || {},
...arguments[2] || {}
})
}
}
})
现在在 Axios 的 prototype 上添加了对应的方法,但是 axios
实例返回出去的是 request
方法啊,也就是说这个返回出去的 request
方法需要有 Axios.prototype
上 这些方法,如何做呢?
将 Axios.prototype
的方法搬运到 request
上去
下面的工具方法就可以做到
const utils = {
extend(a, b, context) {
for(let key in b) {
if (b.hasOwnProperty(key)) {
if (typeof b[key] === 'function') {
a[key] = b[key].bind(context);
} else {
a[key] = b[key]
}
}
}
}
}
然后修改 createInstance
方法
function createInstance() {
let axios = new Axios()
let req = axios.request.bind(axios)
utils.extend(req, Axios.prototype, axios) // 混入,使得axios的request拥有get,post...等方法
return req;
}
此时我们可以使用 axios.get(‘xxx’)
来调用了
document.querySelector('#btnId').onclick = () => {
axios.get('http://localhost:3000/get/info').then(res => {
console.log('响应数据,', res)
})
}
请求和响应拦截器
我们发送一个请求时,会先执行请求拦截器的代码,然后执行我们的发送代码。
返回时会先执行响应拦截器的代码对数据进行一个过滤筛选,然后再把数据返回。
使用过拦截器的知道,我们需要通过 axios.interceptors.request.use
和 axios.interceptors.response.use
来调用,也就是说 axios
有个 interceptors
对象,这个对象有两个拦截器,一个处理请求,一个处理响应,所有 Axios
的构造函数可以写成
class Axios {
constructor() {
this.interceptors = {
request: new InterceptorsManage,
response: new InterceptorsManage
}
}
request(config) {
...
}
}
而且两个拦截器都会调用use
方法,当执行 use
的时候会将我们传入的回调函数 push 到拦截器中处理。
拦截器也是一个构造函数
class InterceptorsManage{
constructor() {
this.handlers = []
}
use(fillfield, rejected) {
this.handlers.push({fillfield, rejected})
}
}
上面我们知道,我们返回的 axios
实例其实是 request
方法,所以还是需要将拦截器搬到 request
上去
function createInstance() {
let axios = new Axios();
let req = axios.request.bind(axios);
utils.extend(req, Axios.prototype, axios)
utils.extend(req, axios) // 混入,使axios的request拥有interceptor拦截器
return req;
}
发送请求时,会先获取 request
拦截器的的方法来执行,再执行我们发送的请求,再获取 response
拦截器的方法来执行
因为需要重写我们的请求方法 request
先将 ajax 单独提取出来,用来插入执行的队列中
class Axios {
constructor() {
this.interceptors = {
request: new InterceptorsManage,
response: new InterceptorsManage
}
}
request(config) {
let chain = [this.sendAjax.bind(this), undefined]
// 请求拦截
this.interceptors.request.handlers.forEach(interceptor => {
chain.unshift(interceptor.fillfield, interceptor.rejected)
})
// 响应拦截
this.interceptors.response.handlers.forEach(interceptor => {
chain.push(interceptor.fillfield, interceptor.rejected)
})
// 执行队列
let resolvePromise = Promise.resolve(config)
while(chain.length > 0){
// 不断对 config 从上一个promise传递到下一个 promise
// Promise.resolve() 返回是一个新的 promise 对象
resolvePromise = resolvePromise.then(chain.shift(), chain.shift())
}
return resolvePromise
}
sendAjax(config) {
return new Promise(resolve => {
const {url = '', method = 'get', data = {}} = config;
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.onload = function() {
resolve(xhr.responseText);
}
xhr.send();
})
}
}
此时拦截器已经起作用了
axios.interceptors.request.use(function(config) {
console.log('请求拦截器先走,', config)
return config
})
axios.interceptors.response.use(function(data) {
console.log('拦截到响应数据了,', data)
return data
})
document.querySelector('#btnId').onclick = () => {
axios.get('http://localhost:3000/get/info').then(res => {
console.log('响应数据,', res)
})
}
取消请求
先看使用
const axios_cancel = axios.CancelToken
const source = axios_cancel.source()
axios.interceptors.request.use(function(config) {
console.log('请求拦截器先走,', config)
return config
})
axios.interceptors.response.use(function(data) {
console.log('拦截到响应数据了,', data)
return data
})
document.querySelector('#btnId').onclick = () => {
axios.get('http://localhost:3000/get/info', {
cancelToken: source.token
}).then(res => {
console.log('响应数据,', res)
})
}
// 取消
source.cancel()
取消请求也是在 request
阶段,因为我们的请求之后是需要传给下一阶段拦截器 response
的,所有还是需要返回一个 promise
sendAjax(config) {
return new Promise((resolve, reject) => {
const {url = '', method = 'get'} = config;
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.onload = function() {
resolve(xhr.responseText);
}
xhr.send();
// 取消请求
if(config.cancelToken) {
config.cancelToken.promise.then(() => {
xhr.abort()
resolve('请求取消了')
})
}
})
}
cancelToken需要返回一个promise的resolve 方法
function CancelToken(executor) {
let resolvePromise
this.promise = new Promise(resolve => {
resolvePromise = resolve
})
executor(resolvePromise)
}
CancelToken.source = function() {
let cancel;
let token = new CancelToken(c => {
cancel = c
})
return {token, cancel}
}
var axios = createInstance();
axios.CancelToken = CancelToken
这里 CancelToken
构造函数中需要传一个函数,作用是将控制内部 Promise 的 resolve 暴露出去给 source.cancel
函数,这样内部的 Promise 状态就可以通过source.cancel
方法来控制,所以说如果接口处不调用 source.cancel
请求是不会 abort
的