Ajax簡介
Ajax(Asynchronous JavaScript And XML),可以通過其在瀏覽器中向服務器發送異步請求,無需刷整個頁面就可以獲取數據。
Ajax不是新的編程語言,而是一種將現在的標準組合在一起使用的新方式
使用場景:實現懶加載,當需要展示某一數據時再去請求數據
優點:
- 無需刷新整個頁面就可以與服務器端進行通信
- 允許根據用戶時間來更新部分頁面內容
缺點:
- 沒有瀏覽歷史,不能回退
- 存在跨域問題
- SEO不友好:使用ajax請求的數據,是異步請求的方式。SEO主要是獲取源文檔內容,也就是響應體內的內容,但是響應體內的內容需要在解析的過程中動態去獲取的,所以SEO無法獲取通過ajax請求的數據,也即通過ajax展示的數據對SEO不友好。
XML
XML(可擴展標記語言):被設計用來傳輸和存儲數據。XML和HTML類似,不同的是HTML中都是預定義標簽,而XML都是自定義標簽,用來表示一些數據。但是目前大部分使用JSON來替代XML,原因:JSON數據更容易書寫,并且轉換也更方便。
HTTP
HTTP(hypertext transport protocol)協議,超文本傳輸協議,協議詳細規定了瀏覽器和萬維網服務器之間互相通信的規則。主要規定了請求和響應規則。
格式:
- 請求報文
行 POST /getNames HTTP/1.1 (分別是:請求方式/請求資源路徑/HTTP版本號)
頭 Host: xxx.xxx.xxx
Connection: keep-alive
Pragma: no-cache Cache-Control: no-cache
Accept-Encoding: gzip, deflate, br(以上都是瀏覽器告訴服務器想要請求什么樣的數據,以及想要跟服務器進行怎樣的約定)
空行
體 username=admin&password=admin(如果是post請求則請求體有數據)
- 響應報文
行 HTTP/1.1 200 OK (分別是:HTTP版本號/響應狀態碼/響應狀態碼的描述)
頭
cache-control: no-cache
content-encoding:br
content-type: application/json; charset=utf-8(以上都是服務器告訴瀏覽器響應的數據相關的信息)
空行
體 響應數據
Ajax使用
ajax基本使用
// 獲取xmlhttprequest實例
const xhr = new XMLHttpRequest();
// 初始化
xhr.open('GET','http://127.0.0.1:8000/server');
// 發送請求 如果部需要發送請求體,則必須傳null,因為該參數在一些瀏覽器中是必須的。
xhr.send(null);
// 事件綁定,處理響應結果,每次readyState數據發生變化都會觸發onreadystatechange事件。
xhr.onreadystatechange = function () {
// readyState表示當前處在請求/響應的哪個階段
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
// 響應行
console.log(xhr.status);// 狀態碼
console.log(xhr.statusText);//狀態碼字符串
// 所有的響應頭
console.log(xhr.getAllResponseHeaders());
// 響應體
console.log(xhr.response);
// or 響應體的字符串表示,該方式不受reponseType設置影響
console.log(xhr.responseText);
}
}
}
readyState的屬性值有:
- 0:未初始化。尚未調用open方法。
- 1:已打開(open)。已調用open方法,但是還沒有調用send()方法
- 2:已發送(send)。已調用send方法,尚未收到響應。
- 3:接受中。已收到部分響應。
- 4:完成。已收到全部的響應。
所以可以看出,一般通過判斷readyState是否為4從而判斷是否獲取到了響應數據。
以上基本使用中用到了open方法作為請求的初始化操作。那么open方法如何去使用呢?
xhr.open('GET', 'http://127.0.0.1',true);
第一個參數代表請求的方式,一般有:
- GET
- POST
第二個參數代表請求的URL,如果是GET方式請求,當要傳輸查詢字符串的時候,則需要在URL后面進行拼接
例如:
/test?uname=admin&password=admin
第三個參數表示是否使用異步請求,默認為true。如果為false,則是同步請求,會導致阻塞。
初始化完成之后,可以使用send方法發送數據。
- GET方法,由于該方法傳輸查詢參數是通過拼接字符串的形式,所以當使用send方法的時候,不需要傳入任何數據,但是有些瀏覽器需要該參數,因此最好傳一個null。
- POST方法,可以使用該方法傳輸任何數據,該數據作為請求體進行傳輸。
send方法可以不寫么?不可以,原因:open僅僅初始化了請求,但是并沒有發送請求,而發送請求的真正時間點,在send方法調用之后。
設置請求頭
可以使用setRequestHeader設置預定義請求頭和自定義請求頭
xhr.setRequestHeader('uname','admin');// 自定義請求頭
注意:自定義請求頭可以會觸發預檢請求。
獲取json數據
當服務器端返回的數據是json字符串數據的時候,那客戶端如何獲取json數據本身呢?
- 手動轉換
利用JSON.parse(jsonstr)來進行轉換
- 配置,自動轉換
XMLHttpRequest實例對象上有一個屬性:responseType,可以通過這個屬性對返回的數據進行自動轉換,這里需要賦值為json,即:xhr.responseType = 'json'
這樣,獲取到的響應體數據會自動轉換為json格式,但是需要注意的是,只有response這個屬性數據才會受responseType數據設置的影響。responseText本身就是對響應體的字符串表示,所以不受影響。
超時和網絡異常處理
- 對于請求超時,客戶端可以這樣設置
xhr.timeout = 2000;
表示,如果2s中還未收到響應,則作為超時處理,此時會取消請求。
例如:
一般對于請求超時,客戶端都會通知用戶,類似的,這里可以使用超時回調,來進行超時的一個處理:
xhr.ontimeout = function() {
alert('網絡超時');
}
- 對于網絡異常/斷網的情況下,也有對應的回調函數供開發人員去操作
xhr.onerror = function() {
alert('哦豁,網絡異常');
}
取消請求
可以利用XMLHttpRequest實例對象上的方法abort()取消還沒有獲得響應的請求。
xhr.abort();
重復請求問題
有一種場景:用戶點擊按鈕,發送請求。但是如果用戶不斷點擊按鈕就可能發生不斷發請求的情況,服務器就需要處理相同的請求,從而造成服務器的壓力比較大。
例如:
以上便是用戶點擊多次的一個情況,可以看到請求也發了多次。
為了避免這個情況,需要在用戶多次點擊的時候進行處理,比如:當用戶點擊多次的時候,如果上一次請求還在發送過程中,則取消上一次的請求,重新開啟一個新的請求。這種方式也就是我們經常所說的防抖解決方案。
let xhr = null;
let isSending = false; // 標識是否正在發送請求
btn.addEventListener('click', function () {
// 獲取xmlhttprequest實例
if(isSending) xhr.abort();
xhr = new XMLHttpRequest();
isSending = true;
xhr.responseType = 'json';
xhr.onerror = function() {
alert('哦豁,網絡出錯')
}
// 初始化
xhr.open('POST','http://127.0.0.1:8000/server');
xhr.setRequestHeader('uname', 'admin');
// 發送請求
xhr.send();
// 事件綁定,處理響應結果
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
isSending = false;
if (xhr.status >= 200 && xhr.status < 300) {
// input.innerHTML = JSON.parse(xhr.responseText).uname;
input.innerHTML = xhr.response.uname;
}
}
}
})
其實除了防抖利用標識符解決外,最有效的方式則是禁用當前的按鈕,不允許用戶進行點擊,一方面告訴用戶請求已經發送了,另一方面也防止用戶多次點擊的情況。
Jquery中使用Ajax
- 引入jquery
這里采用cdn引入
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
- 編寫get/post請求
$.get('http://127.0.0.1:8000/server',{a: 12, b: 34}, function (data) {
console.log(data)
},'json')
$.post('http://127.0.0.1:8000/server', {a:12, b:34},function (data) {
console.log(data)
}, 'json')
這兩個方法第一個參數表示請求的url,第二個參數表示傳遞的數據,第三個參數表示對返回的響應體的格式要求,也即responseType的值。
通過的請求方法:
$.ajax({
// url
url: 'http://127.0.0.1:8000/server',
// method
type: 'POST',
// data
data: {a: 1, b: 2},
// 響應體類型
dataType: 'json',
// headers
headers: {
a: 1
},
// 超時時間設置
timeout: 2000,
// 成功回調
success: function (data) {
console.log(data);
},
// 失敗回調,包括網絡異常/超時/等情況
error: function (err) {
console.log(err);
}
})
fetch發送請求
fetch屬于js全局的方法,所以可以直接調用,他是一個基于promise的http請求方法。且是一個異步請求。
// 當調用fetch的時候,基本上請求就開始發送了。
let f = fetch('http://127.0.0.1:8000/server', {
// 初始化配置選項
method: 'POST',
headers: {
uname: 'chen'
},
// 請求體
body: {
uname: 'chen'
}
});
f.then(response=> {
// return response.text();// 返回一個已解決的響應體字符串表示的期約
return response.json();// 返回一個已解決的響應體json表示的期約
}).then(data=> {
console.log(data);
})
ajax工具庫-axios
axios基于promise的網絡請求庫,可以適用于瀏覽器和node環境
- 引入axios(采用CDN的方式)
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
- 發送get請求
// 配置基礎的url
axios.defaults.baseURL = 'http://127.0.0.1:8000';
axios.get('/server', {
params: {
a: 1,
b: 2
},
headers: {
uname: 'chen'
}
}).then(data=>{
console.log(data);
})
第一個參數表示請求的路徑,如果預先設置了基礎的url,此處設置的url會跟基礎的url進行拼接。第二個參數是對請求的基礎配置
- 發送post請求
// 配置基礎的url
axios.defaults.baseURL = 'http://127.0.0.1:8000
axios.post('/server', {
// 請求體
name: 'admin',
password: '123'
}, {
// 請求配置
headers: {
uname: 'chen'
},
// post請求也可以在Url后面添加額外的信息
params: {
a: 1,
b: 2
}
}).then(data=>{
console.log(data);
})
第一個參數跟get請求一致,第二個參數表示請求體數據,第三個參數表示請求配置。需要注意的是,由于axios的post請求默認的Content-Type為application/json;charset=UTF-8,因此會觸發預檢請求,需要對數據進行配置/服務器進行配置
跨域問題
ajax請求是不支持跨域的,也就是ajax請求滿足同源策略。所以對于想要獲取不同源的數據,就需要解決跨域問題。
跨域問題的解決方案有:
- JSONP
JSONP是一個非官方的跨域解決方案,只支持get請求。在網頁中一些標簽:img link iframe script。不受同源策略的影響。而JSONP就是利用script標簽的跨域能力來發送請求的。
script標簽如何發送請求呢?
我們知道如果要引入一個外部腳本,可以使用script標簽的src屬性進行指定。當瀏覽器解析到當前的這個script標簽的時候,會將其src的js腳本進行引入,類似于將js腳本中的代碼放在script標簽中相應的位置。
比如:當前有一個外部腳本test.js
console.log(124);
在html文件中進行引入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSONP</title>
</head>
<body>
<div></div>
<script src="/js/test.js"></script>
</body>
</html>
此時瀏覽器解析完成時(這里先不考慮腳本中代碼的執行),HTML呈現為:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSONP</title>
</head>
<body>
<div></div>
<script>
console.log(124);
</script>
</body>
</html>
可以看到對應的位置被外部Js腳本替換。(這里的外部js腳本必須是可執行的js腳本,如果js腳本里面是123,則認為是不合法的)
以上引入的js腳本是同一個域下的,同樣可以引入不同域下面的js腳本,我們用的比較多的就是cdn引入的jquery腳本
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
所以按照以上的思路,我們可以利用script標簽的src引入不同域的外部資源。但是如何獲取數據呢?
之前有提到過,script標簽引入的外部資源必須是可執行的js腳本。而如果簡單的想要獲取某個非可執行的js腳本數據,這是不合法的。
所以JSONP就說,既然需要執行的js腳本,那么服務器就給你提供一個客戶端已有的函數字符串。同時將你需要的數據拼接到函數字符串中。
描述比較生硬,可能不容易理解,可以看以下的例子。
具體實現:
客戶端:
<div class="btn">發送請求</div>
<div class="input"></div>
<script>
let btn = document.querySelector('.btn');
let input = document.querySelector('.input');
function handle(data) {
input.innerHTML = data.uname;
}
btn.addEventListener('click', function () {
let script = document.createElement('script');
// 發起請求
script.src = 'http://127.0.0.1:8000/server';
document.body.appendChild(script);
})
</script>
服務器端(node):
app.get('/server',(req,res) => {
let data = {
uname: 'op'
}
let resData = JSON.stringify(data);
// 將數據拼接到函數字符串中
res.send(`handle(${resData})`);
})
可以看到服務器將含有數據的函數調用字符串返回給了客戶端,
具體返回的內容如下:
handle({"uname":"op"})
客戶端接收到了這個字符串內容,就會嘗試解析,類似于執行js腳本,由于客戶端之前有這個handle函數,所以就會直接調用handle函數,利用handle函數就可以獲取到服務器端傳過來的數據了。
利:
- 實現簡單
弊:
- 需要服務器知道客戶端已有的函數名稱
- 容易遭受xss攻擊
- jQuery發送jsonp
jquery有一個方法getJSON可以很方便得發送jsonp,同時不需要服務器端知道函數名稱得情況下傳遞函數字符串。
服務器(node):
app.get('/server',(req,res) => {
let data = {
uname: 'op'
}
let resData = JSON.stringify(data);
// 獲取客戶端傳遞得函數名稱
let ca = req.query.callback;
res.send(`${ca}(${resData})`);
})
客戶端:
<div class="btn">發送請求</div>
<div class="input"></div>
<script>
$('.btn').click(function () {
$.getJSON('http://127.0.0.1:8000/server?callback=?',function (data) {
$('.input').html(`${data.uname}`);
})
})
</script>
第一個參數為目標Url,這里需要注意得是后面必須拼接callback參數,否則服務端無法獲得函數名稱,第二個參數是函數回調,利用該函數可以獲取到服務器返回得數據。
- CORS
CORS:跨域資源共享。是官方得跨域解決方案,特點:不需要客戶端做任何特殊得操作,完全在服務器中進行處理,支持get/post請求。
工作形式:當瀏覽器發現當前請求違反了CORS,則會發起一個預檢請求,詢問服務器是否允許這個請求,服務器通過設置響應頭告訴瀏覽器,允許該請求,則瀏覽器會將用戶發送得請求發送到指定得服務器。
一般設置以下響應頭信息:
- Access-Control-Allow-Origin: 表示允許的訪問來源。或者可以使用"*" 表示允許所有的訪問來源。這個字段一般用于對跨域請求的支持。
- Access-Control-Allow-Headers:表示允許的自定義請求頭。
- Access-Control-Allow-Methods:表示允許的請求方式。
例子node():
app.use('/server', (req,res,next) => {
// 設置所有域都可以訪問該域的資源
res.setHeader('Access-Control-Allow-Origin','*');
// 設置允許的自定義請求頭
res.setHeader('Access-Control-Allow-Headers', '*');
next();
})