# Http、Ajax 和跨域
# 一、Http 请求概述
# 1.1 输入一个 URL 回车
在浏览器地址栏里输入一个 URL 然后按下回车,会发生以下事情:
- DNS 解析,建立 TCP 连接(三次握手),发送 Http 请求。
- server 接收 Http 请求,处理并返回。
- 客户端接收到返回数据后,处理数据(如渲染页面,执行 js)。
# 1.2 简单说明
打开浏览器,按下F12
会打开浏览器控制台,查看控制台的Network
页签。然后在浏览器地址栏里输入www.baidu.com
并回车。
DNS 解析:DNS(域名系统)拿到
https://www.baidu.com
,将它解析成一个IP 地址,例如180.101.49.11:443
。这个 IP 地址就是百度某个服务器的 IP 地址。443
是 https 的默认端口,80
是 http 默认端口。DNS 解析是 DNS 客户端向 DNS 服务器端发送一份查询报文也就是域名等,DNS 服务器端会返回一份报文也就是 IP 地址等。建立 TCP 连接:也就是三次握手。第一次握手,客户端询问服务端“你是否可用”;第二次握手,服务端告知客服端“我可用”;第三次握手,客户端告知服务端“我即将访问你”。
发送 Http 请求:大多发送的是 GET 或者 POST 请求,可以是 URL 直链请求也可以使用 ajax 异步发送请求。
server 处理请求:确认访问权限,处理好业务逻辑之后会返回响应。
// node.js处理请求 const http = require("http"); const server = http.createServer((req, res) => { console.log("req.method", req.method); console.log("req.url", req.url); res.end("hello"); }); server.listen(8000);
1
2
3
4
5
6
7
8接收响应:客户端处理响应,比如加载返回的 js、css 文件并执行它们。
# 二、XMLHttpRequest 对象
# 2.1 XHR 的使用
Ajax是Asynchronous JavaScript And XML
的缩写,表示异步的 JavaScript+XML。Ajax 是一门浏览器与服务器的通信技术,它的特点是无需刷新页面即可从服务器取得数据(请求然后响应最后修改 DOM,无需刷新页面),它的核心是XMLHttpRequest 对象(简称 XHR),前端人员可以使用这个对象进行服务器数据请求。需要注意的是,它名称虽然包含 XML,但其实与数据格式无关(可能是纯文本、XML、JSON)。
// 先实例化它,原生XMLHttpRequest,没有参数
var xhr = new XMLHttpRequest();
2
实例化 XMLHttpRequest 之后,我们就可以准备发送一个请求了,先配置请求信息(仅配置),再发送请求(建立连接)。
- 使用
xhr.open()
进行配置。该方法接收三个参数,第一个参数是什么类型的请求,比如是"GET"
还是"POST"
(最好都大写);第二个参数是请求地址,比如http://localhost/test.txt
;第三个参数是是否异步发送请求,一般是true
(可以不传,默认值就是true
)。 - 发送请求使用
xhr.send(param)
,GET 请求是没有param
的,POST 是有param
的,param
是请求主体发送的数据(request body)。
// 配置请求信息
xhr.open("GET", "http://localhost/test.txt", true);
// 发送请求。GET是没有param,POST是有param的,param是request body
xhr.send(param);
2
3
4
发送请求过后,因为一般是异步的,所以我们要知道请求/响应到了什么阶段,xhr 的readyState
属性就记录这些信息:
0
:未初识化。尚未调用open()
方法。常定义UNSENT = 0; // 初始状态
。1
:启动。已经调用open()
方法,但尚未调用send()
方法。常定义OPENED = 1; // open被调用
。2
:发送。已经调用send()
方法,但尚未接收到响应。常定义HEADERS_RECEIVED = 2; // 接收到response header
。3
:接收。已经接收到部分响应数据。常定义LOADING = 3; // 响应正在被加载
。4
:完成。已经接收到全部响应数据。常定义DONE = 4; // 请求完成
。
并且readyState
的值每次变化时都会触发readystatechange
事件。那前端人员就可以监听这个事件,再配合readyState
的为4
,就可以判断何时去处理响应。
xhr.onreadystatechange = function () {
// 已经接收到全部响应数据
if (xhr.readyState == 4) {
// 处理响应
}
};
2
3
4
5
6
浏览器收到服务器的响应后,会填充XHR 对象的属性。相关属性如下:
responseText
(新脚本是response
):作为响应主体被返回的文本(不管什么数据类型)。responseXML
(这是旧脚本):如果响应的内容类型是text/xml
或application/xml
,这个属性中将保存包含着响应数据的XML DOM
文档。status
:响应额 HTTP 状态。statusText
:HTTP 状态的说明,作为参考,一般不作为后续 js 判断。
前端人员需要在响应返回后,拿到这个 XHR 对象做校验和业务逻辑处理(处理响应)。校验就是判断状态码也就是status
属性,是[200, 300)
以及304
都是这次请求成功的标志。
// `[200, 300)`以及`304`是请求成功
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
2
3
4
5
6
如果在接收响应前想取消异步请求(终止请求),可以使用xhr.abort()
方法,来停止 XHR 对象的事件触发,并且不能访问到与响应有关的属性。
因为技术的进步,responseText
和responseXML
都比较老旧了,它们常被response
替代,并且还常使用xhr.responseType
来设置响应格式:
""
:同"text"
。"text"
:response 是 DOMString 对象中的文本。"arraybuffer"
:response 是一个包含二进制数据的 ArrayBuffer。"blob"
:response 是一个包含二进制数据的 Blob 对象。"document"
:response 是一个 HTML Document 或 XML XMLDocument,根据接收到的数据的 MIME 类型而定。"json"
:response 是通过将接收到的数据内容解析为 JSON,从而创建 js 对象。
这一节最后,我们写一个比较完整的例子:
// 配置请求信息
var xhr = new XMLHttpRequest();
// 配置请求信息
xhr.open("GET", "/blog/xmlhttprequest/json", true);
// 设置响应格式,我们用常用的json格式
xhr.responseType = 'json';
// 发送请求。GET是没有param
xhr.send()
// 监听readystate的变化,readystate为4去处理响应
xhr.onreadystatechange = function() {
// readystate为4,表示已经接收到全部响应数据,可以开始处理响应信息了
if (xhr.readystate == 4) {
// 响应状态码status为[200, 300)以及304是请求成功
if ((xhr.status >= 200 && xhr.status <300 ) || xhr.status == 304) {
// 写法一,这是旧脚本的写法
console.log('xhr.responseText', xhr.responseText);
// 写法二,这是新脚本的写法,返回的是json并且还转为了js对象
console.log('xhr.response', xhr.response);
} else {
console.log(('Request was unsuccessful: ' + xhr.status);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2.2 进度事件
进度事件(Progress Events)定义了与客户端服务器通信有关的事件:
progress
:在接收响应期间持续不断地触发。例如下载进度。error
:在请求发生错误时触发。例如网络中断或者无效的 URL。load
:在接收到完整的响应数据时触发。即使xhr.status
为400
或500
等。abort
:在因为调用abort()
方法而终止连接时触发。loadstart
:在接收到响应数据的第一个字节时触发。loadend
:在通信完成或触发error
、abort
或load
事件后触发。timeout
:请求超时。可以在请求发送前设置xhr.timeout=30000
。
前三个用的比较多,也经常使用load
事件来替代readystatechange
事件,当然内部还是继续搭配xhr.status
状态码判断返回结果。
progress
常用作进度指示器,监听的回调函数有个 event 参数,他有三个参数,lengthComputable
、position
和totalSize
,分别表示“进度信息是否可用”、“已接收的字节数”和“根据 Content-Length 响应头部确定的预期字节数”。后两个参数在现如今的 js 中应该是loaded
和total
了(意思还是一样的),position
和totalSize
可能或许还会存在。
var xhr = new XMLHttpRequest();
// 下载一个文件
xhr.open("GET", "/blog/xmlhttprequest/load", true);
xhr.send();
// 请求收到了完整的响应
xhr.onload = function () {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
// 写法一,这是旧脚本的写法
console.log("xhr.responseText", xhr.responseText);
// 写法二,这是新脚本的写法
console.log("xhr.response", xhr.response);
} else {
console.log("xhr.status", xhr.status);
console.log("xhr.statusText", xhr.statusText);
}
};
// 接收响应期间一直不断的触发该事件
xhr.onprogress = function (event) {
if (event.lengthComputable) {
// 写法一,这是旧脚本的写法
console.log(`Received ${event.position} of ${event.totalSize} bytes`);
// 写法二,这是新脚本的写法
console.log(`Received ${event.loaded} of ${event.total} bytes`);
}
};
// 请求失败,断网或者URL错误
xhr.onerror = function () {
console.log("Request failed");
};
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
# 2.3 头部信息和 POST 请求
每个 HTTP 请求和响应都会带有一个头部信息(请求头部和响应头部),我们来了解一下默认的浏览器头部信息:
Accept
:浏览器能够处理的内容类型。Accept-Charset
:浏览器能够显示的字符集。Accept-Encoding
:浏览器能够处理的压缩编码。Accept-Language
:浏览器当前设置的语言。Connection
:留恋其与服务器之连接的类型。Cookie
:当前页面设置的任何 Cookie。Host
:发出请求的页面所在域。Refere
:发出请求的页面的 URI。(HTTP 规范拼错了单词,本来应该是 referre,以规范为主)User-Agent
:浏览器的用户代理字符串。
我们不建议修改默认的浏览器头部信息,但可以使用自定义的头部信息,xhr.setRequestHeader(xxx, yyyy)
,xxx 是字段名,yyy 是值。要注意的是setRequestHeader()
方法必须得在open()
和send()
之间使用。
xhr.open("GET", "http://localhost/test.txt", true);
xhr.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);
2
3
我们可以获取响应头部信息,使用xhr.getResponseHeader(xxx)
获取单个,xhr.getAllResponseHeader()
获取全部响应头部信息。
GET 请求,是将简单传参数据放到了 URL 的末尾,但必须经过encodeURIComponent()
进行编码之后才能放到 URL 的末尾,所有的键值对儿都必须由&
进行分隔。
xhr.open('GET', 'example.php?name1=value1&name2=value2&name3=value3', true);
// 向现有URL的末尾添加查询字符串参数
function addURLParam(url, name, value)() {
// 含有?那就追加&,还不含有?那就加追?
url += (url.indexOf('?') == -1 ? '?' : '&');
// 添加?或&就可以添加键值对儿了
url += encodeURIComponent(name) + '=' encodeURIComponent(value);
return url;
}
2
3
4
5
6
7
8
9
POST 请求,是将较为复杂数据作为了请求的主体提交的,这些数据大概率会被服务器保存,属于重要数据。这些数据是放在send()
的入参里,数据格式通常是multipart/form-data
、JSON
、XML
。
特别注意,很久以前的表单数据传递要设置xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
。而在后来,定义了一个新类型FormData,简化了表单数据序列化,也不用再手动设置Content-Type
头部信息。
FormData 方式(其实就是multipart/form-data
):
// FormData序列化表单,创建与表单格式相同的数据
var data = new FormData(document.forms[0]);
// 追加新的键值对
data.append("name", "JavaScript");
var xhr = new XMLHttpRequest();
xhr.open("POST", "example.php", true);
// 使用FormData后无需设置Content-Type头部信息
// xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
// 发送请求
xhr.send(data);
2
3
4
5
6
7
8
9
10
JSON
形式:
var xhr = new XMLHttpRequest();
// 转为json字符串
var json = JSON.stringify({ name: "Bob", age: 8 });
xhr.open("POST", "example.php", true);
// 这里不要忘了,这不同于FormData,这个得手动设置
xhr.setRequestHeader("Content-type", "application/json; charset=utf-8");
xhr.send(json);
2
3
4
5
6
7
# 2.4 上传
上传请求比较特殊,它的上传进度不是xhr.onprogress
而是xhr.upload.onprogress
,并且其他常用的事件都是在xhr.upload
上。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>
<body>
<!-- 文件上传 -->
<input type="file" onchange="upload(this.files[0])" />
<script type="text/javascript">
function upload(file) {
var xhr = new XMLHttpRequest();
// 上传进度
xhr.upload.onprogress = function (event) {
console.log(`Uploaded ${event.loaded} of ${event.total}`);
};
// 请求收到了完整的响应
xhr.onload = function () {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
console.log("xhr.response", xhr.response);
} else {
console.log("xhr.status", xhr.status);
console.log("xhr.statusText", xhr.statusText);
}
};
// 请求失败
xhr.onerror = function () {
console.log("Request failed");
};
xhr.open("POST", "/blog/xmlhttprequest/upload");
xhr.send(file);
}
</script>
</body>
</html>
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
# 三、跨域资源共享
# 3.1 CORS
使用 Ajax 技术有个限制,那就是不能跨域进行请求(跨域也叫跨源),它只能访问相同域名、相同端口、相同协议的资源(同源)。
CORS(Cross-Origin Resource Sharing)是由 W3C 推出的一个基于 HTTP 头部的跨域资源共享技术。CORS 新增了一组的 HTTP 头部字段,存储着预检请求信息或源信息;CORS 也让服务器声明了哪些源站有权访问哪些资源(判断标准),接收请求时服务器会拿到源信息,并以这个标准作为判断依据;另外,对于非简单请求会先进行预检,拿到预检请求信息判断是否安全标准,在通过预检后才能进行实际的跨域请求。
# 3.2 简单请求
大多浏览器是通过XMLHttpRequest 对象实现了对 CORS 的原生支持。如果要检测浏览器的 XHR 是否支持 CORS,可以检测 XHR 对象上是否具有withCredentials
属性(不支持 IE10 以及以前版本)。
简单请求:请求方法要较为简单,自定义的头部信息较为安全和标准。也就是大部分情况满足下面条件即可(少部分去 MDN 查):
- 使用较为简单的请求方法,
GET
或HEAD
或POST
。 - 自定义头部字段
Accept
、Accept-Language
、Content-Language
、Content-Type
,这些字段的值要安全和标准。- 其中
Content-Type
字段值必须是text/plain
、multipart/form-data
、application/x-www-form-urlencoded
之一。 - 至于
Accept
、Accept-Language
和Content-Language
是什么标准,可以查:网站 1 (opens new window)、网站 2 (opens new window)、网站 3 (opens new window)。
- 其中
简单请求的 HTTP 头部有个非常重要的源信息,它是存储在Origin
字段里,它包含了请求页面的域名、端口和协议信息。如果服务器成功通过了这个请求,会在响应头部里使用Access-Control-Allow-Origin
回发相同的源信息;如果访问的是公共资源,能被任意外域访问,那Access-Control-Allow-Origin
得值就是'*'
。
function createCORSRequest(method, url, isAsyn) {
var xhr = new XMLHttpRequest();
// 检测XHR是否支持CORS,只需判断对象上是否有withCredentials属性
if ("withCredentials" in xhr) {
xhr.open(method, url, isAsyn);
return xhr;
}
return null;
}
// 这是一个简单请求
var request = createCORSRequest("GET", "http://bar.other/resources/public-data/", true);
if (request) {
request.onload = function () {};
request.send();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上面这个例子的简单请求的请求/响应报文可能如下:
// 请求报文 GET /resources/public-data/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X
10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding:
gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Connection: keep-alive Referer:
http://foo.example/examples/access-control/simpleXSInvocation.html Origin: http://foo.example // 请求的头部信息 //
响应报文 HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 00:23:53 GMT Server: Apache/2.0.61 Access-Control-Allow-Origin: * //
访问的是公共资源 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type:
application/xml [XML Data]
2
3
4
5
6
7
8
# 3.3 非简单请求
非简单请求就是除了上一节的简单请求以外的一些特殊 HTTP 请求,比如使用 PATCH 方法、DELETE 方法、MIME 类型的 POST 方法等。对于非简单请求,浏览器会先使用OPTIONS 方法对服务器发起一个预检请求,服务器对预检请求信息进行检查(因为这类请求比简单请求不安全不标准,所以要预检),预检通过后服务器才会允许该源站发送实际的跨源请求(那就可以继续进行非简单请求了),并且还告知该源站以后是否需要携带身份凭证(HTTP 认证相关数据和 cookie)。
预检请求的头部里依然有Origin
,除了Origin
还有两个重要字段Access-Control-Request-Method
和Access-Control-Request-Headers
。
Access-Control-Request-Method
是告知服务器之后的实际跨域请求将使用什么方法(PATCH?POST?)。Access-Control-Request-Headers
是告知服务器之后的实际跨域请求将携带什么自定义请求头部字段。
同样,服务器成功通过预检请求后,返回给浏览器的响应头部除了有Access-Control-Allow-Origin
之外,还有三个重要字段:
Access-Control-Allow-Methods
: 表明服务器允许客户端使用哪些方法。Access-Control-Allow-Headers
: 表明服务器允许携带那些自定义请求头部字段。Access-Control-Max-Age
: 是一个秒单位的数值,表明该非简单请求在这个时间内不用进行第二次预检了。
function createCORSRequest(method, url, isAsyn) {
var xhr = new XMLHttpRequest();
// 检测XHR是否支持CORS,只需判断对象上是否有withCredentials属性
if ("withCredentials" in xhr) {
xhr.open(method, url, isAsyn);
// 自定义了两个头部字段,并会体现在`Access-Control-Request-Headers`字段里
xhr.setRequestHeader("X-PINGOTHER", "pingpong");
xhr.setRequestHeader("Content-Type", "application/xml");
return xhr;
}
return null;
}
// 这是一个非简单请求
var request = createCORSRequest("POST", "http://bar.other/resources/post-here/", true);
if (request) {
request.onload = function () {};
request.send('<?xml version="1.0"?><person><name>Arun</name></person>');
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
预检的请求/响应报文
// 请求报文
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
// 源信息
Origin: http://foo.example
// 告知服务器,之后实际跨域请求,它使用的是POST方式
Access-Control-Request-Method: POST
// 告知服务器,之后实际跨域请求里,我会携带X-PINGOTHER和Content-Type这两个自定义头部字段
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
// 响应报文
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
// 源信息
Access-Control-Allow-Origin: http://foo.example
// 告知客户端,之后实际跨域请求可使用POST、GET、OPTIONS方法
Access-Control-Allow-Methods: POST, GET, OPTIONS
// 告知客户端,之后实际跨域请求可携带X-PINGOTHER和Content-Type这两个自定义头部字段
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
// 告知客户端,该非简单请求在24小时内不用进行第二次预检了
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
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
之后的实际跨域请求/响应报文:
POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: http://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: http://foo.example
Pragma: no-cache
Cache-Control: no-cache
<?xml version="1.0"?><person><name>Arun</name></person>
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
[Some GZIP'd payload]
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
# 3.4 带凭据的请求
默认情况下,对于跨域 XMLHttpRequest 请求,浏览器不会发送身份凭证信息。如果要发送(预检结果告诉你下次请求要带凭证),可以将withCredentials
属性设置为 true,让请求携带凭证信息传给服务器。服务器也接收带凭证的请求,会将Access-Control-Allow-Credentials:true
添加到响应头部信息里。
function createCORSRequest(method, url, isAsyn) {
var xhr = new XMLHttpRequest();
// 检测XHR是否支持CORS,只需判断对象上是否有withCredentials属性
if ("withCredentials" in xhr) {
xhr.open(method, url, isAsyn);
// 发送的请求里携带了身份凭证信息
xhr.withCredentials = true;
return xhr;
}
return null;
}
var request = createCORSRequest("GET", "http://bar.other/resources/credentialed-content/", true);
if (request) {
request.onload = function () {};
request.send();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
而浏览器发的请求带了凭证,但服务器没有Access-Control-Allow-Credentials
,那最终 js 会获取不到这个响应信息(response 为空),并会触发 onerror 事件。
# 四、其他跨域技术
# 4.1 图像 Ping
一个网页可以从任何网页中加载图像,不用担心跨域问题。动态创建图像,使用它们的 onload 和 onerror 事件处理程序是否接收到响应。
动态创建图像常使用图像 Ping,请求的数据是通过查询字符串形式发送的,而响应可以是任意内容(通常是像素图或 204 响应)。不过它只能发送 GET 请求,无法访问服务器的响应文本,也就是说它是单向的数据传输。
通过图像 Ping,浏览器得不到任何具体的数据,但通过侦听 load 和 error 事件,它能知道响应是什么时候接收到的。
var img = new Image();
img.onload = img.onerror = function () {
alert("Done!");
};
// 发送了一个name参数,图像Ping常用于跟踪用户点击次数
img.src = "http://www.example.com/test?name=Nicholas";
2
3
4
5
6
# 4.2 JSONP
JSONP(JSON with padding)是填充式 JSON 或参数式 JSON,讲一个回调函数追加到跨域访问的地址后面,再这个字符串放到<scipt>
标签的src
属性里,在页面加载这个<script>
时(和图像 Ping类似,可从任何网址加载脚本),会对服务器发起请求,当响应达到时,回调函数会被触发,回调函数里的参数就是 response 响应数据。
function handleResponse(response) {
console.log("response.ip", response.ip);
console.log("response.city", response.city);
console.log("response.region_name", response.region_name);
}
var script = document.reateElement("script");
// 将jsonp放入到跨域地址后面
script.src = "http://freegeoip.net/json?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);
2
3
4
5
6
7
8
9
JSONP相比于图像 Ping类,它能够直接访问响应文本(response),是双向的。但如果服务器不安全,响应中可能夹带恶意代码;还有确定 JSONP 请求是否失败并不容易,有可能需要定时器来监视。
# 4.3 Comet
Ajax 是页面向服务器请求数据,而Comet是一种服务器向页面推送数据的技术。Comet(服务器推送)有两种实现:长轮询和流。
短轮询(也叫常规轮询),浏览器定期发送请求给服务器,服务器会将目前为止的数据作为响应传递给浏览器(不管该数据是否有效)。服务器会一直被动接收请求,还得必须作出响应,尽管可能是无效的响应,这非常消耗服务器的性能。
而长轮询,浏览器发送一个请求,服务器虽然始终处于连接打开,但是没有新数据是不会推送数据给浏览器,直到有新数据可发送才会去推送。服务器掌握了主动权,只发送有效的数据,这很节省服务器资源。无论是短轮询还是长轮询,浏览器在接收数据之前,都会先发起对服务器的连接。
流就是 HTTP 流,它在页面的整个生命周期内只使用一个HTTP 连接。浏览器向服务器发送一个请求,服务器就会一直保持连接打开,并周期性向浏览器推送数据。(长轮询本质还是一个个 HTTP 连接,流就只有一个,且 readyState 大部分时间是3
)。流常用于服务器将输出缓存内容一次性全部发送给客户端。
function createStreamingClient(url, progress, finished) {
var xhr = new XMLHttpRequest();
var received = 0; // 处理了多少个字符
xhr.open("GET", url, true);
xhr.onreadystatechange = function () {
var result;
if (xhr.readyState == 3) {
result = xhr.responseText.substring(received);
received += result.length; // 每次递增
progress(result);
} else if (xhr.readyState == 4) {
finished(xhr.responseText);
}
};
xhr.send(null);
return xhr;
}
var client = createStreamingClient(
"streaming.php",
function (data) {
console.log("Received:" + data);
},
function () {
console.log("Done!");
}
);
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
管理 Comet 连接是很容易出错的,随着时间的推移,社区为 Comet 提供了两个新的 API 来完善以前的问题。
# 4.4 服务器发送事件
SSE(Serve-Sent Events,服务器发送事件)是用于创建服务器到浏览器的单向连接,服务器可以通过这个连接发送任意数量的数据。服务器响应的 MIME 类型必须是text/event-strean
,而且数据输出要能被 js 的 API 解析。
使用SSE时要订阅新的事件流,实例化 EventSource,并将一个 URL 作为第一个参数,如果要跨域那第二参数应该是{withCredentials: true}
。
EventSource 实例对象有个readyState
属性,值为0
表示正在连接,1
表示连接成功,2
表示连接关闭了。如果你非要关闭连接,可以使用close()
方法,但是你想重新使用EventSource,只能重新实例化EventSource(订阅新的事件流)。
EventSource 还有三个事件:
open
:在建立连接时触发。message
:在从服务器接收到新事件时触发,事件回调有个event
参数,该参数有个data
属性,该属性就是服务器返回的数据(字符串)。eror
:在无法建立连接时触发。
// 异步的EventSource
let eventSource = new EventSource("/events/subscribe", { withCredentials: true });
eventSource.onmessage = function (event) {
// json形式的:{"user":"Bob","message":"First line\n Second line"}
console.log("message", event.data);
};
2
3
4
5
6
响应格式可能是纯文本的,最简单的情况是每个数据项都带有前缀data:
,例如:
data: foo
data: bar
data: foo
data: bar
2
3
4
5
6
上面这样的文本,在之前代码的console.log("message", event.data)
,会输出三次,第一次是foo
,第二次是bar
,第三次是foo\nbar
。data:
是相邻两行,那么它们将作为一条 message 返回,中间会插入\n
换行符(对应服务器生成流时也只用一个\n
)。如果data:
之间有空行,它们会作为两条 message 返回(对应服务器生成流时用两个\n
)。
SSE支持短轮询、长轮询和 HTTP 流,而且能在断开连接时自动确定何时重新连接,服务器可设置延迟响应时间retry: 3000
。
为了正确地重新连接,每一条消息都应设置一个 id,具体是通过前缀id:
来给 message 添加 id 的,这个id: 1
紧挨着data:
上一行或下一行都可以。而浏览器收到带有 id 的 message 时,EventSource 实例对象会有个lastEventId
属性,它就是 id 的具体值。
id: 1
data: foo
data: bar
id: 2
id: 3
data: foo
data: bar
2
3
4
5
6
7
8
9
如果断开连接,会向服务器发送一个包含Last-Event-ID
字段的特殊头部的请求,以便服务器知道下一次该触发哪个事件(保证浏览器能接收到顺序正确的数据)。
# 4.5 Web Socket
Web Socket是在一个单独的持久连接上提供全双工、双向通信,http 协议升级到 web socket 协议(服务器要使用专门支持这种协议的服务器)。未加密的连接不再是http://
而是ws://
;加密连接也不再是https://
而是wss//
。
可以对比一下 WebSocket 和 EventSource:
WebSocket | EventSource |
---|---|
双向:客户端和服务端都能交换消息 | 单向:仅服务端能发送消息 |
二进制和文本数据 | 仅文本数据 |
WebSocket 协议 | 常规 HTTP 协议 |
WebSocket 协议能让客户端与服务端之间发送非常少量的数据,而不用担心 HTTP 那样字节级的开销。由于 WebSocket 传递的数据包很小,所以它非常适合移动应用,可以最大程度解决宽带和网络延迟的问题。
使用 WebSocket,得先实例化 WebSocket,并传入连接的 URL。同源策略对 WebSocket 不适用(没有跨域限制),因此可以通过它打开任何站点的连接。
实例化 WebSocket 后,浏览器会立马尝试创建连接。WebSocket 实例对象也有一个readyState
属性:
WebSocket.CONNECTING
,值是0
:正在建立连接WebSocket.OPEN
,值是1
:已经建立连接WebSocket.CLOSING
,值是2
:正在关闭连接WebSocket.CLOSED
,值是3
:已经关闭连接
WebSocket 还有 4 个事件:
open
:在成功建立连接时触发。message
:在接收到数据时触发。error
:在发生错误时触发,连接不能持续了。close
:在连接关闭时触发。
使用 WebSocket 发送数据是send(param)
方法,param 就是要发送的数据,param 只能是纯文本,如果是对象就要使用JSON.stringify(param)
进行转换(序列化)。使用 WebSocket 接收数据,在message
事件的回调函数中,该函数的参数event
,这个event
的data
属性就是返回数据,该数据是穿文本字符串,一般使用前要经过JSON.parse()
进行转换。
一个较为完整的例子:
var socket = new WebSocket("wss://www.example.com/server.php");
// 连接已建立
socket.onopen = function (e) {
// 因为连接的建立是异步的,所以得保证连接建立了才发送请求。
// 如果参数是对象,要转换为JSON字符串
socket.send(JSON.stringify({ name: "Bob" }));
};
// 接收到数据
socket.onmessage = function (event) {
// 返回的event.data是JSON字符串,需要转换为对象
var data = JSON.parse(event.data);
console.log(`接收到数据:${data}`);
};
// 连接关闭,当我们不再使用WebSocket可以关闭它:socket.close([code], [reason])
socket.onclose = function (event) {
if (event.wasClean) {
// 正常关闭,code一般为1000
console.log(`WebSocket连接正常关闭, code=${event.code} reason=${event.reason}`);
} else {
// 服务器进程被杀死或网络中断,event.code 通常为 1006
console.log(`WebSocket连接丢失, code=${event.code} reason=${event.reason}`);
}
};
// 连接发生错误
socket.onerror = function (error) {
console.log(`WebSocket发生了错误:${error.message}`);
};
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