Ajax

Asynchronous Javascript And Xml

传统的请求方式:

  • URL地址栏

  • 超链接

  • form表单

  • 通过JS代码

    • window.open(url)

    • document.location.href = url

    • window.location.href = url

缺陷:

  1. 页面全部刷新,用户体验较差

  2. 用户体验不连贯

概述

Ajax可以在浏览器中发送异步请求,请求A和请求B是异步的;不需要等对方的执行结果。

在同一个浏览器页面当中,可以发送多个ajax请求,这些ajax请求之间不需要等待,是并发的。

对于Ajax来说,服务器可能会响应三种数据:

  1. 普通文本

  2. XML字符串

  3. 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形势。
  1. 点击省下拉列表,获取省份(pcode is null)
  2. 省份选择完毕(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的书写。

  1. 引入Axios

  2. 使用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方法,阻塞等待请求成功的结果。