JavaScript中的HTTP请求

还记得上大学那会做课程设计,我当时做了一个花卉网站,用java开发的,页面全是用JSP写的,还有那段时间特别流行的用PHP写Web页面。这些页面都是需要跟服务器端的代码部署再同一容器下的,页面中的动态内容都是在服务器端生成的,到了客户端只是展示而已。用这种方式开发网站并没有明确的前后端分工。

2005年AJAX概念的提出改变了这一切,它让客户端可以自行请求API接口,与服务器端交互,并且在不刷新页面的情况下更改网页中的局部内容,AJAX得以实现的核心就是XMLHttpRequest,现在各大浏览器厂商都支持了这一技术。

MDN上,对Ajax的描述如下:

AJAX是异步的JavaScript和XML(Asynchronous JavaScript And XML)。简单点说,就是使用 XMLHttpRequest 对象与服务器通信。 它可以使用JSON,XML,HTML和text文本等格式发送和接收数据。AJAX最吸引人的就是它的“异步”特性,也就是说它可以在不重新刷新页面的情况下与服务器通信,交换数据,或更新页面。

你可以使用AJAX最主要的两个特性做下列事:

  • 在不重新加载页面的情况下发送请求给服务器。
  • 接受并使用从服务器发来的数据。

2015年ES6的规范中又提出了Fetch API,结合Promise,可以更加方便地进行HTTP请求了。

本文主要学习总结一下XMLHttpRequestFetch的使用,开始吧~

XMLHttpRequest API

为了更好地了解XMLHttpRequest,可以看一遍XMLHttpRequest规范,以窥全貌。这里也有一份关于DOM的各种API文档,也可以查看XMLHttpRequest

基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
let httpRequest
if (window.XMLHttpRequest) {
httpRequest = new XMLHttpRequest()
} else {
// 兼容 IE 6或更老的浏览器
httpRequest = new ActiveXObject("Microsoft.XMLHTTP")
}

httpRequest.onreadystatechange = stateChange

httpRequest.open('GET', 'https://jsonplaceholder.typicode.com/users', true)

httpRequest.send()

function stateChange(evt) {
if (this.readyState === XMLHttpRequest.UNSENT) {
// 值为 0,httpRequest以被创建

} else if (this.readyState === XMLHttpRequest.OPENED) {
// 值为 1,open()方法被成功调用

} else if (this.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
// 值为 2,以遵循所有的重定向(如果有),已收到 Response Headers

} else if (this.readyState === XMLHttpRequest.LOADING) {
// 值为 3,已收到 Response body

} else if (this.readyState === XMLHttpRequest.DONE) {
// 值为4,数据传输已完成,或者发生了错误

}
}
  1. 创建一个XMLHttpRequest对象;
  2. 绑定状态改变时的回调方法;
  3. 调用open()方法,指定请求方式和请求的URL,最后一个参数指定请求过程是否异步的,true为异步,false为同步,默认是true。
  4. 调用send()方法,发起请求。

除了绑定readystatechange的回调来处理各种状态下要做的事,XMLHttpRequest还提供了单独的各种情况的事件处理回调。

event handler event handler event type 说明
onloadstart loadstart 开始请求时触发
onprogress progress 正在传输数据时触发,所以会被调用多次
onabort abort 请求被终止时触发,比如调用了abort()
onerror error 请求失败时触发
onload load 请求成功时触发
ontimeout timeout 请求超时时触发
onloadend loadend 请求完成时触发(成功或失败)

可以用事件绑定的方式设置事件被触发时的回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
httpRequest.onloadstart = loadStartHandler
httpRequest.onreadystatechange = stateChange
httpRequest.onload = loadHandler
httpRequest.onerror = errorHandler
httpRequest.onloadend = loadendHandler

httpRequest.send()

function loadStartHandler(evt) {
console.log('load started ...')
}

function loadHandler(evt) {
console.log('load succeeded ...')
// 获取返回数据
let jsonStr = this.responseText
}

function errorHandler(evt) {
console.log('load failed ...')
}

function loadendHandler(evt) {
console.log('load ended ...')
}

function stateChange(evt) {
console.log('state changed ...')
if (this.readyState === XMLHttpRequest.UNSENT) {
// 值为 0,httpRequest以被创建
} else if (this.readyState === XMLHttpRequest.OPENED) {
// 值为 1,open()方法被成功调用
} else if (this.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
// 值为 2,以遵循所有的重定向(如果有),已收到 Response Headers
} else if (this.readyState === XMLHttpRequest.LOADING) {
// 值为 3,已收到 Response body
} else if (this.readyState === XMLHttpRequest.DONE) {
// 值为4,数据传输已完成,或者发生了错误
}
}

注意,一定要先绑定Event Handler,然后再调用send(),避免Event Handler不会被触发。

也可以用事件监听的方式绑定Event Handler:

1
2
3
4
5
6
7
httpRequest.addEventListener('loadstart', loadStartHandler)
httpRequest.addEventListener('readystatechange', stateChange)
httpRequest.addEventListener('load', loadHandler)
httpRequest.addEventListener('error', errorHandler)
httpRequest.addEventListener('loadend', loadendHandler)

httpRequest.send()

设置请求的Headers

通过 setRequestHeader(header, value)可以设置请求头的字段和值,该方法必须在open()send()之间调用。如果多次对同一个请求头赋值,只会生成一个合并了多个值的请求头。例如:

1
2
3
4
5
httpRequest.setRequestHeader('X-Test', 'one')
httpRequest.setRequestHeader('X-Test', 'two')

// 该字段在请求头中的形式如下
// X-Test: one, two

处理响应

XMLHttpRequest提供了一些与响应有关的属性和方法,可以到规范中查看,其中下面几个是比较常用的:

status,用来判断响应状态。

responseType,响应数据的类型,是一个可读写的属性,允许创建XMLHttpRequest时设置它的值,常见的类型有:

Value Description
“” 默认类型,会视作和text时一样
“arraybuffer” 返回数据是包含二进制数据的ArrayBuffer
“blob” 返回数据是包含二进制数据的Blob对象
“document” 返回数据是一个 HTML 文档或 XML 文档
“json” 返回数据是 JSON Object
“text” 返回数据是字符串

response,成功发送请求后,该属性会包含 DOMString、ArrayBuffer、Blob 或 Document 形式(具体取决于 responseTyp 的设置)的请求数据。。

responseText,服务器返回的是字符串时,会用这个字段来接收。此时responseType为空字符串或text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xhr = new XMLHttpRequest()
xhr.open('GET', '/server', true)

// 如果要设置responseType,只能设置为空字符串,或者text
xhr.responseType = 'text'

xhr.onload = function() {
if (xhr.status === 200) {
console.log(xhr.response)
console.log(xhr.responseText)
}
}

xhr.send(null)

responseXML,服务器返回的是一个XML文档或HTML文档时,会用这个字段来接收。此时responseType为空字符串或document,响应头中的Content-typetext/htmltext/xmlapplication/xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var xhr = new XMLHttpRequest()
xhr.open('GET', '/server', true)

// 如果要设置responseType,只能设置为空字符串,或者document
xhr.responseType = 'document'

// 强制 XMLHttpRequest 将响应回来的数据按 XML 解析
xhr.overrideMimeType('text/xml')

xhr.onload = function() {
if (xhr.status === 200) {
console.log(xhr.response)
console.log(xhr.responseXML)
}
}

xhr.send(null)

发送数据

通过send()方法可以发送数据到服务器,send()接受的数据类型有:DOMString、Document、FormData、Blob、File、ArrayBuffer。但是当请求方式是GETHEAD时,发送的数据会被忽略。

html5rocks上的这篇文章:XMLHttpRequest2 新技巧给出了发送每种数据类型时的例子,讲的非常好,如果对于怎么发送FormData、Blob、File、ArrayBuffer等数据类型的数据不了解,建议看一看。

超时处理

通过XMLHttpRequesttimeout属性设置超时时间,单位是毫秒,然后绑定timeout触发时的回调,即可在超时后做一些事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xhr = new XMLHttpRequest();
xhr.open('GET', '/server', true);

xhr.timeout = 2000; // time in milliseconds

xhr.onload = function () {
// Request finished. Do processing here.
};

xhr.ontimeout = function (e) {
// XMLHttpRequest timed out. Do something here.
};

xhr.send(null);

监测进度

可以监听XMLHttpRequestprogress事件,获取上传或下载的进度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var xhr = new XMLHttpRequest()
xhr.open('GET', '/server', true)

var progressBar = document.querySelector('progress')
xhr.onprogress = function(e) {
if (e.lengthComputable) {
progressBar.value = (e.loaded / e.total) * 100;
}
}

// 监听上传进度
// xhr.upload.onprogress = function(e) {
// if (e.lengthComputable) {
// progressBar.value = (e.loaded / e.total) * 100;
// }
// }

xhr.send()

跨域请求

只要对方的域设置了允许当前站点发送AJAX请求,那么通过XMLHttpRequest发送跨域请求和普通请求就没什么区别。服务器端要开启 CORS 请求,需要在响应头中设置Access-Control-Allow-Origin字段。

1
2
3
Access-Control-Allow-Origin: http://example.com  // 允许example.com发过来的AJAX请求

Access-Control-Allow-Origin: * // 允许所有域发过来的AJAX请求

在发出 CORS 请求时,浏览器会自动在请求头中带上Origin字段,OriginAccess-Control-Allow-Origin的值一样,就可正常请求。

另外,CORS 请求默认是不会带上Cookie和HTTP认证信息的,如果要把Cookie发送给服务器,需要服务器同意,指定Access-Control-Allow-Credentials字段。

1
Access-Control-Allow-Credentials: true

在客户端创建XMLHttpRequest实例时,也需要指定withCredentials属性为true。

1
2
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。

Fetch API

基础用法

一个基本的fetch请求设置起来很简单:

1
2
3
4
5
6
7
8
9
10
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => {
return response.json()
})
.then(data => {
console.log(data)
})
.catch(error => {
console.log(error)
})

只有当网络故障或请求被阻止时才会将fetch返回的Promise会被reject,当收到一个代表错误的HTTP状态码时,fetch返回的Promise不会被reject,还是会走到resolve,所以最好先判断response.status,然后再继续后面的操作。利用Promise可以链式调用的特点,可以像下面这样封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function status(response) {
if (response.status >= 200 && response.status < 300) {
return Promise.resolve(response)
} else {
return Promise.reject(new Error(response.statusText))
}
}
function json(response) {
return response.json()
}
fetch('https://jsonplaceholder.typicode.com/users')
.then(status)
.then(json)
.then(data => {
console.log(data)
})
.catch(error => {
console.log(error)
})

Headers

我们可以将对Request Headers的设置集中到Headers实例上。

可以创建一个空的Headers实例,然后再通过appendhasgetsetdelete修改。

1
2
3
4
5
6
7
8
9
10
var headers = new Headers()

headers.append('Content-Type', 'text/plain')
headers.append('X-My-Custom-Header', 'CustomValue')

headers.has('Content-Type') // true
headers.get('Content-type') // 'text/plain'
headers.set('Content-Type', 'application/json')

headers.delete('X-My-Custom-Header')

也可以在创建Headers实例时就设置各字段:

1
2
3
4
var headers = new Headers({
'Content-Type': 'text/plain',
'X-My-Custom-Header': 'CustomValue'
})

为了能上headers,再创建一个Request实例:

1
2
3
4
5
6
7
var request = new Request('https://jsonplaceholder.typicode.com/users', {
headers: new Headers({
'Content-Type': 'application/json'
})
})

fetch(request).then(response => { //... })

Request

可以创建一个Request对象,直接传给fetch(),在Request对象中可以设置的字段主要有:

  • method - GETPOSTPUTDELETEHEAD
  • url - 请求的url
  • headers - Headers对象
  • referrer - 当前请求页面的来源页面的地址
  • mode - corsno-corssame-origin
  • credentials - 在请求时是否要带上cookie?omitsame-origin
  • redirect - followerrormanual
  • cache - 缓存模式(defaultreloadno-cache

每个字段的具体含义,以及其他更多可配置的Request信息,可以查看Fetch API规范

一个简单的Request对象:

1
2
3
4
5
6
7
8
9
10
var request = new Request('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
mode: 'cors',
redirect: 'follow',
headers: new Headers({
'Content-Type': 'application/json'
})
})

fetch(request).then(response => { //... })

第一个参数url是必传的,后面的字段都可以省略,这些字段都是只读的。

实际fetch()还接受第二个参数,可配置请求的相关信息,跟创建Request对象很像:

1
2
3
4
5
6
7
8
fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
mode: 'cors',
redirect: 'follow',
headers: new Headers({
'Content-Type': 'application/json'
})
})

可以在 MDN 上查看 fetch() 全部的可配置项。

Response

通过fetch()可以拿到Response对象,也可以手动创建Response对象,当使用Service Workers时可能会遇到需要手动创建Response对象的情况。Response的属性有:

  • type - basiccors
  • url
  • useFinalURL — Boolean类型,标识url是否是最终的URL
  • status — status code
  • ok - Boolean类型,标识响应是否成功(status在200~299)
  • statusText - HTTP状态码短语(ex: OK)
  • headers — 响应头

Response也提供了如下方法:

  • clone() — clone 一个Response对象
  • error() — 返回一个关联网络错误的Promise对象
  • redirect() — 用重定向URL创建一个新的Response对象
  • arrayBuffer() — 返回一个Promise对象,会将一个ArrayBuffer对象传递给then方法
  • blob() - 返回一个Promise对象,会将一个Blob对象传递给then方法
  • formData() — 返回一个Promise对象,会将一个FormData对象传递给then方法
  • json() — 返回一个Promise对象,会将一个JSON对象传递给then方法
  • text() — 返回一个Promise对象,会将一个USVString(text)传递给then方法

例如,加载图片:

1
2
3
4
5
6
7
fetch('/server/flower.jpg')
.then(response => {
return response.blob()
})
.then(imageBlob => {
document.querySelector('img').src = URL.createObjectURL(imageBlob)
})

发送数据

发送数据时,指定好请求方式,将要发送的数据放在fetch()第二个参数的body字段中,最好还能在headers中指定好Content-type

例如,发送JSON数据:

1
2
3
4
5
6
7
8
let params = { username: 'example' }
fetch('/server', {
method: 'POST',
body: JSON.stringify(params),
headers: new Headers({
'Content-type': 'application/json'
})
})

发送表单数据:

1
2
3
4
5
6
7
fetch('/server', {
method: 'POST',
body: 'foo=bar&lorem=ipsum',
headers: new Headers({
'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
})
})

发送Form Data:

1
2
3
4
fetch('/server', {
method: 'POST',
body: new FormData(document.getElementById('comment-form'))
})

跨域请求

如果对方的服务器支持 CORS 请求,那么就可以使用fetch()正常进行跨域请求。此外,fetch()有一个mode的只读属性可以设置请求的模式,如corsno-corssame-origin,这用于确定跨域请求是否能得到有效的响应,以及响应的哪些属性是可读的。

  • cors — 能发出跨域请求,并且能读取到部分reponse headers 和全部的body内容。
  • no-cors — 能发出跨域请求,但是会指示浏览器在任何情况下都禁止前端JavaScript代码读取另一个源返回的reponse headers 和 body。
  • same-origin — 指示浏览器只能去向相同的源发送请求,如果请求了另一个源,会报错。

参考资料

【1】 使用 XMLHttpRequest

【2】 XMLHttpRequest2 新技巧

【3】 跨域资源共享 CORS 详解

【4】 fetch API

【5】 使用 Fetch

【6】 Introduction to fetch()

本文作者:意林
本文链接:http://shinancao.cn/2019/08/15/JS-HTTP-Request/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!