Ajax
Asynchronous Javascript And Xml
传统的请求方式:
-
URL地址栏
-
超链接
-
form表单
-
通过JS代码
-
window.open(url)
-
document.location.href = url
-
window.location.href = url
-
缺陷:
-
页面全部刷新,用户体验较差
-
用户体验不连贯
概述
Ajax可以在浏览器中发送异步请求,请求A和请求B是异步的;不需要等对方的执行结果。
在同一个浏览器页面当中,可以发送多个ajax请求,这些ajax请求之间不需要等待,是并发的。
对于Ajax来说,服务器可能会响应三种数据:
-
普通文本
-
XML字符串
-
JSON字符串
Ajax解析响应回来的数据,并将解析之后的数据渲染到div图层当中,这个div就完成局部更新了。
-
Ajax不是一种技术,是多种技术结合的产物。
-
Ajax是Web前端的JS代码。
-
Ajax数据多用JSON传输
-
AJAX可以更新网页的部分,而不需要重新加载整个页面
-
AJAX可以做到在同一个网页中同时启动多个请求,类似于在同一个网页中启动“多线程”,一个“线程”一个“请求”。
XMLHttpRequest
XMLHttpRequest对象是AJAX的核心对象。
- XMLHttpRequest对象的方法
方法 | 描述 |
---|---|
open(method,url,async,user,psw) |
method:请求方式 url:文件位置 async:true同步、false异步 user:可选用户名 psw:可选密码 |
send() |
将请求发送到服务器,用于GET请求 |
send(String) |
将请求发送到服务器,用于POST请求 |
- XMLHttpRequest对象的属性
属性 | 描述 |
---|---|
readyState |
保存XMLHttpRequest的状态;0 请求未初始化、1 服务器连接已建立、2 请求已收到、3 正在处理请求、4 请求已完成且响应已就绪 |
onreadystatechange |
当 readyState 属性发生变化时被调用的函数 |
responseText |
以字符串返回响应数据 |
status |
返回请求的状态号200: "OK" 403: "Forbidden" 404: "Not Found" |
Ajax的请求和响应都是完全依靠XMLHttpRequest对象的,XMLHttpRequest对象的readyState属性记录下了XMLHttpRequest对象的状态,readState属性对应的状态值:
-
0 : 请求未初始化
-
1 : 服务器连接已建立
-
2 : 请求已收到
-
3 : 正在处理请求
-
4 : 请求已完成且响应已就绪
当XMLHttpRequest对象的readState属性值变为4时,请求就完成了。
get
//1. 创建对象
let xhr = new XMLHttpRequest();
//2. 注册回调函数
xhr.onreadystatechange = function () {
if (this.readyState == 4){
console.log(typeof this.readyState)
if (this.status == 200){
console.log(typeof this.status)
document.querySelector('#app').innerText = this.responseText;
}
}
}
在readyState变化时调用onreadyStateChange事件回调函数,该函数被调用不止一次
响应就绪后有一个[[HTTP]]状态码,200表示请求成功,404表示资源不存在,通过this.status
可以获取Http的状态码
如果状态码为200,代表响应成功结束,可以通过XMLHttpRequest的属性responseText获取响应数据
- 开启通道,发送请求
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (this.readyState == 4){
console.log(typeof this.readyState)
if (this.status == 200){
console.log(typeof this.status)
document.querySelector('#app').innerText = this.responseText;
}
}
}
//开启通道:xhr.open(请求方式,服务器地址,async:同步,用户名,密码)
xhr.open('GET','/ajax/request',true,null,null);
//发送GET请求
xhr.send();
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (this.readyState == 4){ //number
console.log(typeof this.readyState)
if (this.status == 200){ //number
console.log(typeof this.status)
document.querySelector('#app').innerText = this.responseText;
}
}
}
//开启通道:xhr.open(请求方式,服务器地址,async:同步,用户名,密码)
xhr.open('GET','http://localhost:8080/ajax/getRequest',true,null,null);
//发送GET请求
xhr.send();
- get请求是在url上提交数据
get请求的缓存问题
对于低版本的IE浏览器来说,Ajax的get请求可能会走缓存,存在[[JavaWeb#get和post的区别|缓存问题]],Http的get请求会被缓存起来
POST请求的响应内容不会被浏览器缓存起来
优点:从浏览器的缓存中获取资源速度快
缺点:无法从服务器端获取最新的资源
走缓存的必要条件:Get请求并且请求路径没有变化
解决方法:对请求连接加一个时间戳,每一次发送的请求路径都是不同的
xhr.open('GET','/ajax/request?t=' + new Date().getTime(),true);
post
POST在请求体中提交数据,不能在URL行上提交
使用xhr.send(String)
方法
当前有表单:
<body>
用户名: <input type="text" name="username" id="username"> <br>
密 码 : <input type="text" name="password" id="password"> <br>
<button id="btn">POST</button>
<div id="myDiv"></div>
</body>
<script>
document.querySelector('#btn').addEventListener('click', function () {
let username = document.querySelector('#username').value;
let password = document.querySelector('#password').value;
console.log('username = ' + username);
console.log('password = ' + password);
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200){
document.querySelector('#myDiv').innerText = xhr.responseText;
}
}
xhr.open('POST','http://localhost:8080/ajax/postRequest',true,null,null);
xhr.send(`username=${username}&password=${password}`);
})
</script>
点击按钮,发送请求并将表单提交的数据一并提交,服务器端将数据转换为字符串回显到div中
public class AjaxServletPOST extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp){
//跨域
Map<String, String[]> parameterMap = req.getParameterMap();
StringBuilder builder = new StringBuilder();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
builder.append(entry.getKey() + " " + Arrays.toString(entry.getValue())).append('\n');
}
System.out.println(builder);
resp.getWriter().write(builder.toString());
}
}
服务器程序,接收请求参数输出到控制台并格式化字符串返回
点击POST发送请求,可以查看到报文:
请求负载中有数据,但是在服务器端无法获取到任何数据
此时并不是以表单形式提交的,正常表单提交的报文应该是
需要使用Ajax模拟form表单提交数据
Ajax模拟form表单
document.querySelector('#btn').addEventListener('click', function () {
let username = document.querySelector('#username').value;
let password = document.querySelector('#password').value;
console.log('username = ' + username);
console.log('password = ' + password);
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200){
document.querySelector('#myDiv').innerText = xhr.responseText;
}
};
xhr.open('POST','http://localhost:8080/ajax/postRequest',true,null,null);
xhr.setRequestHeader("Context-Type","application/x-www-form-urlencoded"); //模拟表单数据
xhr.send(`username=${username}&password=${password}`);
这时的请求报文:
基于JSON的数据交互
前端需要的数据格式:
[
{"username" : "zhangsan", "age" : 20, "gender" : true, "hobby" : ['smoke','drink']},
{"username" : "lisi", "age" : 23, "gender" : true, "hobby" : ['smoke','drink']},
{"username" : "wangwu", "age" : 26, "gender" : true, "hobby" : ['smoke','drink']}
]
后端就需要返回该格式的字符串:
public class ParseJsonStrServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
StringBuilder builder = new StringBuilder();
builder.append("[");
builder.append("{\"username\" : \"zhangsan\" , \"age\" : 20, \"gender\" : true, \"hobby\" = ['smoke','drink']} ,");
builder.append("{\"username\" : \"lisi\" , \"age\" : 23, \"gender\" : true, \"hobby\" = ['smoke','drink']} ,");
builder.append("{\"username\" : \"wangwu\" , \"age\" : 26, \"gender\" : false, \"hobby\" = ['smoke','drink']} ");
builder.append("]");
response.getWriter().write(builder.toString());
}
}
使用JSON.parse就可以将字符串转为JSON对象
但是手动拼接JSON字符串太麻烦了,可以使用fastjson进行改进
fastjson
alibaba捐献给Apache的开源软件
转换之后的结果:
```json
{"age":20,"id":"001","username":"zhangsan"}
List集合:
```json
[
{
"age": 20,
"id": "001",
"username": "zhangsan"
},
{
"age": 22,
"id": "002",
"username": "lisi"
},
{
"age": 23,
"id": "003",
"username": "wangwu"
}
]
Ajax乱码问题
-
get请求
- 发送数据到服务器,服务器获取是否会乱码
- 服务器响应给前端的中文是否会乱码
-
post请求
- 发送数据到服务器,服务器获取是否会乱码
- 服务器响应给前端的中文是否会乱码
结论:Tomcat10的Ajax不会出现乱码
Tomcat9
Get请求没有问题,响应会乱码
POST 请求会乱码,响应也会乱码
解决请求乱码:request.setCharacterEncoding("UTF-8")
解决响应乱码:response.setContentType("text/html;charset=UTF-8")
Ajax的同步和异步
-
Ajax请求1和Ajax请求2同时并发,不需要等待对方,这就是异步
-
Ajax请求2必须等待Ajax请求1结束后才能发送,这就是同步
//ajax请求1:
xhr.open('GET','URL',false);
//ajax请求2:
xhr.open('GET','URL',true);
表示:ajax请求1不支持异步请求,ajax请求2支持异步请求
ajax请求1发送之后,必须等待ajax请求1的结束才能发送其他ajax请求
ajax请求2发送之后不影响其他ajax请求的发送
当前有两个按钮:
后端代码:
发送请求之后休眠5s结束此次请求
- 如果先发送Ajax1,再发送Ajax2:
鼠标移入btn2,不能点击,不能变为hover样式(变深),同步必须等待Ajax1处理完毕
- 如果先发送Ajax2,再发送Ajax1:
鼠标移入btn1,可以点击,可以变为hover样式,异步无须等待Ajax2处理完毕
在验证用户名和其他信息时最好使用同步,需要在点击 “注册” 按钮之前对所有信息校验完毕,也就是未校验完毕时不能点击 “注册” 按钮
案例
省市联动
在网页上选择对应的省份之后动态关联出该省份对应的市,选择对应的市后动态关联出对应的区
下拉列表选项改变会触发change事件
- 数据库表的设计
t_area (区域表)
id(PK-自增) code name pcode
---------------------------------------------
1 001 河北省 null
2 002 河南省 null
3 003 石家庄 001
4 004 邯郸 001
5 005 郑州 002
6 006 洛阳 002
7 007 丛台区 004
将全国所有的省、市、区、县等信息都存储到一张表当中。
采用的存储方式实际上是code pcode形势。
- 点击省下拉列表,获取省份(pcode is null)
- 省份选择完毕(change事件),发送ajax请求获取区(pcode = code)
同源与跨域
- 子资源:嵌入到HTML文档中的HTML元素,1993年引入了第一个子资源
<img>
,通过引入子资源使网页变得更美观、更复杂。
当渲染一个带有<img>
的网页,必须从一个域获取子资源;之后出现了<script>、<frame>、<video>、<audio>、<iframe>、<link>、<form>
等,这些子资源可以在网页加载后由浏览器获取,他们都可以发起网络请求
域与跨域
域(Origin)由三部分组成:协议、主机名、端口号;组成域的三部分有一个不同,域则不同
跨域请求是:访问https://example.com
时,首页有一个图标http://example2.com/posts/animal.png
,加载这个图标;这个图标的域和我们访问的域是不相同的,这就是跨域的请求
跨域的危害
假设浏览器不存在CORS,并且浏览器允许各种跨域请求
假设有两个网站 a.com和b.com,a.com是我们的网站(假定为电商平台或者公司后台),需要登录之后才能交易,登录凭证存储在cookie当中。在b.com中嵌入了一个特殊的脚本,这个脚本尝试读取a.com下的cookie信息,如果当前浏览器没有任何跨域限制,就可以通过b.com发送Ajax请求到a.com(自动携带cookie),就可以使用当前用户身份进行删除、购买等操作
b的首页中可能包含有发送Ajax请求访问a.com的代码,而Ajax请求是不会改变浏览器地址栏的,也就是会自动携带有a.com对应的cookie,可以用当前用户身份直接访问a.com
同源策略
同源策略通过阻止访问不同的资源来防止跨域攻击,但是某些标签还是可以跨域请求,例如:
Tags | Cross-Origin | Note |
---|---|---|
<iframe> |
允许嵌入 | 取决于X-Frame-Oprions |
<link> |
允许嵌入 | 可能需要正确的Content-Type |
<form> |
允许写入 | 经常用此标签进行跨域写操作 |
不允许跨域访问的资源:
-
localStorage
-
IndexedDB
-
Cookie
-
Ajax
同源策略解决了很多问题,但限制性很强。
Ajax 跨域
跨域是指从一个域名的网站去请求另一个域名的资源,比如从百度 https://baidu.com
页面去请求京东https://www.jd.com
通过超链接、form表单、js代码(window.location.href)等方式进行跨域是没有问题的
因为a、form提交、location.href = ? 直接改变了地址栏刷新了整个页面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--超链接的跨域访问-->
<a href="http://localhost:8081/b/b.html">跨域访问 b服务器的index页面</a>
<!--表单的跨域访问-->
<form action="http://localhost:8081/user/login">
用户名:<input type="text" name="username" id="username">
密码: <input type="password" name="password" id="password">
<input type="submit" value="提交">
</form>
<!--JS代码跨域-->
<button onclick="window.location.href='http://localhost:8081/b/b.html'">跨域访问 b服务器的index页面</button>
<!--script标签跨域-->
<script src="http://localhost:8081/b/js/jQuery.js"></script>
<!--加载其他站点的图片-->
<img src="http://localhost:8081/b/bd_log.png">
</body>
</html>
但是对于Ajax请求来说,如果跨域访问:
请求还是会发送的,但是报文以及控制台报错:
Ajax跨域请求被CORS 同源策略阻止
浏览器规定,A站点的JS代码无法与非同源的B站点之间进行资源的交互
-
无法读取非同源网站的Cookie、LocalStorage和IndexedDB
-
无法接触非同源网站的DOM
-
无法向非同源地址发送Ajax请求
在同一个浏览器窗口中,浏览器的内存只有一份,在同一个内存中访问b站点的资源就是跨域,两个站点不允许共享同一个XMLHttpRequest对象。共享同一个XMLHttpRequest对象是不安全的。
共享XMLHttpRequest是危险的,因为
导致Ajax不能访问的是同源策略,同源策略是浏览器的安全策略
- 同源的定义
协议一致、域名一致、端口号一致,三者同时一致才是同源,其他都是不同源
同源时XMLHttpRequest可以共享,不同源XMLHttpRequest对象不能共享
之前的超链接、form表单等都是不同源的(浏览器地址栏改变,没有内存共享),没有XMLHttpRequest安全问题;Ajax请求发送是依赖XMLHttpRequest对象,Ajax请求另一个站点的资源就是共享了同一个XMLHttpRequest对象
现实开发中的系统都是分布式微服务系统,需要解决Ajax跨域的问题
解决Ajax跨域访问
服务端设置响应头:被请求站点允许Ajax跨域
或者设置为response.setHeader("Access-Control-Allow-Origin","*")
所有站点都可以跨域访问本站点
jsonp:json with padding GET
jsonp不是一种真正的Ajax请求,可以完成Ajax的局部刷新效果,是一种类似于Ajax的请求
可以通过<script>
标签的src属性(本身就可以跨域)访问servlet完成跨域访问
当前页面中有如下js函数:
在页面中使用script标签进行跨域访问:
这时如果后端返回:
就是将请求到的数据替换为了script标签内的内容
所以会调用sayHello方法:
也可以动态的传递函数名称:
此时明显是GET请求,所以后端可以直接获取请求的参数:
注意:通过请求头提交数据的明显是GET请求,也就是JSONP只支持GET请求
jsonp的缺陷
如果在对应的b(8080)的Servlet中返回一段js代码:
上文已经提到过,返回的内容被替换为script标签的标签体,这段js代码一定会被执行,这样就是b站点借助了a站点的xhr对象访问到了a站点,这是极其危险的操作,如果a站点保存了用户的登录状态,b站点可以模仿用户的身份进行任何操作。
jsonp实现局部刷新
script在页面加载时执行,无法达成局部刷新效果;希望点击某个按钮后再加载script标签,执行完就可以局部刷新
HttpClient代理机制
可以将Ajax请求发送到本站点中的某个Servlet上,这个Servlet再请求目标站点的资源
现在只需要解决如何在ProxyServlet中发送GET/POST请求
-
使用JDK内置的API java.net.URL,可以发送Http请求
-
使用第三方的开源组件 apache的Httpclient,需要引入组件
现在要完成的需求:在A站点的ajax5.html中访问B站点的/hello程序
ajax5.html同源访问ProxyServlet:
ProxyServlet通过apache commons-httpclient组件发送GET请求访问TargetServlet:
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
//get
String channelId = "sdd";
String clientId = "123";
// 目标地址
String url = "http://localhost:8081/b/hello";
HttpGet httpGet = new HttpGet(url);
// 设置类型 "application/x-www-form-urlencoded" "application/json"
httpGet.setHeader("Content-Type", "application/x-www-form-urlencoded");
//System.out.println("调用URL: " + httpGet.getURI());
CloseableHttpClient httpClient = HttpClients.createDefault();
// 执行请求并获取返回
HttpResponse resp = httpClient.execute(httpGet);
HttpEntity entity = resp.getEntity();
//System.out.println("返回状态码:" + resp.getStatusLine());
// 显示结果
BufferedReader reader = new BufferedReader(new InputStreamReader(entity.getContent(), "UTF-8"));
String line = null;
StringBuffer responseSB = new StringBuffer();
while ((line = reader.readLine()) != null) {
responseSB.append(line);
}
System.out.println("响应数据:" + responseSB);
reader.close();
httpClient.close();
response.getWriter().write(responseSB.toString()); /*响应给ProxyServlet,再响应给Ajax*/
}
TargetServlet响应给ProxyServlet,ProxyServlet再响应给Ajax :
Nginx反向代理
Axios
Axios简化了Ajax的书写。
-
引入Axios
-
使用Axios发送请求
axios方法的参数是一个对象,指定请求方式method和请求地址url
简化写法
为了简化书写,Axios为所有请求方式提供了别名:
- 格式:
axios.请求方式(url, [,data [, config] ])
如何在页面加载完毕就获取请求数据呢?
可以在[[Vue#Vue的生命周期|created]]就进行操作,此时data数据代理和methods已经创建完毕,也可以在mounted中进行操作
省市区联动
要求:页面加载完毕后,默认加载并显示出第一个省、第一个市、第一个区的信息
思路:axios请求第一个省份信息,获取省份id后再请求市信息的pid = 省份id的信息,请求市信息完毕后再请求区ppid = 市id的数据
这样做会导致一个问题:请求市信息的axios必须等待请求省信息的axios完毕后才能执行,请求区信息的axios必须等待请求市信息的axios完毕后才能执行。
这样就会导致“回调地狱”:
通过原生ajax可以设置请求省、请求市、请求区的ajax的async参数均为false,这三个ajax都是同步执行的。
Axios也可以通过设置await、async解决这个问题:
注意:
- await必须在async函数内才有效。
- await实际上就是取代了then方法,阻塞等待请求成功的结果。