01-Ajax&Axios

EUNEIR發表於2024-03-13

Ajax

Asynchronous Javascript And Xml

傳統的請求方式:

  • URL位址列

  • 超連結

  • form表單

  • 透過JS程式碼

    • window.open(url)

    • document.location.href = url

    • window.location.href = url

缺陷:

  1. 頁面全部重新整理,使用者體驗較差

  2. 使用者體驗不連貫

AJAX同步請求

概述

Ajax可以在瀏覽器中傳送非同步請求,請求A和請求B是非同步的;不需要等對方的執行結果。

在同一個瀏覽器頁面當中,可以傳送多個ajax請求,這些ajax請求之間不需要等待,是併發的。

image-20230516200952715

對於Ajax來說,伺服器可能會響應三種資料:

  1. 普通文字

  2. XML字串

  3. JSON字串

Ajax解析響應回來的資料,並將解析之後的資料渲染到div圖層當中,這個div就完成區域性更新了。

AJAX非同步請求

  • 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傳送請求,可以檢視到報文:

image-20230519102250493

請求負載中有資料,但是在伺服器端無法獲取到任何資料

此時並不是以表單形式提交的,正常表單提交的報文應該是

image-20230519103022025

需要使用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的開源軟體

image-20230519134416298

轉換之後的結果:

```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請求的傳送

當前有兩個按鈕:

image-20230519153629420

後端程式碼:

Snipaste_2023-05-19_15-36-57

傳送請求之後休眠5s結束此次請求

  • 如果先傳送Ajax1,再傳送Ajax2:image-20230519153856824

滑鼠移入btn2,不能點選,不能變為hover樣式(變深),同步必須等待Ajax1處理完畢

  • 如果先傳送Ajax2,再傳送Ajax1:image-20230519154109683

滑鼠移入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),就可以使用當前使用者身份進行刪除、購買等操作

image-20230520141011019

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

image-20230520104215616

透過超連結、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請求來說,如果跨域訪問:

image-20230520125928413

請求還是會傳送的,但是報文以及控制檯報錯:

image-20230520125953212

image-20230520130014245

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跨域

image-20230520132114194

或者設定為response.setHeader("Access-Control-Allow-Origin","*") 所有站點都可以跨域訪問本站點

jsonp:json with padding GET

jsonp不是一種真正的Ajax請求,可以完成Ajax的區域性重新整理效果,是一種類似於Ajax的請求

可以透過<script>標籤的src屬性(本身就可以跨域)訪問servlet完成跨域訪問

當前頁面中有如下js函式:

image-20230520150550721

在頁面中使用script標籤進行跨域訪問:

image-20230520144451322

這時如果後端返回:

Snipaste_2023-05-20_15-06-20

就是將請求到的資料替換為了script標籤內的內容

所以會呼叫sayHello方法:

image-20230520150730369

也可以動態的傳遞函式名稱:

image-20230520150821341

此時明顯是GET請求,所以後端可以直接獲取請求的引數:

image-20230520150903672

注意:透過請求頭提交資料的明顯是GET請求,也就是JSONP只支援GET請求

jsonp的缺陷

如果在對應的b(8080)的Servlet中返回一段js程式碼:

image-20230520145455699

上文已經提到過,返回的內容被替換為script標籤的標籤體,這段js程式碼一定會被執行,這樣就是b站點藉助了a站點的xhr物件訪問到了a站點,這是極其危險的操作,如果a站點儲存了使用者的登入狀態,b站點可以模仿使用者的身份進行任何操作。

jsonp實現區域性重新整理

script在頁面載入時執行,無法達成區域性重新整理效果;希望點選某個按鈕後再載入script標籤,執行完就可以區域性重新整理

image-20230520152012019

HttpClient代理機制

可以將Ajax請求傳送到本站點中的某個Servlet上,這個Servlet再請求目標站點的資源

image-20230520154908910

現在只需要解決如何在ProxyServlet中傳送GET/POST請求

  • 使用JDK內建的API java.net.URL,可以傳送Http請求

  • 使用第三方的開源元件 apache的Httpclient,需要引入元件

現在要完成的需求:在A站點的ajax5.html中訪問B站點的/hello程式

image-20230520161559409

ajax5.html同源訪問ProxyServlet:

Snipaste_2023-05-20_16-16-30

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 :

image-20230520162022713

Nginx反向代理

Axios

Axios簡化了Ajax的書寫。

  1. 引入Axios

    image.png

  2. 使用Axios傳送請求

image.png

axios方法的引數是一個物件,指定請求方式method和請求地址url

簡化寫法

為了簡化書寫,Axios為所有請求方式提供了別名:

  • 格式:axios.請求方式(url, [,data [, config] ])

image.png

如何在頁面載入完畢就獲取請求資料呢?

可以在[[Vue#Vue的生命週期|created]]就進行操作,此時data資料代理和methods已經建立完畢,也可以在mounted中進行操作

省市區聯動

image.png

要求:頁面載入完畢後,預設載入並顯示出第一個省、第一個市、第一個區的資訊

思路:axios請求第一個省份資訊,獲取省份id後再請求市資訊的pid = 省份id的資訊,請求市資訊完畢後再請求區ppid = 市id的資料

這樣做會導致一個問題:請求市資訊的axios必須等待請求省資訊的axios完畢後才能執行,請求區資訊的axios必須等待請求市資訊的axios完畢後才能執行。

這樣就會導致“回撥地獄”:

image.png

透過原生ajax可以設定請求省、請求市、請求區的ajax的async引數均為false,這三個ajax都是同步執行的。

Axios也可以透過設定await、async解決這個問題:

image.png

注意:

  • await必須在async函式內才有效
  • await實際上就是取代了then方法,阻塞等待請求成功的結果。