Ajax - 异步,回调 & Promise

Ajax是Javascript的核心,在SPA(单页应用)中也获得广泛的使用。它是我们页面和服务端之间通讯的桥梁。虽然目前也有了新的fetch,但是实际上Ajax还是暂时没有离开我们的视线(还有很多面试官喜欢问)。所以在这篇文章中,我会通过下面几个section来说说当今前端Ajax的使用。

怎样创建一个Ajax调用

1
2
3
const xhr = new XMLHttpRequest() // IE已经退出历史帷幕,这里不再考虑ActiveXObject兼容
xhr.open(methodType, URL, async)
xhr.send()

上面三行代码就实现了一个发送给服务端的Ajax请求了。
1) 第一行创建了一个XMLHttpRequest的实体类,通过这个实体类我们可以发起XHR调用并且获得相应。
2) 第二行用来设置我们的请求,methodType用来设定请求的类型:POST,GET,PUT,DELETE等,GET/POST是最常见的。URL就是我们调用的服务地址,async设定是否是异步,默认为true,如果不为true,那么send()方法将不会return直到服务端有响应回访。
3) 最后一行就是实发出请求,浏览器会创建一个HTTPRequest并发送去服务端。xhr会在请求上保存所有的信息,例如HTTPConnection状态,HTTPResponse状态以及状态码。

如果需要两个Ajax调用,就要重复这三行代码(当然封装了就更好了)。

Ajax状态改变函数监听

为了获悉到实时的访问状态,我们需要监听onreadystatechange事件:

1
2
3
4
5
6
7
8
9
10
11
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log('reaceived the response', xhr.responseText)
} else {
console.log('error in processing the request')
}
} else {
console.log('waiting for the response finish')
}
}

当HTTPConnection状态发生改变的时候都会调用一次onreadystatechange,并且可以读取到xhr当前的状态readyState,主要状态如下:

0: 请求未初始化
1: 服务器连接已建立
2: 请求已接收
3: 请求处理中
4: 请求已完成,且响应已就绪

当状态为4的时候,就说明了响应结果已经返回了并且可以去判断请求的状态status(200,404,500等等)了。当xhr.readyState === 4 && xhr.status === 200的时候,就可以去读取xhr.responseText的内容了,注意的是,responseText的格式可能是XML,JSON,plain text,二进制或其他,这就要取决于客户端和浏览器之间的约定了。

完整的Ajax调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function ajaxCall(url, { methodType = 'GET' }) {
const xhr = new XMLHttpRequest()
xhr.open(methodType, url, true)
xhr.send()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log('xhr done successfully')
const resp = JSON.parse(xhr.responseText)
} else {
console.log('reqeust failed')
}
} else {
console.log('reqeust pending')
}
}
console.log('reqeust sent succesfully')
}

ajaxCall('https://test.com', { methodType: 'Get' })

那么POST请求呢

对于GET来说,参数可以直接加在URL上没难度,那么POST呢?
实际上xhr.send(...)的参数就是接受要传出去的参数,对于POST请求我们可以这么做:

1
2
3
... // 省略初始化
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded') // 设定POST请求的请求头格式
xhr.send(`id=${id}`) // 发送带参数的请求

我们需要回调

为了看起来方便,暂时我们也只考虑GET请求,也就是用回上面的完整代码。
当我们封装好ajax请求函数之后,我们需要一个机制,可以让函数通知我们请求已经结束了/发生异常了,首先我们选择JS的基础回调函数来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function ajaxCall(url, { methodType = 'GET' }, callback) {
const xhr = new XMLHttpRequest()
xhr.open(methodType, url, true)
xhr.send()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log('xhr done successfully')
const resp = JSON.parse(xhr.responseText)
callback && callback(resp)
} else {
console.log('reqeust failed')
callback && callback(xhr.statusText)
}
} else {
console.log('reqeust pending')
}
}
console.log('reqeust sent succesfully')
}

ajaxCall('https://test.com', { methodType: 'Get' }, function(res){
console.log(res)
})

这样就可以在我们正常的流程中获取到请求的返回结果了。

加上Promise更实用

很多时候,请求是嵌套的,或者需要其他耗时的操作,容易造成回调地狱,这时候就可以用Promise来帮助我们解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function ajaxCall(url, { methodType = 'GET' }, callback) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open(methodType, url, true)
xhr.send()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log('xhr done successfully')
const resp = JSON.parse(xhr.responseText)
resolve(resp)
} else {
console.log('reqeust failed')
reject(xhr.status)
}
} else {
console.log('reqeust pending')
}
}
console.log('reqeust sent succesfully')
})
}

let res = await ajaxCall('https://test.com', { methodType: 'Get' })

个人更喜欢使用async/await,来到这一步,一个异步的ajax请求函数(非完整)的到此结束了。

等等,跨域问题呢?

实际上,XMLHttpRequest应对跨域问题就两种解决方案:

  1. CORS,全称是“跨域资源共享”(Cross-origin resource sharing)。详细标准的内容可以看这篇文章
  2. JSONP,这个方案虽然可以支持更古老的浏览器,但是缺点是:
    1) 只支持`GET`请求
    2) 没有错误处理机制,发生错误的时候要么是跨域错误,要么是404连接错误。
    3) 脆弱性,因为对返回的代码绝对的信任,所以进一步暴露了CSRF漏洞。
    

因此建议还是使用第一种方案,当然这种方案更重要的是和后端同事沟通好。

总结

到这里未知,一个Ajax的内容就基本差不多了,而fetch相关的话可以看我的另一篇文章。好了到此结束。