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方法,阻塞等待請求成功的結果。