1.什么是ajax请求
AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)。
AJAX 不是新的编程语言,而是一种使用现有标准的新方法。
AJAX 最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。
AJAX 不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行
Ajax就是用 JS 发起一个请求,并得到服务器返回的内容。这跟以前的技术最大的不同点在于「页面没有刷新」,改善了用户体验,仅此而已。
那么我们如何发送一个ajax请求呢?
1. 创建一个对象 XMLHttpRequest
var xhr = new XMLHttpRequest();为了支持ie6以及更早的版本,要 var xhr=new ActiveXObject()
2.监听请求成功后的状态变化
3.设置请求参数
4.发起请求
5.操作DOM,实现动态局部刷新
// 简单的ajax原生实现
var url = '请求url';
var result;
var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();
XHR.onreadystatechange = function() {
if (XHR.readyState == 4 && XHR.status == 200) {
result = XHR.response;
console.log(result);
}
}
接下来,我们就要监听请求成功的状态变化了
onreadystatechange:用来监听readyState的变化的
readyState:表示当前请求的后台的状态
status:表示处理的结果
其中readyState:表示当前请求的后台的状态
0:请求未初始化(还没有调用open())
1:请求已经建立,但是还没有发送(还没有调用send())
2:请求已经发送,正在处理中
3:请求正在处理中,通常响应中已经有部分数据可以用了
4:响应已经完成,可以获取并使用服务器的响应了
而status:表示处理的结果(状态码)
1XX,表示收到请求正在处理中
status == 200 是表示处理的结果是OK的
状态码:200到300是指服务端正常返回
304:如果网页自请求者上次请求后再也没有更改过,应将服务器配置为返回此响应,进而节省带宽和开销
404:找不到对象(404 not found)
503:服务器超时
设置请求参数
xhr对象接受三个参数
1:表示请求类型
2:表示请求的网址
3:表示是否异步
get/post/put/delete
Get和post方法的区别:
get是获取数据,get的send方法的参数可以是null或者空,对发送信息有限制,一般在2000个字符,一般是用来查询(幂等)
post可以发送数据,但是在使用post方法发送数据,需要使用setRequestHeader()来添加HTTP头,同时,post的send()方法需要写入要发送的数据的值, 一般用于修改服务器上的资源,对信息数量无限制,也更安全
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
application/x-www-form-urlencoded代表什么意思?
form的enctype属性为编码方式,常用有两种:application/x-www-form-urlencoded和multipart/form-data,默认为application/x-www-form-urlencoded。
x-www-form-urlencoded当action为get时候,浏览器用x-www-form-urlencoded的编码方式把form数据转换成一个字串(name1=value1&name2=value2…),然后把这个字串append到url后面,用?分割,加载这个新的url。
使用post提交需要忘记content-type的问题
4.解决方案
xhr.open("post", "/carrots-admin-ajax/a/login",true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("name=" + name + "&pwd=" + code);
Content-type要作为请求头放在open和send之间
2.回调地狱
在ajax的原生实现中,利用了onreadystatechange事件,当该事件触发并且符合一定条件时,才能拿到我们想要的数据,之后我们才能开始处理数据。
这样做看上去并没有什么麻烦,但是如果这个时候,我们还需要做另外一个ajax请求,这个新的ajax请求的其中一个参数,得从上一个ajax请求中获取,这个时候我们就不得不如下这样做:
var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
var result;
var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();
XHR.onreadystatechange = function() {
if (XHR.readyState == 4 && XHR.status == 200) {
result = XHR.response;
console.log(result);
// 伪代码
var url2 = 'http:xxx.yyy.com/zzz?ddd=' + result.someParams;
var XHR2 = new XMLHttpRequest();
XHR2.open('GET', url, true);
XHR2.send();
XHR2.onreadystatechange = function() {
...
}
}
}
当出现第三个ajax(甚至更多)仍然依赖上一个请求的时候,我们的代码就变成了一场灾难。这场灾难,往往也被称为回调地狱。
因此我们需要一个叫做Promise的东西,来解决这个问题。
当然,除了回调地狱之外,还有一个非常重要的需求:为了我们的代码更加具有可读性和可维护性,我们需要将数据请求与数据处理明确的区分开来。上面的写法,是完全没有区分开,当数据变得复杂时,也许我们自己都无法轻松维护自己的代码了。
当我们想要确保某代码在谁谁之后执行时,我们可以利用函数调用栈,将我们想要执行的代码放入回调函数中。
// 一个简单的封装
function want() {
console.log('这是你想要执行的代码');
}
function fn(want) {
console.log('这里表示执行了一大堆各种代码');
// 其他代码执行完毕,最后执行回调函数
want && want();
}
fn(want);
确保我们想要的代码压后执行,除了利用函数调用栈的执行顺序之外,我们还可以利用队列机制。
function want() {
console.log('这是你想要执行的代码');
}
function fn(want) {
// 将想要执行的代码放入队列中,根据事件循环的机制,我们就不用非得将它放到最后面了,由你自由选择
want && setTimeout(want, 0);
console.log('这里表示执行了一大堆各种代码');
}
fn(want);
如果浏览器已经支持了原生的Promise对象,那么我们就知道,浏览器的js引擎里已经有了Promise队列,这样就可以利用Promise将任务放在它的队列中去。
function want() {
console.log('这是你想要执行的代码');
}
function fn(want) {
console.log('这里表示执行了一大堆各种代码');
// 返回Promise对象
return new Promise(function(resolve, reject) {
if (typeof want == 'function') {
resolve(want);
} else {
reject('TypeError: '+ want +'不是一个函数')
}
})
}
fn(want).then(function(want) {
want();
})
fn('1234').catch(function(err) {
console.log(err);
})
看上去变得更加复杂了。可是代码变得更加健壮,处理了错误输入的情况。
3.promise
为了更好的往下扩展Promise的应用,这里需要先跟大家介绍一下Promsie的基础知识。
一、 Promise对象有三种状态,他们分别是:
- pending: 等待中,或者进行中,表示还没有得到结果
- resolved(Fulfilled): 已经完成,表示得到了我们想要的结果,可以继续往下执行
- rejected: 也表示得到结果,但是由于结果并非我们所愿,因此拒绝执行
这三种状态不受外界影响,而且状态只能从pending改变为resolved或者rejected,并且不可逆。在Promise对象的构造函数中,将一个函数作为第一个参数。而这个函数,就是用来处理Promise的状态变化。
new Promise(function(resolve, reject) {
if(true) { resolve() };
if(false) { reject() };
})
上面的resolve和reject都为一个函数,他们的作用分别是将状态修改为resolved和rejected。
二、 Promise对象中的then方法,可以接收构造函数中处理的状态变化,并分别对应执行。then方法有2个参数,第一个函数接收resolved状态的执行,第二个参数接收reject状态的执行。
function fn(num) {
return new Promise(function(resolve, reject) {
if (typeof num == 'number') {
resolve();
} else {
reject();
}
}).then(function() {
console.log('参数是一个number值');
}, function() {
console.log('参数不是一个number值');
})
}
fn('hahha');
fn(1234);
then方法的执行结果也会返回一个Promise对象。因此我们可以进行then的链式执行,这也是解决回调地狱的主要方式。
三、Promise中的数据传递
利用Promise的知识,对最开始的ajax的例子进行一个简单的封装
var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
// 封装一个get请求的方法
function getJSON(url) {
return new Promise(function(resolve, reject) {
var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();
XHR.onreadystatechange = function() {
if (XHR.readyState == 4) {
if (XHR.status == 200) {
try {
var response = JSON.parse(XHR.responseText);
resolve(response);
} catch (e) {
reject(e);
}
} else {
reject(new Error(XHR.statusText));
}
}
}
})
}
getJSON(url).then(resp => console.log(resp));
为了健壮性,处理了很多可能出现的异常,总之,就是正确的返回结果,就resolve一下,错误的返回结果,就reject一下。并且利用上面的参数传递的方式,将正确结果或者错误信息通过他们的参数传递出来。
现在所有的库几乎都将ajax请求利用Promise进行了封装,因此我们在使用jQuery等库中的ajax请求时,都可以利用Promise来让我们的代码更加优雅和简单。这也是Promise最常用的一个场景,因此我们一定要非常非常熟悉它,这样才能在应用的时候更加灵活。
四、Promise.all
当有一个ajax请求,它的参数需要另外2个甚至更多请求都有返回结果之后才能确定,那么这个时候,就需要用到Promise.all来帮助我们应对这个场景。
Promise.all接收一个Promise对象组成的数组作为参数,当这个数组所有的Promise对象状态都变成resolved或者rejected的时候,它才会去调用then方法。
var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
var url1 = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-03-26/2017-06-10';
function renderAll() {
return Promise.all([getJSON(url), getJSON(url1)]);
}
renderAll().then(function(value) {
console.log(value);
})
五、 Promise.race
与Promise.all相似的是,Promise.race都是以一个Promise对象组成的数组作为参数,不同的是,只要当数组中的其中一个Promsie状态变成resolved或者rejected时,就可以调用.then方法了。而传递给then方法的值也会有所不同,大家可以再浏览器中运行下面的例子与上面的例子进行对比。
function renderRace() {
return Promise.race([getJSON(url), getJSON(url1)]);
}
renderRace().then(function(value) {
console.log(value);
})
4.axios的功能优势
在vue项目中,和后台交互获取数据这块,我们通常使用的是axios库,它是基于promise的http库,可运行在浏览器端和node.js中。他有很多优秀的特性,例如
官方文档中的描述:
- Make XMLHttpRequests from the browser 通过浏览器发送XMLHttpRequest请求
- Make http requests from node.js node.js中发送http请求
- Supports the Promise API 支持Promise请求的API
- Intercept request and response 请求拦截和响应拦截
- Transform request and response data 转换请求和响应中的数据格式
- Cancel requests 取消请求
- Automatic transforms for JSON data 自动转换JSON数据格式
- Client side support for protecting against XSRF 客户端防御XSRF
所以vue官方果断放弃了对其官方库vue-resource的维护,直接推荐我们使用axios库。
5.为什么需要对axios进行再一次封装
使用vue-cli创建的项目中,我们需要模块化的集中统一处理ajax请求,比如统一进行请求和响应拦截处理,结合vue的特性可以实现全局的loading和登录控制以及身份验证等操作,通过将数据请求和数据处理进行拆分,达到模块化的目的,方便后期维护。
6.具体过程
项目封装完成代码结构图:
代码的结构说明,http文件夹存放的就是封装axios的代码,knowledge和menu文件夹存放的是业务模块的请求代码,
base.js存放的是项目请求地址分别分为测试地址和生产环境地址,common.js存放的是利用promise进一步封装的ajax请求,config.js存放的是后台返回的状态值不是状态码,
http.js就是封装axios的基础逻辑代码,index.js是存放导出模块的代码。
看到文章前面基础部分的讲解和代码注释以及熟悉vue全家桶技术,此处不在解释每个文件代码的作用,每个文件的源代码如下:
http.js
/* 封装axios */
import axios from 'axios';
import store from '../store/index';
let CancelToken = axios.CancelToken;
let cancel;
// 创建axios实例
let instance = axios.create({timeout: 5000});
// 设置post请求头
instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
/**
* 请求拦截器
* 简单的防止恶意程序批量刷接口
* 每次请求前进行时间判断处理,两次请求的时间间隔小于500ms就取消此次请求,大于500ms才发送
*/
instance.interceptors.request.use(
config => {
config.cancelToken = new CancelToken(function (c) {
cancel = c;
});
let time = new Date().getTime();
let storeTime = store.state.time;
let gapTime = time - storeTime;
if (gapTime < 500) {
cancel('请求过于频繁');
}
store.commit('setTime', time);
store.commit('showLoading');
store.commit('hideNetwork'); // 隐藏断网界面
return config;
},
error => {
Promise.error(error);
});
// 响应拦截器
instance.interceptors.response.use(
// 请求成功
res => {
store.commit('hideLoading');
store.commit('hideNetwork');
return res.status === 200 ? Promise.resolve(res) : Promise.reject(res);
},
// 请求失败
error => {
const {response} = error;
store.commit('hideLoading');
if (response) {
return Promise.reject(response);
} else {
store.commit('showNetwork'); // 显示断网界面
}
});
export default instance;
base.js
/**
* 接口域名的管理
*/
const base = {
cs: 'http://xxxxxxxxxxx:9999/api',
pd: 'http://xxxxxxxxxxxx:9999/api'
};
export default base;
common.js
/* 通用处理 */
import base from './base'; // 导入接口域名列表
import axios from './http'; // 导入http中创建的axios实例
const common = {
post (url, params) {
return new Promise((resolve, reject) => {
axios.post(`${base.cs}${url}`, JSON.stringify(params)).then(res => {
resolve(res.data);
}, err => {
reject(err.message);
});
});
},
get (url, params) {
return new Promise((resolve, reject) => {
axios.get(`${base.cs}${url}`, {
params: params
}).then(res => {
resolve(res.data);
}, err => {
reject(err.message);
});
});
}
};
export default common;
config.js
// 请求成功后台返回正确码
export const ERR_OK = 1;
// 请求成功但后台返回错误码
export const ERR_NO = -1;
menu.js 业务逻辑
/**
* 知识库接口列表
*/
import common from '../common';
const menu = {
// 获取菜单树形结构
getMenuTreeList (params) {
return common.post('/menu/getusermenu', params);
}
};
export default menu;
index.js
/**
* api接口的统一出口
*/
import knowledge from './knowledge/knowledge';
import menu from './menu/menu';
// 导出接口
export default {
knowledge,
menu
};
main.js中进行导入和挂载到vue的全局属性上
import api from 'http/index';
...
Vue.prototype.$api = api;
app.vue
....
<transition name="net">
<div v-if="netWork" class="network">
<div class="network-gif"></div>
<h3>你的网络情况好像不太好,请点击刷新试试吧............</h3>
<div class="network-refresh" @click="onRefresh">刷新</div>
</div>
</transition>
....
// js代码
import { mapState } from 'vuex';
import Loading from 'components/loading/Loading.vue';
export default {
name: 'App',
computed: {
...mapState([
'loading',
'netWork'
])
},
methods: {
onRefresh () {
this.$router.replace('/refresh');
}
},
components: {
Loading
}
};
refresh.vue 实现刷新的空界面
<template>
<div class="refresh">
</div>
</template>
<script type="text/ecmascript-6">
export default {
name: 'Refresh',
beforeRouteEnter (to, from, next) {
next(vm => {
vm.$router.replace(from.fullPath);
});
}
};
</script>
<style lang="stylus" rel="stylesheet/stylus" scoped>
</style>
至此axios的封装代码已经结束,下面是如何使用示例
示例代码:
// 组件中导入 import { ERR_OK } from 'http/config';
this.$api.menu.getMenuTreeList({login: 'admin'}).then(res => {
if (res.success === ERR_OK) {
this.menuList = res.menu;
}
}).catch(err => {
this.$Message.error(err);
});
7.源代码使用说明
如若转载文章请注明出处,文章链接https://blog.csdn.net/zhiy9ong/article/details/86569230,源代码可以自由参考复制使用。