本文主要针对Ajax,Promise,Axios三者的本质、优缺点,使用实战做了阐述,抽象了应用办法,高度横向做了对比,一起进入学习吧~
一、Ajax
AJAX:异步 JavaScript 和 XML,用来发送异步请求。有了Ajax之后,在无需重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。
Ajax是基于现有的Internet标准,联合使用了XMLHttpRequest,JS/DOM,CSS,XML等技术。
它有个较大的缺陷就是业务逻辑需要在回调函数中执行,如果多个请求存在依赖关系,就会产生回调地狱问题,对读写理解困难。
1. 创建XHR
由于IE7以下版本不支持XMLHttpRequest对象,使用 ActiveXObject,因此需要做兼容处理:
let request;
// code for IE7+, Firefox, Chrome, Opera, Safari
if (window.XMLHttpRequest) {
request = new XMLHttpRequest();
} else { // code for IE6, IE5
request = new ActiveXObject("Microsoft.XMLHTTP"); // 新建Microsoft.XMLHTTP对象
}
2. XHR对象常用方法
(1)XMLHttpRequest.open()
初始化一个请求。已激活的请求再次调用此方法时,相当于调用了abort()
,会中断上一个请求。
当设置了async
(第三个参数)true
之后,请规定onreadystatechange
事件中的就绪状态执行响应函数。
(2)XMLHttpRequest.send()
向服务器发送http请求。如果open()
中定义的是异步请求,则此方法会在请求发起后立刻返回;如果是同步,则在请求返回后才执行。
(3)XMLHttpReqeust.abort()
当请求已经发出,则立刻中断请求。将readystate
设置为0
,立刻调用onreadystatechange
方法执行回调函数。
(4)XMLHttpRequest.setReqeustHeader()
用于给HTTP请求增加自定义请求头,在open()
之后,send()
之前定义。设置多个相同的请求头,在发起时会进行合并。
(5)XMLHttpRequest.getResponseHeader()
根据名称获取HTTP请求头,如果需要一次请获取全部,请使用getAllHeetResponseHeader()
方法。
3. 封装一个原生的Ajax请求
结合以上的内容,封装一个兼容Post和Get的Ajax请求如下:
ajaxRequest(method, url, data, callback) {
let xhr;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
if (method.toLocalCase() === "get") {
url = url + this.getParams(data);
xhr.open(method, url, true);
xhr.send();
} else { // post
xhr.open(method, url, true);
xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded");
data ? xhr.send(data) : xhr.send()
}
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.state === 200) {
// 约定返回字符串格式,如是xml格式使用responseXML
callback(xhr.responseText);
} else {
reject(new Error(xhr.statusText))
} else {
if (xhr.readyStatue === 0) {
alert("请求已取消");
}
}
}
}
buildGetParams(obj) {
if (!obj || obj.keys.length === 0) return '';
let str = Object.keys(obj).reduce(function(prev, cur, index) {
if (index === 1) {
prev = prev + '=' + obj[prev]
}
return prev + (prev ? '&' : '') + cur.toString() + '=' + obj[cur]
})
}
模拟使用post调用
this.ajaxRequest('post', 'http://demo-domain:8080/test/ajax_post',{ name: 'test' }, function(data) {
let result = JSON.parse(data)
console.log(result)
});
二、Promise
Promise对象由ES6提供,它表示一个尚未完成且预计在未来完成的异步操作。Promise 仅仅是一种决定异步任务状态确定时会出现什么结果的技术。它本身并不提供异步请求功能,更不能替代Ajax。
Promise将异步请求的回调操作,改成了同步的链式写法,最直观的就是使用Promise处理ajax请求时,可以解决回调地狱问题;另外,Promise封装了统一的接口,使得控制异步操作变得更加简单。
但它的缺点也比较明显,发起的Promise无法取消中断,其次,如果不设捕获异常的回调函数,Promise内部抛出的错误无法反应到外部出来。另外,未执行完成的Promise也无法确定是刚开始还是即将结束(即ajax的readyState在哪个过程)。
1. 创建Promise
Promise自身提供了封装好的Promise()
构造器,使用new
关键字创建。我们可以自定义一个异步函数,传入Promise的构造器如:Promise(asyncFn)
let promise = new Promise((resolve, reject) => {
// 执行结束需要使用resolve或reject将结果返回
})
2. Promise的基本API
Promise有三种状态,分别是 pending
:执行状态、fulfilled
:已成功、rejected
:已失败。
pending
只能改变为fulfilled
或rejected
其中一种,且状态一旦改变,将凝固不再改变。即使再次修改,该操作也会被忽略。
(1)Promise.prototype.then(onFulfilled, onRejected)
当Promise的状态凝固后,就会调用 .then
方法。该方法接受两个回调函数,即成功回调函数resolve()
,和失败回调函数reject()
,并且返回一个Promise。通常情况下,我们通过一定的条件判断如res.code
为1
表示成功,将结果作为参数即resolve(res.data)
传递出去。否则使用reject(res.message)
返回错误。
当然,.then()
的第二个参数非必选,你也可以通过 .catch()
API来实现
(2)Promsie.prototype.catch(onRejected)
该API表示Promise从pending
状态改变为rejected
状态时调用,接受一个失败回调函数,返回一个Promise,使用效果同.then()
的第二个参数。
(3)Promise.prototype.all()
该方法接受一个数组,用于将多个Promise包装成一个Promise来执行,只有所有的请求状态都凝固后才改变自己的状态。并且只有在全部请求都返回fulfilled
,该函数才返回 fulfilled
(4)Promise.prototype.race()
竟速模式,该方法接受一个可遍历对象,将多个Promise包转成一个Promise来执行,最先成功(fulfilled
)返回的请求当作该Promise的响应返回
......
3. 一种建议的Promise链式写法
虽然.then()
方法很强大,但并不建议在其中定义处理异常方法,原因是如果在.then()
的onFulfilled
回调函数中发生了异常,其内部定义的onRejected
是无法捕获到的。但是在.then()
后面使用.catch()
则可以捕获到之前发生的所有异常。一种健康的写法是:
function taskA () { ... };
function taskB () { ... };
function finalTask () { ... };
var promise = new Promise();
promise
.then(taskA)
.catch(onRejectedA)
.then(taskB)
.catch(onRejectedB)
.then(finalTask)
【注】不要使用 promise.then(taskA).then(taskB).then(finalTask).catch(onRejected)
,虽然这么定义所有的异常也都会被捕获到,但如果是taskA
发生了异常,那taskB,fianlTask
也将不被执行。
4. 使用Promise写法实现Ajax请求
为了简便以下只以Get请求为例,
getRequest(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('get', url, true);
xhr.send();
xhr.onreadystatechange() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(xhr.statusText))
}
}
}
})
}
// 调用
this.getRequest("http://demo-domain:8080?name=test").then(resp => {
console.log(resp)
})
三、Axios
Axios是一个基于Promise的http库,支持node端和浏览器端,支持拦截器、自定义请求头、取消请求等高级配置,支持自动转换JSON,也支持npm包的使用。总之,优点多多,用就对了...
针对Axios,在本文笔者不想写简单通用的功能,如有需要可以去axios官网学习了解。笔者想阐述更高效的用法如下
1. 自定义axios实例
使用自定义配置新建一个简单的axios实例
let request = axios.create({
baseURL: 'http://some-domain.com/api/',
timeout: 1000,
headers: { 'X-Custom-Header': 'footbar' }
})
2. 常用axios的请求配置
以下配置中,只有 url
是必须的,如果没有配置method
,将默认使用get
方法。以下为常用的配置,更多请参见官网。
-
url: '/user'
用户请求服务器资源的 URL -
method: 'post'
创建请求时使用的方法 -
baseURL: 'http://demo-domain:8080/api/'
自动加在URL(非绝对路径时)前的路径 -
headers: { 'x-Requested-With': 'XMLHttpRequest' }
自定义请求头 -
params: { 'ID': '12345' }
与请求一起发送的URL参数,当method指定为GET时使用 -
data: { 'name': 'zhangfs' }
请求主体参数,用于PUT,POST,PATCH方法 -
timeout: 8000
请求超时时间,0表示无超时;超过时间请求被中断 -
withCredentials: false
请求跨域时是否需要使用凭证 -
auth: { username: 'zhangfs' }
用于HTTP基础验证,将复写Authorization头 -
proxy: {
host: '127.0.0.1',
port: 9000,
}
代理服务器的主机名与端口,将设置Proxy-Authoriation -
cancelToken: new CancelToken(function(cancel) { ... })
指定取消请求的cancel token
全局默认配置
使用axios.defaults.xxx
来配置;注意在实例中配置的优先级高于默认配置优先级
let request = new axios({
baseURL: 'http://demo-domain.com:8080/api'
})
request.defaults.header.common['Authorization'] = AUTH_TOKEN
request.defaults.header.post['Content-Type'] = 'application/x-www-form-urlencoded'
3. 请求拦截器
在请求或响应被 then 或 catch 处理前拦截它们。如果自定义了axios实例如request,则使用 request.interceptors.xxx
// 添加请求拦截器
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);
});
当然,也可以对拦截器进行移除,此时需要对拦截器显命名
var myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);
4. 取消请求
ajax调用请求的abort()
通过改变readyState=0
来进行中断请求,axios则通过cancel token取消。
Axios 的 cancel token API 基于cancelable promises proposal,它还处于第一阶段。
可以使用 CancelToken.source
工厂方法创建 cancel token
var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
还可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token
var CancelToken = axios.CancelToken;
var cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});
// 取消请求
cancel();
值得注意的是,可以使用同一个cancel token取消多个请求。
5. 一个axios实战实例
请求封装文件service.js
const service = axios.create({
headers: {},
baseURL: Vue.prototype.configer.baseURL,
timeout: 8000,
withCredentials: true
})
service.interceptors.request.use(
config => {
if (config.method === 'get') {
if (config.url.indexOf('?') < 0) {
config.url += '?r=' + new Date().getTime()
} else {
config.url += '&r=' + new Date().getTime()
}
}
return config
},
error => {
return Promise.reject(error)
}
)
service.interceptors.response.use(
response => {
// 处理返回体
const result = response.data
const code = Number(result.code)
/**
* Desc: 接口请求业务异常统一处理
* Desc: 如:50008: 非法Token; 50012: 第三端登录; 50014: Token已过期;
*/
if (code === 3002 || code === 50008 || code === 50012 || code === 50014 || code === 302) {
alert('您已退出登录')
} else {
if (result.code && result.code !== '200') {
console.log(result.message)
}
return result
}
}, errorFn // HTTP Code异常统一处理
)
api封装文件 api.js
import service from '@/plugins/service'
import axios from 'axios'
const CancelToken = axios.CancelToken
export function queryDemoApi(data, _this) {
return request({
url: '/packageRoute/queryDemoApi',
method: 'post',
data,
cancelToken: new CancelToken(function executor(c) {
_this.cancelAjax = c
})
})
}
组件内使用 info.vue
data() {
return {
cancelRequest: null
}
}
method: {
pageTriggleSearch(username) {
if (typeof this.cancelRequest === 'function') {
this.cancelRequest()
}
this.queryInfoApi({ name: username}, this).then((data) => {
this.cancelRequest = null
// do something else
})
}
}