第 6 章 Ajax
前言
本章是第六章Ajax相关的内容。
Ajax是一种流行的前后端数据交互的方式,通过异步请求就可以在不需要刷新页面的情况下,达到局部刷新的效果。
Ajax并非是一种全新的技术,而是由以下技术组合而成:
- 使用CSS和XHTML做页面呈现
- 使用DOM进行交互和动态显示
- 使用XMLHttpRequest对象和服务器进行异步通信
- 使用JavaScript进行绑定和调用,操作DOM
在学完后,希望掌握下面知识点:
- Ajax的基本原理及执行过程
- Nodejs搭建简易服务器
- Ajax提交form表单
- Ajax进度事件
- JSON序列化和反序列化
- Ajax跨域解决方案
6.1 Ajax的基本原理及执行过程
Ajax的基本原理是通过XMLHttpRequest对象向服务器发送异步请求,获取服务器返回的数据后,利用DOM的操作来更新页面。
下面是Ajax的执行流程:
其中最核心的部分就是XMLHttpRequest对象。它是一个JavaScript对象,支持异步请求,可以及时向服务器发送请求和处理响应,并且不阻塞用户,达到不刷新页面的效果。
下面重点讲解XMLHttpRequest对象相关知识点。
6.1.1 XMLHttpRequest对象
XMLHttpRequest对象从创建到销毁存在一个完整的生命周期,在生命周期的每个阶段会调用XMLHttpRequest对象的不同函数,在函数中需要通过XMLHttpRequest对象的特定属性来判断函数执行情况。
下面就会记录XMLHttpRequest对象的函数和属性
(1)XMLHttpRequest对象的函数
abort()
:如果请求已经发送,则停止当前请求getAllResponseHeaders()
:获取所有HTTP请求的响应头部,作为键值对返回;如果没有收到回应,则返回nullgetResponseHeader("key")
:获取特定key的HTTP响应头,没有收到响应或不存在则返回nullopen("method","URL","[asyncFlag]","[userName]","[password]")
:建立对服务器的调用。method表示请求方式,可以为GET
、POST
或PUT
;URL表示请求路径,绝对路径或相对路径均可;后面三个参数可选,表示是否异步、用户名、密码。asyncFlag=true表示异步,false表示同步,默认为true异步send(content)
:向服务器发送请求setRequestHeader("key","value")
:设置请求头属性为key的值为value。在设置请求头之前需要先调用open()
函数,设置的header将随着send()
函数一起发送
(2)XMLHttpRequest对象的属性
onreadystatechange
:状态改变的事件触发器。每个状态改变时都会触发这个事件处理器,通常会调用一个JavaScript函数readyState
:请求的状态。有 5 个可取的值:0
:未初始化,XMLHttpRequest对象已创建1
:open()函数已调用,send()函数未调用,请求还未发送2
:send()函数已调用,HTTP请求已发送到服务器,未收到响应3
:所有响应头接收完成,响应体开始接收但未完成4
:HTTP响应接收完成
responseText
:接受的数据文本格式的服务器响应体(不包括响应头)responseXML
:服务器的响应,兼容DOM的XML对象,解析后可得到DOM对象status
:服务器返回的HTTP状态码,用数字表示,如200表示成功,404表示资源未找到statusText
:HttP状态码的文本表示,如200时对应"OK",404时对应"Not Found"
6.1.2 XMLHttpRequest对象生命周期
由于浏览器的差异性,创建XMLHttpRequest对象时需要使用不同的方法,主要体现在IE浏览器与其他浏览器之间。
由于现在IE已经基本弃用,因此就不会再记录相关知识。
下面是一个标准的XMLHttpRequest创建方法。
(1)创建XMLHttpRequest对象
function createXMLHttp() {
var xmlhttp;
// code for IE7+, firefox, chrome, opera, safari
if (window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest();
}
// code for IE5, IE6 省略
return xmlhttp;
}
(2)建立连接
当XMLHttPRequest对象创建完毕后,便可以通过open()
函数建立连接,它指定了请求的url地址以及通过url传递的参数;数据传输方式,默认值为true,表示采用异步传输方式
var xhr = createXMLHttp();
xhr.open('post', '/admin/w/saceUser', true);
(3)发送请求并传递数据
在使用open()
函数建立连接后,便可以使用send()
函数发送请求,并传递数据content。由于传递的数据并不是必须的,所以content值可以为空
var content = {userName:'kingx', password:'123456'};
xhr.send(content);
(4)处理响应
在XMLHttpRequest对象中有一个很重要的onreadystatechange属性,它表示XMLHttpRequest对象状态改变的事件触发器,每次readyState的取值变化时,属性onreadystatechange对应的函数都会被执行一次。
当readyState的值为4时代表响应接收完成,需要注意的是响应接收完成并不代表请求是成功的,我们需要通过HTTP请求status状态码来判断,当status值为200时代表请求成功。
因此在onreadystatechange()
回调函数中,我们需要同时判断readyState和status两个值才能对响应值做正确的处理
xhr.onreadystatechange = function(){
// 当readyState为4,且状态码为200时代表请求成功
if (xhr.readyState === 4 && xhr.status === 200){
// 处理响应值
document.write(xhr.responseText);
}
}
6.1.3 Ajax的优缺点
(1)优点
- 可以再不刷新页面的情况下更新数据
- 异步通信
- 前后端分离
- 前后端负载分离
- 标准化支持
(2)缺点
- 破坏浏览器的正常后退功能
- 安全性问题
- 对搜索引擎支持较弱
- 违背URL唯一资源定位的初衷
6.2 使用Nodejs搭建简易服务器
这块不过多赘述
6.3 使用Ajax提交form表单
form表单默认提交方式会刷新页面,而且会在页面之间进行跳转。
使用Ajax提交form表单就可以在不刷新页面的情况下提交请求,然后在处理响应时通过JavaScript操作DOM,并展示后台处理的信息。
6.3.1 通用处理
使用Ajax提交form表单时,需要对form表单进行特殊处理,包括:
- 将form标签的action属性和method属性去掉
- 将提交form表单按钮的type='submit’改为type=‘btton’
下面是一个例子:
<form name="userForm" id="userForm">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" class="form-control" name="username" id="username">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" class="form-control" name="password" id="password">
</div>
<div class="form-group">
<label for="telphone">电话</label>
<input type="text" class="form-control" name="telphone" id="telphone">
</div>
<div class="form-group">
<label for="email">邮箱</label>
<input type="text" class="form-control" name="email" id="email">
</div>
<div class="text-center">
<input type="button" class="btn btn-default btn-primary" value="提交" id="submit">
<input type="button" class="btn btn-default" value="取消" id="cancel">
</div>
</form>
效果如下
6.3.2 使用原生Ajax进行提交
使用原生Ajax提交form表单包含以下过程:
- 绑定提交按钮事件
- 创建XMLHttpRequest对象
- 建立连接
- 设置请求头
- 获取数据
- 发送请求
- 处理响应
(1)绑定提交按钮事件
在单击提交按钮时触发Ajax请求的操作,将整个Ajax操作封装在ajaxSubmitForm()函数里。按钮获取与事件绑定使用原生的JS语法。
var submitBtn = document.getElementById('submit');
submitBtn.addEventListener('click',function(){
ajaxSubmitForm();
})
(2)创建XMLHttpRequet对象
同样是使用6.1.2小节中封装的函数即可
function createXMLHttp() {
var xmlhttp;
if (window.XMLHttpRequest){
xmlhttp = new XMLHttpRequest();
}
return xmlhttp;
}
var xhr = createXMLHttp();
(3)建立连接
本实例可以理解为一个用户的注册操作,发送的请求为POST秦秋,使用异步处理方式
xhr.open('post', '/saveUser', true);
(4)设置请求头
由于本实例中发送的是POST请求,需要设置数据传输格式,即设置Content-type属性值。可以通过setRequestHeader()
函数对其进行设置,将其值设置为比较普遍的 JSON数据格式
xhr.setRequestHeader('Content-type', 'application/json; charset=UTF-8');
(5)获取数据
通过原生的DOM操作方式获取页面填写的数据
var username = document.getElementById('username').value;
var password = document.getElementById('password').value;
var telphone = document.getElementById('telphone').value;
var email = document.getElementById('email').value;
var content = {
username: username, password: password, telphone: telphone, email: email
};
因为在请求头中设置了数据传输格式为json,所以需要将content对象处理为json字符串
content = JSON.stringify(content);
(6)发送请求
只需要调用send()
函数就可以发送请求
xhr.send(content);
(7)处理响应
设置onreadystatechange属性对应的回调函数,在回调函数中进行判断。当响应接收完毕,readyState为4,同时请求状态码status为200时,即表示请求成功,然后就可以编写对应的处理逻辑
xhr.onreadystatechange = function () {
// 当readyStatew为4,且状态码为200时代表请求成功
if (xhr.readyState === 4 && xhr.status === 200) {
// 处理响应值
document.write(xhr.responseText);
}
}
在使用原生Ajax提交form表单内容时,需要考虑浏览器兼容性问题,并且该方式的代码冗余度高,需要经常进行状态的判断,因此这并不是一种很好的处理form表单的方式。
6.3.3 使用jQuery处理Ajax请求进行提交
使用jQuery处理Ajax请求,解决了浏览器兼容性的问题,对原生Ajax请求的高度封装也使得代码变得精简。我们只需要关注在使用Ajax时需要什么,然后传递对应的参数,处理不同的回调即可。
但因为也不用,所以不记录了。
6.3.4 使用jQuery序列化form表单进行提交
表单的序列化,表示的是可以自动将表单内填写的内容自动处理为字符串或者对象格式,便于与服务端进行传递,从而避免重复性地通过代码获取单个表单元素输入值。
同样因为不用,不记录了。
6.3.5 使用FormData对象进行提交
FormData对象是HTML5中新增的对象,服务于Ajax请求,用于发送数据。
FormData对象将数据编译成key-value类型的键值对,以便于XMLHttpRequest对象发送数据。其主要用于发送form表单数据,但也可以独立于form表单,发送带有键的数据。
FormData对象提交的最大的优势是可以异步上传文件。
FormData对象的提交既可以支持原生Ajax请求,也可以支持jQuery请求。jQuery就不记录了。
下面记录一下原生Ajax请求使用FormData对象发送form表单数据:
其他部分,例如XMLHttpRequest对象的创建和请求发送,以及请求成功的回调,这里就不做详细描述,我们重点来看FormData对象的使用
(1)请求头设置
使用原生Ajax请求发送带有文件流的FormData数据时,需要对请求头进行对应的设置,即将Content-type属性设置为application/x-www-form-urlencoded
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
(2)生成FormData实例
var formData = new FormData();
(3)添加数据
通过append()函数向formData对象中添加需要发送的数据,不管是简单的文本类型数据还是文件类型数据,都可以添加到formData对象中
(4)发送数据
调用send()函数传递FormData对象
xhr.send(formData);
6.4 关于Ajax请求的get方式和post方式
Ajax请求通常会有get和post两种方式
6.4.1 get方式和post方式的区别
使用get和post都可以向服务器发送请求,但是发送机制不同:
- 参数传递
- get请求:将参数添加到请求URL的后面,没有请求主体,调用
send()
函数时传递的参数为null,即xhr.send()
- post请求:会将请求的数据放在请求体中,用户是无法通过URL直接看到的,调用
send()
函数时传递的参数为data,即xhr.send(data)
- get请求:将参数添加到请求URL的后面,没有请求主体,调用
- 服务端参数获取:使用Express作为服务端框架,get请求通过
Request.query
来获取参数;而使用 post请求时需要添加中间件,同时通过Request.body
来获取参数 - 传递的数据量:get请求传输的数据量小;post请求传递的数据量大
- 安全性
- get请求:安全性较低,因为其请求的参数会出现在URL上,而且采用明文进行数据传输,通过浏览器缓存或者历史记录可以很容易获取到某些隐私请求的参数
- post请求:安全性较高,post请求通过请求体进行数据传输,数据不会出现在URL上,隐藏了请求数据的信息
- 处理form表单的差异性
6.4.2 使用get方式和post方式需要注意的点
- 使用get方式请求时,如果请求的url不发生改变,可能会存在缓存的问题,因此在请求的url后一般会拼接上一个时间戳,以避免出现缓存
- 使用get方式请求时,请求的参数会拼接在url后,如果浏览器编码、服务器编码、数据库编码格式不一致,可能会导致乱码的问题。通常的做法是对请求的参数经过
encodeURIComponent()
函数处理
xhr.open('get', '/getUser?username='+encodeURIComponent(username), true)
- 使用post方式请求时,需要设置请求头中的content-type属性,表示数据在发送至服务器时的编码类型。默认情况下,使用post方式提交form表单时,content-type值为application/x-www-form-unlencoded,另外还可以支持multipart/formdata、application/json等格式
xhr.setRequestHeader('content-type','application/x-www-form-urlencoded')
6.4.3 get方式和post方式的使用场景
(1)Ajax使用get方式的场景
- 请求是为了检索资源,form表单的数据仅用于帮助搜索
- 传递的数据量小,适合于url中传递参数
- 数据安全性低,适合明文传输
(2)Ajax使用post方式的场景
- 请求会修改数据库中的资源,例如增删改
- 传递的数据量大,超出url中携带参数长度的限制
- 用于用户名、密码及身份证号等类似敏感信息的数据传输
6.5 Ajax进度事件
在之前的内容里,我们有讲到通过监听readystatechange事件,在回调函数中获取readyState和status的值并判断请求是否成功。在XHR2草案中,增加了Ajax请求进度事件Progress Events规范,使得XMLHttpRequest对象能在请求的不同阶段触发不同类型的事件,所以我们可以不再需要判断readyState的属性,也可以处理请求成功和失败的操作。
在Progress Events规范中增加了 7 个进度事件:
loadstart
:在开始接收响应时触发progress
:在接收响应期间不断触发,直至请求完成error
:在请求失败时触发abort
:在主动调用abort()
函数时触发,表示请求终止load
:在数据接收完成时触发loadend
:在通信完成或者error、abort、load事件后触发timeout
:在请求超时时触发
一个完整的ajax请求都会从loadstart事件开始,然后不间断地触发progress事件,然后触发load、abort、timeout或者error事件的其中一个,最后触发loadend事件。
(1)load事件
只要浏览器接收到了服务器的响应,不管其状态如何都会触发load事件。例如,对于状态码为404的请求,仍然会触发load事件,所以在进行请求成功的处理时,需要判断status的值。一般我们判断status值大于等于200且小于300,或者status值等于304时,都是当作请求成功进行处理。
在loadstart、load等事件的回调函数中,都会接收一个event对象,通过event对象的target属性可以获取到XMLHttpRequest对象的实例,因此可以访问到XMLHttpRequest对象的所有属性和函数
(2)progress事件
progress事件会在浏览器接收数据的过程中周期性调用。progress事件处理程序会接收一个event对象,通过它的target属性同样可以获取到XMLHttpRequest对象的实例,而且在event对象中增加了3个有用的属性,分别是lengthComputable、loaded和total
lengthComputable
:表示进度信息是否可用。是一个布尔值loaded
:表示已经接收到的字节数,它的值是根据Content-Length响应头部确定的预期字节数total
:表示响应的实际字节数
通过loaded和total属性值可以计算出接收响应的数据百分比,从而实现进度条
6.6 JSON序列化和反序列化
JSON数据在网络传输时存在两种类型,一种是JSON对象类型,一种是JSON字符串类型。两种类型的转换涉及JSON序列化和反序列化的知识。
6.6.1 JSON序列化
JSON序列化即将JSON对象处理为JSON字符串的过程,以方便数据的传输。
有两种方式实现:
- 调用JSON对象内置的stringify()函数
- 为对象仔细定义toJSON()函数
(1)JSON.stringify()函数
JSON.stringify(value[,replacer[,space]])
value
:待处理成JSON字符串的JavaScript值,通常为对象或者数组replacer
:是一个可选参数。如果是一个函数,则表示在序列化过程中,被序列化值的每个属性都会经过该函数处理;如果为一个数组,则表示只有包含在这个数组中的属性名才会被序列化到最终JSON字符串中;如果为null或未传递,则value参数对应值的所有属性都会被序列化space
:是一个可选参数,用于指定缩进用的空白字符串,美化输出。如果是数字,则代表对应个数空格,上限为10;如果小于1则没有空格;如果为字符串则取字符串的前10个字符作为空格;如果为null或未传入则没有空格
在JSON序列化时,如果属性值为对象或者数组,则会继续序列化该属性值,直到属性值为基本类型、函数或者Symbol类型才结束。
(2)自定义toJSON()函数
如果一个被序列化的对象拥有toJSON()函数,那么toJSON()函数就会覆盖默认的序列化行为,被序列化的值将不再是原来的属性值,而是toJSON()函数的返回值。
toJSON()函数用于更精确的控制序列化,可以看作是对stringify()函数的补充。
(3)序列化处理的顺序
- 如果待序列化的对象存在toJSON()函数,则优先调用toJSON()函数,以toJSON()函数的返回值作为待序列化的值,否则返回JSON对象本身
- 如果stringify()函数提供了第二个参数replacer,则对上一步的返回值经过replacer参数处理
- 如果stringify()函数提供了第三个参数,则对JSON字符串进行格式化处理,返回最终的结果
6.6.2 JSON反序列化
JSON反序列化即将JSON字符串转换为JSON对象的过程,得到的结果用于在JavaScript中做逻辑处理。
有两种方式实现:
- 使用JSON对象内置的parse()函数
- 使用eval()函数
(1)JSON.parse()函数
JSON.parse()函数用来解析JSON字符串,构造由字符串描述的JavaScript值或对象
JSON.parse(text[,reviver])
text
:带解析的JSON字符串reviver
:是可选参数。如果是一个函数,则规定了原始值在返回之前如何被解析改造。如果被解析的JSON字符串是非法的,则会抛出异常
JSON.parse('[1,2,3,true]'); // Array [1, 2, 3, true]
JSON.parse('{"name":"小明","age":14}'); // Object {name: '小明', age: 14}
JSON.parse('true'); // true
JSON.parse('123.45'); // 123.45
当使用JSON.parse()函数解析JSON字符串时,需要注意两点;
- JSON字符串中的属性名必须用双引号括起来,否则会解析错误
- JSON字符串不能以逗号结尾,否则会解析异常
(2)eval()函数
eval()函数用于计算JavaScript字符串,并把它作为脚本来执行。
在使用eval()函数进行JSON反序列化时,其语法如下所示:
eval("(" + str + ")")
str
:待处理的字符串
因为JSON字符串是以{}
开始和结束的,在JavaScript中它会被当作一个语句块来处理,所以必须强制将它处理成一个表达式,所以采用括号
var json1 = '{"name":"kingx"}';
var json2 = '{"address":["beijing","shanghai"]}';
console.log(eval("(" + json1 + ")"));// {name: "kingx"}
console.log(eval("(" + json2 + ")"));// {address: ["beijing", "shanghai"]}
6.7 Ajax跨域解决方案
6.7.1 浏览器同源策略
浏览器同源策略是浏览器最基本也是最核心的安全功能,它约定客户端脚本在没有明确授权的情况下,不能读写不同源的目标资源。
同源明确地表示为相同协议、域名和端口号,如果两个资源路径在协议、域名、端口号上有任何一点不同,则它们就不属于同源的资源。
在同源策略上,又分为 2 种表现形式:
- DOM同源策略:禁止对不同页面进行DOM操作,主要的场景是iframe跨域,不同域名下的iframe会限制访问
- XMLHttpRequest同源策略:禁止使用XMLHttpRequest向不是同源的服务器发送Ajax请求
6.7.2 浏览器跨域限制
主要是由没有遵守浏览器的同源策略引起的,浏览器对跨域访问的限制,可以在很大程度上保护用户的隐私数据安全。
下面是两个实际场景
(1)没有DOM同源策略限制
假如浏览器没有DOM同源策略限制,那么不同域的iframe可以相互访问,黑客就可以采用以下的方式发起攻击:
- 做一个假网站,里面用iframe嵌套一个银行网站
- 把iframe宽高调整到占据浏览器可视区的全部空间,这样用户在进入网站后,除了域名,其余看到的内容和其他银行网站是一样的
- 用户在输入用户名和密码后,主网站就可以跨域访问到所嵌套的银行网站的DOM节点,从而黑客就拿到用户输入的用户名和密码了
(2)没有XMLHttpRequest同源策略限制
假如浏览器没有XMLHttpRequest同源策略限制,那么黑客可以进行跨站请求伪造CSRF攻击,具体方式如下:
- 用户登录了个人银行页面A,页面A会在Cookie中添加用户信息
- 用户浏览了恶意页面B,在恶意页面中执行了恶意Ajax请求的代码
- 此时页面B会向页面A发送Ajax请求,该请求会默认发送用户Cookie信息
- 页面A会从请求的cookie中提取用户信息,验证用户无误,就会返回用户的隐私数据,而此时数据就会被恶意页面B获取到,从而造成用户隐私数据的泄露
- 由于Ajax请求的发送会自动执行,所以用户是无感知的
6.7.3 Ajax跨域请求场景
虽然浏览器有跨域访问的限制,但是在某些实际的业务场景中,不可避免地需要进行跨域访问。
6.7.4 CORS
CORS(CrossOrigin Resource Sharing,跨域资源共享)。主要实现方式是服务端通过对响应头的设置,接收跨域请求处理。
不同的服务端框架采用的处理方式不同。
通过服务端的处理不会对前端代码做任何修改,但是由于服务端采用的语言、框架多变,处理方式会依赖各种语言的特性。
6.7.5 JSONP
JSONP是客户端与服务器端跨域通信最常用的解决办法,它的特点是简单适用、兼容老式浏览器、对服务器端影响小。
主要思想可以分 2 步理解:
- 在网页中动态添加一个script标签,通过script标签向服务器发送请求,在请求中会携带一个请求的callback回调函数名
- 服务器在接收到请求后,会处理响应获取返回的参数,然后将参数放在callback回调函数中对应的位置,并将callback回调函数通过json格式进行返回
构建JSONP请求实际是创建一个新的script元素,通过src属性指定跨域请求的url,并在url中携带请求成功的回调函数作为参数。
回调函数必须设置为全局函数。因为服务端在响应后会从全局查找回调函数,所以如果回调函数不是定义在全局作用域中,那么会报错。
(1)优点
- 使用简单,不会有兼容性问题,是目前比较流行的跨域解决方案
(2)缺点
- 只支持get请求,这是JSONP目前最大的缺点。如果是post请求,那么JSONP则无法完成跨域处理
- 响应依赖于其他域的实现,如果请求的其他域不安全,可能会对本域造成一定的安全性影响
- 很难确定JSONP请求是否失败,虽然在HTML5中给script标签增加了onerror事件处理程序,但是存在兼容性问题