说在前面:
传统方法的缺点:
传统的web交互是用户触发一个http请求服务器,然后服务器收到之后,在做出响应到用户,并且返回一个新的页面,每当服务器处理客户端提交的请求时,客户都只能空闲等待,并且哪怕只是一次很小的交互、只需从服务器端得到很简单的一个数据,都要返回一个完整的HTML页,而用户每次都要浪费时间和带宽去重新读取整个页面。这个做法浪费了许多带宽,由于每次应用的交互都需要向服务器发送请求,应用的响应时间就依赖于服务器的响应时间。这导致了用户界面的响应比本地应用慢得多。
什么是ajax?
ajax的出现,刚好解决了传统方法的缺陷。AJAX 是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。
一、 原生AJAX
AJAX:“Asynchronous Javascript And XML”(异步Javascript和XML),是指一种创建交互式应用的网页开发技术。
注意:
① AJAX不是新的编程语言,而是一种使用现有标准的新方法
② AJAX最大的有点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容
③ AJAX不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行。
1. XMLHttpRequest对象:用于在后台与服务器交换数据。意味着可以在不重新加载整个网页的情况下,对网页某部分进行更新。目前所有浏览器都支持XMLHttpRequest
五步使用XMLHttpRequest:
① 创建XMLHttpRequest对象:var obj = new XMLHttpRequest()
② 使用open方法设置请求方式和地址以及相关交互信息:obj.open(method,url,asyncFlag,user,name,callback)
。其中method
有post/get
两种方法,asyncFlag
为是否异步(默认为true,即默认为异步,若写false,浏览器不会报错,会报警告)。user
和name
有时用来确认身份。callback
可用来接收服务器返回的内容
③ 设置发送的数据,开始和服务器端交互:obj.send(data)
。若为get
方法,则数据在url后面,每个参数用&
连接,且数据和地址之间用?
连接(这里注意,使用GET
请求经常会发生的一个错误,就是查询字符串的格式有问题。查询字符串中每个参数的名称和值都必须使用encodeURIComponent()
进行编码,然后才能放到URL
的末尾)。若为post
方法,数据作为send
方法的参数。
④ 注册事件:onreadystatechange
属性 | 描述 |
---|---|
onreadystatuschange | 存储函数(或函数名),每当readyState 属性改变时,就会调用该函数 |
readyState | 存储XMLHttpRequest 的状态(0-4。0:请求未初始化。1:服务器连接已建立。2:请求已接收 3:请求处理中。4:请求已完成,且相应已就绪 |
status | 200:“ok”。404:未找到页面 |
⑤ 服务器响应:obj.responseText/obj.responseXML
属性 | 描述 |
---|---|
responseText | 获得字符串形式的相应数据 |
responseXML | 获得xml形式的相应数据 |
response |
HTTP头部信息
默认情况下,在发送XHR
请求的同时,还会发送下列头部信息
字段 | 解释 |
---|---|
Accept | 浏览器能够处理的内容类型 |
Accept-Charset | 浏览器能够显示的字符集 |
Accept-Encoding | 浏览器能够处理的压缩编码 |
Accept-Language | 浏览器当前设置的语言 |
Connection | 浏览器与服务器之间连接的类型 |
Cookie | 当前页面设置的任何Cookie |
Host | 发出请求的页面所在域 |
Referer | 发出请求的页面的URL |
User-Agent | 浏览器的用户代理字符串 |
通过XHR
对象的setRequestHeader
方法可以设置自定义的请求头部信息。这个方法接受两个参数:头部字段的名称和头部字段的值。要成功发送请求头部信息,必须调用oepn()
方法之后调用send()
方法之前调用setRequestHeader
。服务器在接收到这种自定义的头部信息之后,可以执行相应的后续操作(自定义头部字段最好不要和默认字段名称相同)
调用XHR
对象的getResponseHeader()
方法并传入头部字段名称,可以取得相应的响应头部信息。而调用getAllResponseHeaders()
方法则可以取得一个包含所有头部信息的长字符串。
二、跨域源资源共享
默认条件下,
XHR
对象只能访问与包含它的页面位于同一个域中的资源,这种安全策略可以预防某些恶意行为,但是,实现合理的跨域请求是必要的。
1. CORS
(Cross-Origin Resource Sharing
)
思想:使用自定义的
HTTP
头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。
CORS
(Cross-Origin Resource Sharing
)请求可以分成两种:
- 简单请求
- 复杂请求
简单请求
一个简单的请求大致如下:
HTTP方法是下列之一
HEAD
GET
POST
HTTP头包含
-
Accept
-
Accept-Language
-
Content-Language
-
Last-Event-ID
-
Content-Type
,但仅能是下列之一application/x-www-form-urlencoded
multipart/form-data
text/plain
任何不符合上述要求的(请求方法不是HEAD
、GET
以及POST
其中之一,自定义头部或Content-Type
不为上述三个之一),均可以看做 复杂请求。浏览器对简单请求和复杂请求的处理方式是有区别的。
简单请求说明:
例如:一个简单的GET
/POST
发送的请求,在发送该请求时,需要给它额外添加一个Origin
头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。
一个头部示例:
Accept: `*/*`
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Host: localhost:3000
Origin: http://localhost
Referer: http://localhost/
User-Agent: Mozilla/5.0 (Macintosh; .......
无论服务器是否任务该请求可以接受,都会正常地返回一个HTTP
回应。但是如果服务器认为这个请求可以接受(即请求页面的源信息在可允许范围内),返回的响应中会多出以下几个头信息字段(不一定每次都多如下几个字段,不需要的字段可以删除),其中在Access-Control-Allow-Origin
头部中回发相同的源信息(也可以发送通配符):
Access-Control-Allow-Origin: *//或http://localhost
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
上面的头信息之中,有三个与CORS
请求相关的字段,都以Access-Control-
开头。
Access-Control-Allow-Origin
:该字段是必须的。它的值要么是请求时Origin
字段的值,要么是一个*
,表示接受任意域名的请求。Access-Control-Allow-Credentials
:该字段可选。它的值是一个布尔值,表示是否允许发送Cookie
。默认情况下,Cookie
不包括在CORS
请求之中。设为true
,即表示服务器明确许可,Cookie
可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie
,删除该字段即可(在下面会讲到)。Access-Control-Expose-Headers
:该字段可选。CORS
请求时,XMLHttpRequest
对象的getResponseHeader()
方法只能拿到6个基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必须在Access-Control-Expose-Headers
里面指定。上面的例子指定,getResponseHeader('FooBar')
可以返回FooBar
字段的值。
如果服务器认为这个请求不可以接受,则不会返回这个头部(Access-Control-Allow-Origin
)或者返回的这个头部内容与源信息不匹配,浏览器便会驳回请求页面的请求,即提示报错,错误可以被XMLHttpRequest的onerror
回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。在这里需要注意的是:默认情况下发送请求过程中不能发送和接收cookie的,若需要,请继续向下看~
在这里可以这样理解:对于CORS
的简单请求,当页面发送了请求后,会经过浏览器的一层拦截过滤,默认情况下发送到服务端实际上是无cookie
以及无其他不可发送的信息的。当服务器方进行请求响应时,实际上也经过了浏览器的一层拦截过滤,当浏览器检查响应头Access-Control-Allow-Origin
字段发现页面请求可被接受则将响应内容作为请求的响应内容,否则报错且无响应内容。(自己的理解,如有错误麻烦指出~)
复杂请求
对于复杂请求,浏览器跟服务器之间会有一次预检请求,而简单请求时没有的。
复杂请求说明:
CORS
通过Preflighted Requests
的透明服务器验证机制支持开发人员使用自定义的头部、GET
或POST
之外的方法,以及不同类型的主题内容。在进行复杂请求时,会向服务器发送一个Preflight
请求,这种请求使用OPTIONS
方法,发送下列头部。
Origin:与简单请求相同
Access-Control-Request-Method:请求自身使用的方法
Access-Control-Request-Headers:(可选)自定义的头部信息,多个头部以逗号分隔
示例:
页面请求头部信息:
Origin:http://localhost
Access-Control-Request-Method:POST
Access-Control-Request-Headers:NCZ
发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通
浏览器返回的头部信息模板
Access-Control-Allow-Origin:与简单请求相同
Access-Control-Allow-Methods:允许的方法,多个方法以逗号分隔
Access-Control-Allow-Headers:允许的头部,多个头部以逗号分隔
Access-Control-Max-Age:应该将该预请求缓存多长时间(以秒表示)
对于上例中浏览器返回的头部信息
Access-Control-Allow-Origin:http://localhost
Access-Control-Allow-Methods:POST,GET
Access-Control-Allow-Headers:NCZ
Access-Control-Max-Age:1728000
Preflight
请求结束后,结果将按照响应中指定的时间缓存起来。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。
CORS
跨域发送凭证(cookie
、HTTP
认证以及客户端SSL
证明等)
需要满足以下三个条件:
- 在
open XMLHttpRequest
后,设置withCredentials
为true
即可让该跨域请求携带 凭证
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.withCredentials = true;
xhr.send();
- 需要在目标服务器接受跨域发送的凭证,否则会被浏览器的同源策略挡住:服务器同时设置
Access-Control-Allow-Credentials
响应头为true
, 即可允许跨域请求携带凭证。 - 跨域发送 凭证还要求
Access-Control-Allow-Origin
不允许使用通配符。 事实上不仅不允许通配符,而且只能指定单一域名,否则,浏览器还是会挡住跨域请求。
理解:
- 实际上,当没有设置
withCredentials
时,在请求中不会发送凭证。如果设置了withCredentials
但后端的响应头的Access-Control-Allow-Origin
不为true
,实际上凭证发送到了服务端且可以获取到相关内容,但由于响应头的Access-Control-Allow-Origin
不为true
,故浏览器会当作不可被服务端接受的请求,则会直接报错,即页面无法得到响应信息。 - 在复杂请求中,我们可以在客户端设置
withCredentials
为true
,服务器可以在Preflight
响应中设置Access-Control-Allow-Origin
为true
,即标识允许客户端发送带凭证的请求
2. 图像Ping
可以利用img
标签的跨域特性完成跨域请求。动态创建图像经常用于图像Ping
。图像Ping
是与服务器进行简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,但通常是像素图或204响应。通过图像Ping
,浏览器无法得到任何具体的数据,但通过侦听load
和error
事件,它能知道响应是什么时候接收到的。
图像Ping
最常用于跟踪用户点击页面或动态广告曝光次数。但它有两个缺点:① 只能发送GET
请求 ② 无法访问服务器的相应文本。故其只能用于浏览器与服务期间的单向请求
3. JSONP
思想:利用
<script>
元素的可跨域特性完成跨域请求
- 在页面上引入不同域的js脚本文件是可以的。jsonp正是利用这个特性来实现的。
- 通过script标签引入一个js文件,这个js文件载入成功后会执行我们在url参数中指定的回调函数(实际上是请求了远程的script文件,并服务端返回了执行该回调函数的js语句),并且会把我们需要的json数据作为参数传入。所以jsonp是需要服务器端的页面进行相应的配合的。
- 在使用jsonp跨域时必须要在发送请求前定义回调函数,否则找不到回调函数
eg:有个a.html页面,它里面的代码需要利用ajax获取一个不同域上的json数据,假设这个json数据地址是
http://example.com/data.php
,那么a.html中的代码就可以这样:
四个方案:
方案一:
- 服务端: 将要返回的数据,填充进一条字符串格式的js语句中,组成一条正确的可执行的js语句再返回。
- 客户端: 添加
<script src="服务器端地址">
结果: script可跨域请求到服务器返回的js语句,并在客户端立刻执行。
问题: 服务端返回的js语句是写死的。
方案二:
- 服务端: 返回一条自定义函数的调用语句,要求客户端必须执行指定名称的函数
- 客户端: 添加
<script src="服务器端地址">
并提前定义一个与服务端同名的函数,函数有一个参数可接受服务器端的数据, 函数内可执行任意操作
问题: 函数名是写死的,极容易发生冲突。
方案三:
- 服务端:接受一个函数名参数,将客户端发来的函数名参数拼接到js语句的开头(即返回执行函数名参数对应的函数的js语句)
- 客户端:<script src=“地址?参数名=本地函数名”
问题: script是写死的。只能在页面加载时请求一次
方案四:
- 服务端:接受一个函数名参数,将客户端发来的函数名参数拼接到js语句的开头(即返回执行函数名参数对应的函数的js语句)
- 客户端: 动态创建script元素,script只要被加到页面上,就立刻自动请求
问题: script不断累积
解决: 在回调函数中自动删除script
参考文章:
CORS 跨域发送 Cookie
CORS——一种新的跨域解决方案
从原理分析CORS——我们到底是怎么跨域的
跨域资源共享 CORS 详解
HTTP访问控制(CORS)