一、概述
1、什么是axios?
axios是一个基于promise的HTTP库,可以用在浏览器和node.js中
2、axios有什么特性?
- 从浏览器中创建XMLHTTPRequests
- 从node.js中创建http请求
- 支持Promise API
- 拦截请求和响应
- 转换请求和响应数据
- 自动转换JSON数据
- 客户端支持防御XSRF
二、准备工作
- 首先新建一个mini-axios目录
- 在该根目录下新建一个server.js文件,内容如下:
var express = require('express')
var app = express()
var path = require('path')
// 读取静态资源路径
app.use(express.static(path.join(__dirname, 'src')))
app.all('*', function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Methods', '*');
res.header('Content-Type', 'application/json;charset=utf-8');
next()
})
app.get('/getTest', function (request, response) {
data = {
'fontEnd': '前端',
'suuny': 'zbq'
}
setTimeout(() => {
response.json(data);
}, 4000)
})
var server = app.listen(5000, function () {
console.log('*********server start*********')
})
- 新建一个src/interceptors.js、src/myAxios.js文件
- 新建src/index.html文件,内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button class="btn">点我发送请求</button>
<script type="text/javascript" src="./interceptors.js"></script>
<script type="text/javascript" src="./myAxios.js"></script>
<script>
document.querySelector('.btn').onclick = function() {
}
</script>
</html>
- 在当前目录下打开终端,执行npm init -y 初始化package.json文件
- 安装 express(npm i express)
- 执行npm start(启动server.js)
在浏览器中输入:http://localhost:5000/index.html 能正常访问
准备工作完成
三、实现一个简单的axios
1、实现axios与axios.method方法
1) 新建一个Axios类,并实现核心方法request方法(也就是ajax封装)
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 () {
console.log('a:', xhr.responseText)
resolve(xhr.responseText);
}
xhr.send(data)
})
}
}
2) 导出一个axios方法,通过查看源码,它实际上导出的是axios的request方法
function createInstance () {
var axios = new Axios()
var req = axios.request.bind(axios)
return req
}
var axios = createInstance()
export default axios
3) 在Axios原型上添加get、post等方法,这些方法内部实际上调用的还是Axios上的request方法
var methodsArr = ['get','delete', 'head', 'options', 'put', 'patch', 'post']
methodsArr.forEach(met => {
Axios.prototype[met] = function () {
if(['get','delete', 'head', 'options'].includes(met)) {
return this.request({
method: met,
url: arguments[0],
...arguments[1] || {}
})
} else {
return this.request({
method: met,
url: arguments[0],
data: arguments[1],
...arguments[2] || {}
})
}
}
})
4) 由于此时只有Axios.prototype才有get、post这些方法,但是导出的request方法没有怎么办呢?源码中是通过util.extend方法,将Axios.prototype中的方法直接copy到request上
var util = {
extend (a, b, context) {
for(let key in b){
if (b.hasOwnProperty(key)) {
if (typeof b[key] === 'function') {
a[key] = b[key].bind(context) // 运行request中copy过来的方法,方法中的this指向的还是axios实例
} else {
a[key] = b[key]
}
}
}
}
}
function createInstance () {
var axios = new Axios()
var req = axios.request.bind(axios)
// 新增
util.extend(req, Axios.prototype, axios) // 将Axios原型上的方法搬运到request中
return req
}
至此,axios(实际上是request),axios.method方法已经实现(不得不说这里用的实在巧妙了)
2、实现请求与响应拦截器
首先看一个拦截器的使用
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
什么是请求拦截器呢?拦截器其实就是在发送请求之前会先执行拦截器的代码,我们可以在拦截器中对请求的参数config做些处理。
响应拦截也是如此,在请求响应返回data数据后,会先直接响应拦截函数,我们可以处理放回的data数据
具体实现如下:
1) 首先新建一个拦截器Interceptors类
class Interceptors {
constructor() {
this.handlers = []
}
use (onResolved, onRejected) {
this.handlers.push({
onResolved,
onRejected
})
}
}
2) 将axios新增interceptors对象属性,并为它新增request、response属性,属性值均是Interceptors的实例。新增sendAjax方法,将request内容剪切过去
class Axios {
constructor() {
// 新增
this.interceptors = {
request: new Interceptors(),
response: new Interceptors()
}
}
request (config) {
}
sendAjax (config) {
return new Promise(resolve => {
const {url = '', method = 'get', data={}} = config
const xhr = new XMLHttpRequest()
xhr.open(method, url, true)
xhr.onload = function () {
console.log('a:', xhr.responseText)
resolve(xhr.responseText);
}
xhr.send(data)
})
}
....
}
3) 修改request方法,组装chain执行列表,实现如下:
- 新建一个chain,值为sendAjax函数,undefined
- 将请求拦截的onResolved, onRejected添加至chain最前边
- 将响应拦截的onResolved, onRejected添加至chain的最后 新建promise,遍历chain列表,组装promise执行串,保证执行他们的执行顺序
request(config) {
var chain = [this.sendAjax.bind(this), undefined]
// 请求拦截
this.interceptors.request.handlers.forEach(interceptor => {
chain.unshift(interceptor.onResolved, interceptor.onRejected)
})
// 响应拦截
this.interceptors.response.handlers.forEach(interceptor => {
chain.push(interceptor.onResolved, interceptor.onRejected)
})
var promise = Promise.resolve(config)
while (chain.length > 0) {
promise = promise.then(chain.shift(), chain.shift())
}
return promise
}
4) 由于只有Axiox构造函数才有interceptors拦截器的属性,因此需要将Axios构造函数属性搬运至request方法上
function createInstance () {
var axios = new Axios()
var req = axios.request.bind(axios)
util.extend(req, Axios.prototype, axios) // 将Axios原型上的方法搬运到request中
// 新增
util.extend(req, axios) // 将axios实例上的属性搬运至request中
return req
}
3、实现CancelToken
在实现之前,先看一下CancelToken的使用
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token // 表示CancelToken实例
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
})
source.cancel('不想请求了')
具体实现如下:
实现CancelToken类
实现步骤:
- 实例化一个Promise实例,并作为CanelToken实例的一个属性
- 将promise与resolve进行分离
- 将wrap函数作为参数放入executor中,当wrap被执行时,resove就会被调用
class CancelToken {
constructor (executor) {
this.message = ''
var resolvePromise
this.promise = new Promise ((resolve, reject) => {
resolvePromise = resolve
})
var token = this
executor(function wrap(message) {
if (token.message) {
console.log('已请求完毕,cancel不了')
return
}
token.message = message
resolvePromise(message)
})
}
}
- 实现CancelToken.source方法
实现步骤:
- 实现一个CancelToken实例,并赋值给token
- 为cancel赋值,cancel其实就是上文中wrap函数
- 将token、cancel作为对象的属性,并返回该对象
CancelToken.source = function () {
var cancel
var token = new CancelToken(function executor (c) {
cancel = c
})
return {
cancel,
token
}
}
其实它的设计思路就是采用promise和resolve分离的方式来实现的,然后将resove的执行权交给用户来控制,当resolve被执行,promise后的then回调(回调中有xhr.abort)就会被执行,然后就能终止接口请求
实现思路:首先将cancelToken传入至axios中的sendAjax方法中,当cancel函数被执行后(resolve就被执行了),就执行token.promise中的then回调,then回调就是为了执行XMLHttpRequest方法中的abort方法,用与终止接口请求。因此还要修改一下Axios中的sendAjax方法
sendAjax (config) {
......
// 新增
if (config.cancelToken) {
config.cancelToken.promise.then(function (cancelMessage) {
if (!xhr) {
return;
}
xhr.abort(); // 取消request请求
reject(cancelMessage)
xhr = null
})
}
四、完整代码
Interceptors类
// src/interceptors.js
class Interceptors {
constructor() {
this.handlers = []
}
use (onResolved, onRejected) {
this.handlers.push({
onResolved,
onRejected
})
}
}
CancelToken类
// src/cancelToken.js
class CancelToken {
constructor (executor) {
this.message = ''
var resolvePromise
this.promise = new Promise ((resolve, reject) => {
resolvePromise = resolve
})
var token = this
executor(function wrap(message) {
if (token.message) {
console.log('已请求完毕,cancel不了')
return
}
token.message = message
resolvePromise(message)
})
}
}
CancelToken.source = function () {
var cancel
var token = new CancelToken(function executor (c) {
cancel = c
})
return {
cancel,
token
}
}
Axios类与axios实例
// src/myAxios.js
class Axios {
constructor() {
this.interceptors = {
request: new Interceptors(),
response: new Interceptors()
}
}
request(config) {
var chain = [this.sendAjax.bind(this), undefined]
// 请求拦截
this.interceptors.request.handlers.forEach(interceptor => {
chain.unshift(interceptor.onResolved, interceptor.onRejected)
})
// 响应拦截
this.interceptors.response.handlers.forEach(interceptor => {
chain.push(interceptor.onResolved, interceptor.onRejected)
})
var promise = Promise.resolve(config)
while (chain.length > 0) {
promise = promise.then(chain.shift(), chain.shift())
}
return promise
}
sendAjax (config) {
return new Promise(resolve => {
const {url = '', method = 'get', data={}} = config
const xhr = new XMLHttpRequest()
xhr.open(method, url, true)
xhr.onload = function () {
console.log('a:', xhr.responseText)
resolve(xhr.responseText);
}
xhr.send(data)
// 取消request请求
if (config.cancelToken) {
config.cancelToken.promise.then(function (cancelMessage) {
if (!xhr) {
return;
}
xhr.abort();
reject(cancelMessage)
xhr = null
})
}
})
}
}
// 定义get,post...方法,挂在到Axios原型上
var methodsArr = ['get','delete', 'head', 'options', 'put', 'patch', 'post']
methodsArr.forEach(met => {
Axios.prototype[met] = function () {
if(['get','delete', 'head', 'options'].includes(met)) {
return this.request({
method: met,
url: arguments[0],
...arguments[1] || {}
})
} else {
return this.request({
method: met,
url: arguments[0],
data: arguments[1],
...arguments[2] || {}
})
}
}
})
var util = {
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]
}
}
}
}
}
function createInstance () {
let axios = new Axios();
let req = axios.request.bind(axios)
util.extend(req, Axios.property, axios) // 将Axios原型上的方法搬运到request中
util.extend(req, axios) // 将axios实例上的属性搬运至request中
return req
}
var axios = createInstance()
axios.CancalToken = CancelToken
测试一下
// src/index.html
<script type="text/javascript" src="./interceptors.js"></script>
<script type="text/javascript" src="./cancelToken.js"></script>
<script type="text/javascript" src="./myAxios.js"></script>
<script>
document.querySelector('.btn').onclick = function() {
// axios(config)测试
//axios({
// method: 'get',
// url: '/getTest'
//}).then(res => {
// console.log('getAxios 成功响应', res);
//})
axios.interceptors.request.use(function (config) {
config.method = 'get'
console.log("被我请求拦截器拦截了,哈哈:",config);
console.log('config', config)
return config;
}, function (error) {
return Promise.reject(error)
})
axios.interceptors.response.use(function (response) {
console.log('response拦截了, 哈哈')
response = {message:"响应数据被我替换了,啊哈哈哈"}
return response;
}, function (error) {
console.log('response错了么:', error)
return Promise.reject(error)
})
var CancelToken = axios.CancelToken
var source = CancelToken.source()
axios.get('/getTest', {
cancelToken: source.token // CancelToken的实例
}).then(res => {
console.log('getAxios 成功响应', res);
})
// 测试cancelToken可以将以下注释放开
//setTimeout(() => {
// source.cancel('不想请求了')
//}, 2000)
}
</script>
五、ajax、fetch和axios的区别
ajax
- 存在嵌套地狱问题,不利于代码维护
- 针对mvc模式编程,不符合前端mvvm的浪潮
- 不符合关注分离的原则
fetch
- fetch(号称是ajax的替代品)内部不是使用XMLHttpRequest对象,而是原生js,fetch的代码结构比ajax简单很多
- 更加底层,提供丰富的API(request、response)
- fetch只对网络请求报错,调用reject,对400,500等都当作成功的请求,因此需要封装去处理
- 默认不会带cookie,需要添加配置项, 如:fetch(url, {credentials: 'include'})
- 不支持abort,不支持超时控制,造成资源浪费
- 不能监测请求进度,而xhr可以
- fetch不兼容IE,其他的一些低版本浏览器也不兼容
axios
axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,它有如下特性:
- 从浏览器中创建XMLHTTPRequests
- 从node.js中创建http请求
- 支持Promise API
- 拦截请求和响应
- 转换请求和响应数据
- 取消请求
- 自动转换JSON数据
- 客户端支持防御XRSF axios既提供了并发的封装,也没有fetch的各种问题,而且体积也较小,当之无愧现在最应该选用的请求的方式。
六、其他
CSRF攻击
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。
解决办法:
1) 检查HTTP头上的检查 Referer 字段,一般请求的地址与Referer字段是位于同一个域名下的。当然这个方法也有局限性,因为攻击者也有可能直接去攻击浏览器,篡改其Referer字段
2) 同步表单CSRF校验 CSRF攻击之所以能成功,是因为服务器无法区分正常请求和攻击请求,因此针对这个问题,我们可以将CSRF token保存至表单的隐藏域中,当表单提交时,就可以一并提交了
3) 双重Cookie防御 就是将token保存只Cookie中,在提交(post、put, path, delete)等请求时,通过请求头或者请求体带上Cookie中已设置的token,服务器接收到请求后进行对比校验。
Axios就是通过双重Cookie,源码实现部分如下:
// lib/defaults.js
var defaults = {
adapter: getDefaultAdapter(),
.....
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
};
// lib/adapters/xhr.js
module.exports = function xhrAdapter(config) {
....
// 添加xsrf头部
if (utils.isStandardBrowserEnv()) {
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
...
};
XSS攻击
Cross-Site Scripting(跨站脚本攻击)简称XSS,是一种代码注入攻击。攻击者在目标网站注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如cookie,SessionId等,进而危害数据安全。
XSS 的本质是:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。 而由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,或者利用这些信息冒充用户向网站发起攻击者定义的请求。
解决办法:
1) 过滤用户输入的,检查用户输入的内容中是否有非法内容,如:<>(尖括号)、”(引号)、 ‘(单引号)、%(百分比符号)、;(分号)、()(括号)、&(& 符号)、+(加号)等。、严格控制输出。
2) 表单提交,或url参数传递前,需要对参数进行过滤
function safeStr(str){
return str.replace(/</g,'<').replace(/>/g,'>').replace(/"/g, """).replace(/'/g, "'");
}