文章目录
1. 前言
偶然看到了这个axios这个框架,当然也找到了一个官方的文档说明。
Axios
是一个基于promise
的HTTP
库,可以用在浏览器和node.js
中。
了解一些前端开发的大多都使用过AJAX
来进行网络请求,那么为什么还需要axios
这个库。我们知道AJAX
的引入是为了页面的局部刷新,即:AJAX
= 异步 JavaScript
和 XML
(Asynchronous Javascript And XML
)。而axios
可以看作是对AJAX
的进一步封装,提供了更多的封装以及更加安全,可以防御CSRF
/XSRF
攻击。当然首先还是需要了解一下promise
是什么?
1.1 promise 是什么?
可以查看JavaScript Promise 对象这篇文章。Promise
对象是ES6
中提供的,用来确保在JavaScript
中只有异步操作的结果可以决定哪种状态,其他任何操作都无法改变其状态。Promise
对象代表一个异步操作,有三种状态:
pending
: 初始状态,表示进行中;fulfilled
: 操作成功;rejected
: 操作失败;
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来。相比较于AJAX
而言,有了 Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。比如下面的简单案例:
function test(){
// Promise对象上有then、catch方法
return new Promise(function (resolve, reject) {
// 异步操作
// 代码正常执行
resolve("Message");
// 非正常执行
reject(new Error());
});
}
test().then(function(message){
// resolve
}).catch(function(error){
// reject
});
上面代码中,resolve
方法和 reject
方法调用时,都带有参数。它们的参数会被传递给回调函数。reject
方法的参数通常是 Error
对象的实例,而 resolve
方法的参数除了正常的值以外,还可能是另一个 Promise
实例。
resolve
方法的作用是将Promise
对象的状态从pending
变为resolved
;reject
方法的作用是将Promise
对象的状态从pending
变为rejected
;
当然在其回调方法中还有all
和race
,这里不再介绍,感兴趣可以查看博客:ES6 Promise用法小结。
2. axios
使用命令
yarn add axios
// 我本地安装了两个Node,这里需要切换node版本
nvm use 16.13.0
当然如果安装过慢可以换个源:
yarn config set registry https://registry.npm.taobao.org
// 如果有问题,还是改为默认算了
// yarn config set registry https://registry.yarnpkg.com
当然这里还是在react
项目中进行测试,比如这里简单请求一个get
方式的请求,如下:
import React from 'react'
import axios from 'axios'
export default function Test() {
axios
.get('https://i.maoyan.com/api/mmdb/movie/v3/list/hot.json')
.then(function (response) {
console.log(response) // 处理成功情况
})
.catch(function (error) {
console.log(error) // 处理错误情况
})
.then(function () {
console.log('finish') // 总是会执行
})
return <div>Test</div>
}
结果:
在这里其实也就是跨域问题。
2.1 解决跨域问题
可以使用http-proxy-middleware
,参考网址:Create React App。首先安装这个插件:
yarn add http-proxy-middleware
这里我的安装版本为^2.0.1
。然后在src
目录下创建一个setupProxy.js
,然后在这个文件中写入:
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:5000',
changeOrigin: true,
})
);
};
因为在请求的链接地址中开始的时候为api
,所以这里修改为:
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api', // 代理路径
createProxyMiddleware({
target: 'https://i.maoyan.com', // 真实地址
secure: true, // 如果是https接口 需要配置这个参数为true
changeOrigin: true,
})
);
// 可配置多个
};
然后我们把请求域名删除掉,即将react
修改为:
import React from 'react'
import axios from 'axios'
export default function Test() {
axios
.get('/api/mmdb/movie/v3/list/hot.json') // 去掉非同源域
.then(function (response) {
console.log(response) // 处理成功情况
})
.catch(function (error) {
console.log(error) // 处理错误情况
})
.then(function () {
console.log('finish') // 总是会执行
})
return <div>Test</div>
}
值得注意的是如果访问的还是http://localhost/
域名下的东西,还是会报错403
。这里需要使用网络IP
地址,比如下面两个测试效果图:
可以看到这里的返回了403
,即:
403
Forbidden
:服务器收到请求,但是拒绝提供服务。
所以这里需要使用非localhost
域名:
2.2 Get请求
其实在前面的案例中已经使用过了,这里再次摘抄一下:
axios
.get('/api/mmdb/movie/v3/list/hot.json') // 去掉非同源域
.then(function (response) {
console.log(response) // 处理成功情况
})
.catch(function (error) {
console.log(error) // 处理错误情况
})
.then(function () {
console.log('finish') // 总是会执行
})
这种情况也就是直接请求,通常我们需要传入一些参数信息,可以按照下面的方式进行,这里以百度的为例,比如请求链接https://www.baidu.com/s?wd=React
:
export default function Test() {
axios
.get('/baidu/s', {
params: {
wd: 'React',
},
})
.then(function (response) {
console.log(respons)
})
.catch(function (error) {
console.log(error)
})
.then(function () {
console.log('finish')
})
return <div>Test</div>
}
对应的在setupProxy.js
文件中添加一个配置:
// 可配置多个
app.use(
'/baidu', // 代理路径
createProxyMiddleware({
target: 'https://www.baidu.com', // 真实地址
secure: true, // 如果是https接口 需要配置这个参数为true
pathRewrite: { '^/baidu': '' }, // 重写映射
changeOrigin: true,
})
)
这里重新添加了一个百度的映射。结果为:
当然其实get
方式的请求参数可以直接拼接到URL
链接中。
2.3 Post请求
类似的处理:
// https://passport.meituan.com/account/unitivelogin
axios
.post(
'/login/account/unitivelogin?risk_partner=0&risk_platform=1&risk_app=-1&uuid=1b6c5826fdad48a7833f.1638412798.1.0.0&token_id=DNCmLoBpSbBD6leXFdqIxA&service=maoyan&continue=https://www.maoyan.com/passport/login?redirect=%2F',
{
countrycode: '86',
email: '15128459509',
password: 'C7XRhr',
origin: 'account-login',
}
)
.then(function (response) {})
.catch(function (error) {})
因为这个接口的参数还挺多的,这里我就没有拷贝完全,可以自行抓包查看。请求最后因为非法到了重定向:
侧面也说明了其实Post
请求测试成功的。前面的这种配置方式为别名配置,即对应的请求方法get
、post
、head
、put
等。当然,也可以向 axios
传递相关配置来创建请求:
// 发起一个post请求
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});
比如:
function getMethod() {
axios({
method: 'get',
url: '/baidu/s',
params: { // get使用params,post使用data
wd: 'React',
},
})
.then(function (response) {
console.log(response)
})
.catch(function (error) {
console.log(error)
})
.then(function () {
console.log('finish')
})
}
在使用别名方法时, url
、method
、data
这些属性都不必在配置中指定。
2.4 同时多个请求
比如:
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
Promise.all([getUserAccount(), getUserPermissions()])
.then(function (results) {
const acct = results[0];
const perm = results[1];
});
2.5 更多请求参数配置
在使用爬虫的时候我们知道可以传入请求头部信息等,在axios
中也可以做相应的配置。
axios({
method: 'get', // 默认值
url: '/baidu/s',
baseURL: 'https://www.baidu.com', // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
data: {
// data` 是作为请求体被发送的数据,仅适用 'PUT', 'POST', 'DELETE 和 'PATCH' 请求方法
wd: 'React',
},
// 自定义请求头
headers: {},
timeout: 1000, // 默认值是 `0` (永不超时)
// `responseType` 表示浏览器将要响应的数据类型
// 选项包括: 'arraybuffer', 'document', 'json', 'text', 'stream'
responseType: 'json', // 默认值
responseEncoding: 'utf8', // 默认值
// `transformRequest` 允许在向服务器发送前,修改请求数据
// 它只能用于 'PUT', 'POST' 和 'PATCH' 这几个请求方法
// 数组中最后一个函数必须返回一个字符串, 一个Buffer实例,ArrayBuffer,FormData,或 Stream
// 你可以修改请求头。
transformRequest: [
function (data, headers) {
// 对发送的 data 进行任意转换处理
return data
},
],
// `transformResponse` 在传递给 then/catch 前,允许修改响应数据
transformResponse: [
function (data) {
// 对接收的 data 进行任意转换处理
return data
},
],
// `auth` HTTP Basic Auth
auth: {
username: 'janedoe',
password: 's00pers3cret',
},
})
当然还有很多千奇百怪的设置,完整可查看网站:请求配置
2.6 拦截器
可以在请求或响应被 then
或 catch
处理前拦截它们。比如,请求拦截器:
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
响应拦截器:
axios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
});
使用案例:
function getMethod() {
const myAxiosInstance = axios.create()
myAxiosInstance.interceptors.request.use(
function (config) {
console.log('Before')
return config // 在发送请求之前做些什么
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error)
}
)
myAxiosInstance({
method: 'get', // 默认值
url: '/baidu/s',
params: {
wd: 'React',
},
})
.then(function (response) {
console.log(response)
})
.catch(function (error) {
console.log(error)
})
.then(function () {
console.log('finish')
})
}
当然这里还是非同源域,所以还是需要在setupProxy.js
文件中配置代理。
2.7 请求体编码
默认情况下,axios
将 JavaScript
对象序列化为 JSON
。要以application/x-www-form-urlencoded
格式发送数据,您可以使用以下选项之一。
2.7.1 浏览器
在浏览器中,可以使用URLSearchParams API
,如下所示(qs):
import qs from 'qs';
const data = { 'bar': 123 };
const options = {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
data: qs.stringify(data),
url,
};
axios(options);
2.7.2 Node.js
const querystring = require('querystring');
axios.post('http://something.com/', querystring.stringify({ foo: 'bar' }));
更多的方式请查阅:请求体编码
3. axios底层逻辑
github
项目地址:https://github.com/axios/axios/
这里部分就来简单看下axios
的底层源码实现。注意到在github
的readme
文件中提到了使用CDN
引入的方式为:
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
所以在项目地址中,只需要关注于axios.js这个文件即可。在这个文件可以看到:
var Axios = require('./core/Axios');
所以其实真正的目录位于core/Axios.js文件中。
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
// 定义了原型
Axios.prototype.request = function request(config) {
...
// 验证config参数
// 设置请求方法
// 遍历拦截器链
var requestInterceptorChain = [];
var synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 分发请求
try {
promise = dispatchRequest(newConfig);
} catch (error) {
return Promise.reject(error);
}
...
// 配置请求别名
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: (config || {}).data
}));
};
});
}
// 导出Axios
module.exports = Axios;
可以看到一个简略的实现流程,在这个文件中主要完成的是对上层逻辑的一些封装,包括默认请求方法,请求配置,验证,拦截器遍历,分发请求,提供请求方法别名等。
这里因为主要关注于底层的实现,所以这里我就直接找到dispatchRequest(newConfig)
。注意到这个方法来自外部:
var dispatchRequest = require('./dispatchRequest');
所以这里就继续查看dispatchRequest.js这个文件。关注下面的代码即可:
module.exports = function dispatchRequest(config) {
...
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data
response.data = transformData.call(
config,
response.data,
response.headers,
config.transformResponse
);
return response;
}, ...
}
}
返回一个adapter
,并传入了配置。如果这个adapter
执行成功将返回一个response
。也就是说其实我们的目标进一步定位到了adapter
中。因为这里不清楚config
中存放了什么东西,所以可以追踪defaults.adapter
,注意到:
var defaults = require('../defaults');
可以查看defaults.js文件,可以看到:
adapter: getDefaultAdapter(),
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}
终于在这里看到了adapter
来自于xhr.js
文件,或者来自于http.js
文件。我们通过注释也知道二者是如果浏览器端就使用XMLHttpRequest
,如果是node
就使用node
。也就是核心文件为:
这里仅以xhr.js
为例。简略来说就是:
// 构建XMLHttpRequest对象
var request = new XMLHttpRequest();
// 使用open来打开链接
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// 注册回调监听
request.onabort = function handleAbort() ...
request.onerror = function handleError() ...
request.ontimeout = function handleTimeout() ...
References
References