1.HTTP請求簡介[1]
HTTP(Hypertest Transfer Protocol)是用于傳輸像HTML這樣的超文本文件的應(yīng)用層協(xié)議。它被設(shè)計用于WEB瀏覽器端和WEB服務(wù)端的交互,但也有其它用途。HTTP遵循經(jīng)典的client-server模型,客戶端發(fā)起請求嘗試建立連接,然后等待服務(wù)端的應(yīng)答。HTTP是無狀態(tài)協(xié)議,這意味著服務(wù)端在兩次請求間不會記錄任何狀態(tài)。
2.HTTP請求內(nèi)容
2.1請求URL
每個請求有一個請求URL。
2.2請求方法[2]:
HTTP定義了一系列請求方法,這些方法表明要對給定資源所做的操作。HTTP請求方法包含GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH等8種類型。
2.3應(yīng)答狀態(tài)碼[3]
HTTP應(yīng)答狀態(tài)碼表名一個HTTP請求是否成功完成。應(yīng)答狀態(tài)碼被分為5類:
信息應(yīng)答(100
– 199
)
成功應(yīng)答(200
– 299
)
重定向信息(300
– 399
)
客戶端錯誤(400
– 499
)
服務(wù)端錯誤(500
– 599
)
2.4 HTTP頭[4]
HTTP頭使得客戶端和服務(wù)端之間可以通過HTTP請求和應(yīng)答傳遞信息。HTTP頭包含大小寫敏感的名稱,后面跟一個“:”,然后是http頭的值。HTTP的值前面的空格會被忽略。
2.4.1 Authentication
- WWW-Authenticate: 請求資源時所用的認(rèn)證方法。可為Basic、Negotiate、NTLM等[5]。
- Authorization: 包含服務(wù)端驗證用戶的憑據(jù)。
2.4.2 Cookies
- Cookie: 包含上一次服務(wù)端發(fā)送的Set-cookie頭中的HTTP cookies。
- Set-Cookie: 從服務(wù)端向用戶側(cè)發(fā)送Cookie。
2.4.3 CORS
- Access-Control-Allow-Origin:表明應(yīng)答可以與哪些Origin共享。
2.5 請求體[6]
請求體(body)是Request接口的一個屬性,它是包含請求體內(nèi)容的可讀流。GET和HEAD請求不能攜帶請求體,如果攜帶請求體會返回null。
Demo1發(fā)起了一個POST類型的請求,在Chrome的開發(fā)者工具中可以看到請求URL、請求方法、應(yīng)答狀態(tài)碼、HTTP頭和請求體等信息,如圖1。
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://localhost:8080/day05_lzs/", true);
xhttp.send("fname=Bill&lname=Gates");
Demo1. 使用XMLHttpRequest發(fā)起一個簡單的POST請求
圖1. Chrome瀏覽器中使用XMLHttpRequest發(fā)起的一個簡單的POST請求的請求信息
3.幾種請求的方式
3.1 document類型的請求
如圖2,從Chrome瀏覽器的開發(fā)者工具可以看出,請求的類型分為Fetch/XHR、JS、CSS、Img、Media、Font、Doc、WS (WebSocket)、 Wasm (WebAssembly)、Manifest和Other(不屬于前面列出類型中的一種)。通過瀏覽器地址欄發(fā)起的請求和form表單請求的類型都是document。
圖2. Chrome瀏覽器開發(fā)者工具中請求的幾種類型
3.1.1 通過瀏覽器地址欄發(fā)起的請求
當(dāng)我們訪問一個web頁面時,在瀏覽器地址欄輸入訪問的地址并確認(rèn)后,會發(fā)起document類型的請求,如圖3。
圖3. 使用Chrome瀏覽器在地址欄發(fā)起的document類型的請求
3.1.2 Form請求
Demo2是一個form表單。當(dāng)form表單提交時,會發(fā)送一個document類型的請求,如圖4。
<form action="http://127.0.0.1:8080/day05_lzs">
<fieldset>
<legend>Personal information:</legend>
First name:<br>
<input type="text" name="firstname" value="Mickey">
<br>
Last name:<br>
<input type="text" name="lastname" value="Mouse">
<br><br>
<input type="submit" value="Submit"></fieldset>
</form>
Demo2. 一個form表單的HTML代碼
圖4. 使用Chrome瀏覽器form表單提交時發(fā)起document類型的請求
3.2 XHR請求
3.2.1 XMLHttpRequest簡介
XMLHttpRequest(XHR) 對象用于和服務(wù)端交互。它可以從URL取回數(shù)據(jù)而不用刷新整個頁面,這使得可以只更新頁面的局部而不影響整個頁面。[7]
如Demo3所示,使用XMLHttpRequest發(fā)送GET請求,在URL中添加“?fname=Bill&lname=Gates”實現(xiàn)發(fā)送信息。XMLHttpResponse 對象的 onreadystatechange
屬性中定義了請求接收到應(yīng)答是所執(zhí)行的操作。該GET請求的在瀏覽器中的請求信息如圖5所示。
//GET請求
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
console.log("請求成功回調(diào)")
}
};
xhttp.open("GET", "http://localhost:8080/day05_lzs/?fname=Bill&lname=Gates", true);
xhttp.send();
Demo3. 使用XMLHttpRequest發(fā)送GET請求
圖5. 使用XMLHttpRequest發(fā)送GET請求
3.2.2 使用XMLHttpRequest發(fā)送post請求的4種方式
如Demo3所示,使用XMLHttpRequest發(fā)送POST請求,發(fā)送數(shù)據(jù)的方式有4種[8]。4中POST請求在瀏覽器中的請求信息如圖6-9所示,他們具有不同的請求頭content_type,請求體的格式分為“Request Load”和“Form Data”兩種。
//POST請求,發(fā)送數(shù)據(jù)方式一
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://localhost:8080/day05_lzs/", true);
xhttp.send("fname=Bill&lname=Gates");
//POST請求,發(fā)送數(shù)據(jù)方式二
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://localhost:8080/day05_lzs/", true);
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send("fname=Bill&lname=Gates");
//POST請求,發(fā)送數(shù)據(jù)方式三
var xhttp = new XMLHttpRequest();
const formData = new FormData();
formData.append("fname", "Bill");
formData.append("lname", "Gates");
xhttp.open("POST", "http://localhost:8080/day05_lzs/", true);
xhttp.send(formData);
//POST請求,發(fā)送數(shù)據(jù)方式四
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://localhost:8080/day05_lzs/", true);
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.send('{"fname":"Bill","lname":"Gates"}');
Demo3. 使用XMLHttpRequest發(fā)送POST請求時發(fā)送數(shù)據(jù)的四種方式
圖6. 使用XMLHttpRequest發(fā)送POST請求時發(fā)送數(shù)據(jù)方式一
圖7.使用XMLHttpRequest發(fā)送POST請求時發(fā)送數(shù)據(jù)方式二
圖8.使用XMLHttpRequest發(fā)送POST請求時發(fā)送數(shù)據(jù)方式三
圖9. 使用XMLHttpRequest發(fā)送POST請求時發(fā)送數(shù)據(jù)方式四
3.2.3 JQuery.AJAX()簡介[9]
AJAX 組合了XMLHttpRequest 對象、JavaScript 和 HTML DOM;XMLHttpRequest 對象用于從 web 服務(wù)器請求數(shù)據(jù),JavaScript 和 HTML DOM用于顯示或使用數(shù)據(jù)。不同的瀏覽器對于AJAX的使用方式可能有所不同,JQUERY中解決了這個問題。在JQUERY使用ajax只需一行代碼。
//方式一
$.ajax({ url: "redirectTest",method:"GET",async:true,
success: function(){
console.log("ajax請求成功回調(diào)")
}});
//方式二
$.get("redirectTest",function(){console.log("ajax請求成功回調(diào)")});
Demo4. JQuery中發(fā)送ajax請求的2中方式
3.2.4 AJAX請求跨域時的瀏覽器策略
為了減少跨域請求的風(fēng)險(比如csrf),瀏覽器對從腳本中發(fā)起的跨域HTTP請求有嚴(yán)格限制。比如,XMLHttpRequest和Fetch這兩個API都遵循同源策略(same-origin policy),而對form表單提交的、瀏覽器地址欄發(fā)起的、HTML重定向和JavaScript重定向(詳見3.4.2和3.4.3節(jié))等document類型的請求則沒有限制。同源策略是指,瀏覽器只能加載相同初始域名(origin domain)的應(yīng)答,如果要加載其它域名的應(yīng)答,應(yīng)答頭中必須包含必要的CORS頭,比如“Access-Control-Allow-Origin”[10]。如Demo5,由初始域名為localhost:8080向目標(biāo)域名為127.0.0.1:8080發(fā)送ajax請求時,瀏覽器會報出如圖10的“CORS error”錯誤,錯誤的詳細(xì)信息如圖11??梢酝ㄟ^在CORSFilter對請求進(jìn)行攔截(如Demo6),設(shè)置應(yīng)答頭”Access-Control-Allow-Origin:*“,再次發(fā)送跨域請求,瀏覽器不再報出”CORS error“的錯誤。
//這是localhost:8080/day05_lzs請求響應(yīng)的index.html頁面,即初始域名為localhost:8080
var xhttp = new XMLHttpRequest();
//向目標(biāo)域名127.0.0.1:8080發(fā)送跨域請求
xhttp.open("GET", "http://127.0.0.1:8080/day05_lzs/?fname=Bill&lname=Gates", true);
xhttp.send();
Demo5. 初始域名為localhost:8080,向目標(biāo)域名為127.0.0.1:8080發(fā)送ajax請求
圖10. 跨域發(fā)送ajax請求時,瀏覽器報出“CORS error”
圖11. 跨域發(fā)送ajax請求時,瀏覽器控制臺輸出的詳細(xì)錯誤信息
public class CORSFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//允許所有的初始域名(origin domain)加載該應(yīng)答
rep.setHeader("Access-Control-Allow-Origin","*");
}
}
Demo6. 設(shè)置應(yīng)答頭"Access-Control-Allow-Origin",已解決跨域問題
3.3 Fetch請求
3.3.1 Fetch簡介[11]
Fetch API提供了獲取資源的接口,它比XMLHttpRequest更強(qiáng)大和靈活。使用fetch()方法可以發(fā)起請求并獲取資源。fetch()是一個在Window和Worker的context中的全局方法,這使得可以在任何的context下使用fetch方法。fetch()方法有一個參數(shù)必須要有,需要獲取資源的路徑。它應(yīng)答一個Promise,Promise會被解析為Response。
與XMLHttpRequest屬于callback-based API不同,F(xiàn)etch是promise-based的并可以很容易地在service worker中[12][13]使用。Fetch也整合了前沿的HTTP概念,像HTTP中的CORS及其它擴(kuò)展。
一個簡單的fetch請求看起來如下圖:示例中使用了async/await?!癮sync function”聲明了一個async函數(shù),在函數(shù)體中可以使用await。async/await使得異步的、基于promise的代碼實現(xiàn)更加簡潔,避免了額外配置復(fù)雜的promise鏈[14]。
async function logJSONData() {
const response = await fetch("http://example.com/movies.json");
const jsonData = await response.json();
console.log(jsonData);
}
Demo7. 發(fā)起一個簡單的fetch請求
3.3.1.1 案例一:
如Demo8,fetch()方法可以接收包含多個配置的對象作為第二個參數(shù)。mode(請求跨源模式)中可取值為 no-cors, cors, same-origin,默認(rèn)值是cors。出于安全原因,在Chromium中,no-cors
曾一度只在Service Worker中可用,其余環(huán)境會直接拒絕相應(yīng)的promise[15](后來已經(jīng)重新可用[16])。credentials表示請求是否需要帶上認(rèn)證憑據(jù),可取值為omit
、same-origin
、include
,默認(rèn)值為same-origin
。
// Example POST method implementation:
async function postData(url = "", data = {}) {
// Default options are marked with *
const response = await fetch(url, {
method: "POST", // *GET, POST, PUT, DELETE, etc.
mode: "cors", // no-cors, *cors, same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: {
"Content-Type": "application/json",
// 'Content-Type': 'application/x-www-form-urlencoded',
},
redirect: "follow", // manual, *follow, error
referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
body: JSON.stringify(data), // body data type must match "Content-Type" header
});
return response.json(); // parses JSON response into native JavaScript objects
}
postData("https://example.com/answer", { answer: 42 }).then((data) => {
console.log(data); // JSON data parsed by `data.json()` call
});
Demo8. 使用fetch()發(fā)送請求案例一
3.3.1.2 案例二:
如Demo9,使用HTML標(biāo)簽<input type="file" />
、FormData()和fetch()進(jìn)行文件上傳。
async function upload(formData) {
try {
const response = await fetch("https://example.com/profile/avatar", {
method: "PUT",
body: formData,
});
const result = await response.json();
console.log("Success:", result);
} catch (error) {
console.error("Error:", error);
}
}
const formData = new FormData();
const fileField = document.querySelector('input[type="file"]');
formData.append("username", "abc123");
formData.append("avatar", fileField.files[0]);
upload(formData);
Demo9. 使用fetch()發(fā)送請求案例二
3.3.1.3 案例三:
與jQuery.ajax()請求不同,fetch()返回的promise,即使?fàn)顟B(tài)碼為404或500,也不會被判斷為網(wǎng)絡(luò)錯誤而被拒絕。當(dāng)網(wǎng)絡(luò)錯誤發(fā)生或CORS配置錯誤時,才會被拒絕。所以判斷fetch()是否成功包括判斷解析出的promise,判斷Reponse.ok是否為true,如Demo10。
async function fetchImage() {
try {
const response = await fetch("flowers.jpg");
if (!response.ok) {
throw new Error("Network response was not OK");
}
const myBlob = await response.blob();
myImage.src = URL.createObjectURL(myBlob);
} catch (error) {
console.error("There has been a problem with your fetch operation:", error);
}
}
Demo10. 使用fetch()發(fā)送請求案例三
3.3.1.4 案例四:
如Demo11,除了直接將請求路徑直接傳遞到fetch()方法之外,也可以先創(chuàng)建Request()構(gòu)造器,并將該構(gòu)造器作為fetch()方法的參數(shù)傳遞。
async function fetchImage(request) {
try {
const response = await fetch(request);
if (!response.ok) {
throw new Error("Network response was not OK");
}
const myBlob = await response.blob();
myImage.src = URL.createObjectURL(myBlob);
} catch (error) {
console.error("Error:", error);
}
}
const myHeaders = new Headers();
const myRequest = new Request("flowers.jpg", {
method: "GET",
headers: myHeaders,
mode: "cors",
cache: "default",
});
fetchImage(myRequest);
Demo11. 使用fetch()發(fā)送請求案例四
? fetch()與jQuery.ajax()的主要區(qū)別有以下2點(diǎn):
1)當(dāng)應(yīng)答的HTTP狀態(tài)碼為404或500時,fetch()返回的promise不會被作為HTTP錯誤而被拒絕。在服務(wù)器應(yīng)答后,當(dāng)應(yīng)答的狀態(tài)碼在200-299之間時,則reponse.ok的值為true,否則為false。promise被拒絕僅在網(wǎng)絡(luò)錯誤或CORS配置錯誤等而導(dǎo)致請求無法完成時發(fā)生。
2)除非fetch()方法中的credentials配置項配置為include,否則fetch()將:
- 在跨域請求時不發(fā)送cookies;
- 跨域應(yīng)答中不會設(shè)置任何cookies;
- 從2018開始,默認(rèn)的credentials策略默認(rèn)改為same-origin。
3.3.2 Promise
3.3.2.1 Promise簡介
Promise對象代表一個異步操作的完成狀態(tài)和結(jié)果。Promise代表的完成狀態(tài)和結(jié)果可以在將來某個時間點(diǎn)用到。在Demo12中創(chuàng)建了一個簡單的Promise,這個Promise的resolve()方法的參數(shù)值被方法.then()中的成功狀態(tài)下的回調(diào)函數(shù)所用到。Promise的.then()方法有兩個參數(shù),第一個參數(shù)是成功狀態(tài)下的回調(diào)函數(shù),第二個參數(shù)是失敗狀態(tài)下的回調(diào)函數(shù),第一個參數(shù)是必須要有的。.then()方法的返回值還是一個Promise,如果.then()中回調(diào)函數(shù)的完成狀態(tài)為成功,則可以出現(xiàn)像Demo2中的Promise鏈。[17]
const myFirstPromise = new Promise((resolve, reject) => {
resolve("Success!");
});
myFirstPromise.then((successMessage) => {
console.log(`Yay! ${successMessage}`);
});
//輸出結(jié)果為:Yay! Success!
Demo12. 創(chuàng)建一個簡單的Promise和Promise鏈
Demo13將Promise的.then()方法中的回調(diào)函數(shù)抽取獨(dú)立的函數(shù)了。最下面是Promise鏈,它包含3個.then()調(diào)用,1個.catch()調(diào)用,最后是finally()調(diào)用。只有1個.catch()是通常的做法,將promise鏈中所有的失敗狀狀態(tài)的回調(diào)函數(shù)去掉,只需要在promise的最后添加1個.catch()即可。Promise鏈開頭創(chuàng)建Promise時的參數(shù)為函數(shù)tetheredGetNumber,函數(shù)中resolve()表示完成狀態(tài)為成功,reject()表示完成狀態(tài)為失敗??梢钥闯鯬romise代表的完成狀態(tài)和結(jié)果是不確定的。第一個.then()方法中的失敗回調(diào)函數(shù)troubleWithGetNumber是可以去掉的,因為在promise鏈最后有了.catch()方法。.catch(failureCallback)方法可以看成是.then(null,failureCallback)的簡寫。[17]
// To experiment with error handling, "threshold" values cause errors randomly
const THRESHOLD_A = 8; // can use zero 0 to guarantee error
function tetheredGetNumber(resolve, reject) {
const randomInt = Date.now();
const value = randomInt % 10;
if (value < THRESHOLD_A) {
resolve(value);
} else {
reject(`Too large: ${value}`);
}
}
function determineParity(value) {
const isOdd = value % 2 === 1;
return { value, isOdd };
}
function troubleWithGetNumber(reason) {
const err = new Error("Trouble getting number", { cause: reason });
console.error(err);
throw err;
}
function promiseGetWord(parityInfo) {
return new Promise((resolve, reject) => {
const { value, isOdd } = parityInfo;
if (value >= THRESHOLD_A - 1) {
reject(`Still too large: ${value}`);
} else {
parityInfo.wordEvenOdd = isOdd ? "odd" : "even";
resolve(parityInfo);
}
});
}
new Promise(tetheredGetNumber)
.then(determineParity, troubleWithGetNumber)
.then(promiseGetWord)
.then((info) => {
console.log(`Got: ${info.value}, ${info.wordEvenOdd}`);
return info;
})
.catch((reason) => {
if (reason.cause) {
console.error("Had previously handled error");
} else {
console.error(`Trouble with promiseGetWord(): ${reason}`);
}
})
.finally((info) => console.log("All done"));
Demo13. 一個使用Promise鏈的案例
Promise鏈?zhǔn)悄Хㄒ话愕拇嬖冢琍romise鏈的傳統(tǒng)寫法是金字塔式的,看著很不優(yōu)雅。一個簡單的Promise如Demo14所示,這里不考慮Promise是如何創(chuàng)建的,所以用doSomething()表示一個promise對象。Demo14中的createAudioFileAsync()根據(jù)給定的參數(shù)記錄生成了音像文件,并且有2個回調(diào)函數(shù),1個在音像文件創(chuàng)建成功時使用,1個在音像文件創(chuàng)建失敗后使用。示例的傳統(tǒng)寫法如Demo15所示??梢钥闯?,單個.then()方法的promise寫法與傳統(tǒng)寫法差別很小,但使用2個及以上.then()方法的Promise鏈與傳統(tǒng)寫法的差異很大。
//promise調(diào)用的基本結(jié)構(gòu)
const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);
//一個promise示例
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
function successCallback(result) {
console.log(`Audio file ready at URL: ${result}`);
}
function failureCallback(error) {
console.error(`Error generating audio file: ${error}`);
}
Demo14 promise調(diào)用的基本結(jié)構(gòu)與示例
//傳統(tǒng)寫法
createAudioFileAsync(audioSettings, successCallback, failureCallback);
Demo15. promise調(diào)用示例(Demo14)的傳統(tǒng)寫法
在一次執(zhí)行多個異步操作,且下一個異步操作在上一個異步操作成功后被執(zhí)行,并使用上一個異步操作成功后的結(jié)果,是一個常見的需求。連續(xù)進(jìn)行幾個異步操作傳統(tǒng)的寫法是金子塔式的,如Demo16所示。你也可以使用Promise鏈來實現(xiàn)這個需求,如圖Demo17所示。使用Promise鏈?zhǔn)苟鄠€異步操作更簡潔。Demo17也可以使用箭頭函數(shù)來實現(xiàn),如Demo18。
doSomething(function (result) {
doSomethingElse(result, function (newResult) {
doThirdThing(newResult, function (finalResult) {
console.log(`Got the final result: ${finalResult}`);
}, failureCallback);
}, failureCallback);
}, failureCallback);
Demo16. 連續(xù)多個操作的傳統(tǒng)寫法,下一個異步操作在上一個異步操作后執(zhí)行且使用上一個異步操作的結(jié)果
doSomething()
.then(function (result) {
return doSomethingElse(result);
})
.then(function (newResult) {
return doThirdThing(newResult);
})
.then(function (finalResult) {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
Demo17. 使用Promise鏈實現(xiàn)連續(xù)多個操作,下一個異步操作在上一個操作后執(zhí)行且使用上一個異步操作的結(jié)果
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
Demo18. Demo17使用箭頭函數(shù)實現(xiàn)的代碼
3.3.2.2 Promise使用的常見錯誤
Promise使用的常見錯誤一是在Promise的.then()中忘記return了,這時使用使用這個.then()方法的返回值作為參數(shù)的下一個.then()方法將不能正常執(zhí)行。由于無法獲取上一個.then()方法的返回值,這個.then()方法將不會等待上一個.then()方法執(zhí)行完再執(zhí)行,兩個.then()方法的執(zhí)行時異步的,有競爭關(guān)系,如Demo19。如Demo20,如果上一個的.then()方法沒有返回值,那么下一個.then()方法將會更早被調(diào)用,這導(dǎo)致控制臺輸出的listOfIngredients總是為[]。
doSomething()
.then((url) => {
// 忘記return了
fetch(url);
})
.then((result) => {
// 由于上一個.then()方法沒有返回值,這個.then()不會等待上一次.then()方法執(zhí)行完再執(zhí)行,且result為undefined
});
Demo19. Promise使用的常見錯誤.then()方法中忘記return了
const listOfIngredients = [];
doSomething()
.then((url) => {
// 忘記return了
fetch(url)
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
});
})
.then(() => {
console.log(listOfIngredients);
// 總是[],因為上一個.then()方法沒有完成
});
Demo20. Promise使用的常見錯誤.then()方法中忘記return了
Promise的.then()方法沒有返回值是一個常見的錯誤,除此之外還有一些其它的常見錯誤,Demo21這個案例中包含了3個常見的錯誤。第一個錯誤是Promise的.then()方法沒有返回值的錯誤,已經(jīng)介紹過。第二個錯誤是Promise的嵌套是不必要的。第三個錯誤是Promise鏈的結(jié)尾沒有加上.catch()方法。對該案例錯誤修改后的代碼如Demo22。
// 錯誤案例,命中3個錯誤
doSomething()
.then(function (result) {
// 1.忘記return了
// 2.不必要的嵌套
doSomethingElse(result).then((newResult) => doThirdThing(newResult));
})
.then(() => doFourthThing());
// 3.忘記加上.catch()方法
Demo21. Promise使用的常見3個錯誤
doSomething()
.then(function (result) {
return doSomethingElse(result);
})
//如果這個.then()方法的返回值在下一個.then()方法中沒有用到,下一個.then()方法不用寫參數(shù)
.then((newResult) => doThirdThing(newResult))
.then((/* 參數(shù)不用寫*/) => doFourthThing())
.catch((error) => console.error(error));
Demo22. Demo21中常見的3個錯誤修改后的代碼
3.3.2.3 Promise中的拒絕事件(reject_event)
如果一個promise的拒絕事件沒有被任何處理程序處理,他將會冒泡到調(diào)用棧的頂部,主機(jī)需要將其拋出。在web上,當(dāng)一個promise被拒絕,有兩個事件中一個會被發(fā)送到全局范圍(通常是通過window,如果使用了webworker,則通過worker或基于worker的接口)。這2個事件是:
unhandledrejection
當(dāng)一個promise被拒絕但沒有可用的拒絕事件的處理程序時,發(fā)送該事件。
rejectionhandled
當(dāng)一個被拒絕的promise已引起unhandledrejection事件的發(fā)送,再次被拒絕時一個處理程序被附加到被拒絕的promise時發(fā)送該事件。
這兩種情況下,類型為PromiseRejectionEvent的事件都有一個promise的屬性作為成員,這個屬性表明promise已被拒絕;還包括一個reason屬性,這個屬性提供了promise被拒絕的原因。
這使得為promises提供錯誤處理稱為可能,也為你的promise管理的debug問題提供幫助。這些處理程序在每個context中是全局的,因此所有的錯誤不管源頭在哪,都會去到同樣的事件處理程序。
在Node.js中,promise拒絕的處理有些許不同。你可以通過為Node.js的unhandledRejection(注意與js中unhandledrejection事件的大小寫區(qū)別)事件添加處理程序,來捕獲未被處理的拒絕。
process.on("unhandledRejection", (reason, promise) => {
// Add code here to examine the "promise" and "reason" values
});
Demo22. nodejs中promise拒絕的處理
對于node.js,為了阻止錯誤輸出到控制臺,你可以添加process.on監(jiān)聽器,取代瀏覽器運(yùn)行時的preventDefault() 方法。如果你添加了process.on監(jiān)聽器,但是并沒有對被拒絕的promise進(jìn)行處理,那這個被拒絕的信息將被默默忽略掉。你應(yīng)該在監(jiān)聽器中添加代碼以檢查每個被拒絕的promise并確認(rèn)是否實際上由代碼的bug引起。
3.3.2.4 Promise中的異常處理
談到Promise的異常處理,你可能會想到早期金字塔式的連續(xù)異步操作示例中的failurecallback方法,如Demo23所示。在Promise中的異常處理通常如Demo24的方式,在promise鏈的最后調(diào)用.catch()方法。當(dāng)異常發(fā)生時,瀏覽器會沿著Promise鏈從上到下查找.catch()或onrejected處理器。這很大程度上模仿了如Demo25中的同步代碼的工作方式,Demo25的代碼中使用了await/async。Promises解決了一個金字塔式調(diào)用的底層缺陷,它會捕獲所有的錯誤,包括拋出的異常和編碼錯誤。Promise的異常處理是一個異步操作必要的功能組成部分。
doSomething(function (result) {
doSomethingElse(result, function (newResult) {
doThirdThing(newResult, function (finalResult) {
console.log(`Got the final result: ${finalResult}`);
}, failureCallback);
}, failureCallback);
}, failureCallback);
Demo23. 早期金字塔式的連續(xù)異步操作,且下一個異步操作使用上一個異步操作的結(jié)果
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);
Demo24. Promise鏈中使用.catch()進(jìn)行異常處理
async function foo() {
try {
const result = await doSomething();
const newResult = await doSomethingElse(result);
const finalResult = await doThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch (error) {
failureCallback(error);
}
}
Demo25. Promise鏈異常處理修改為使用async/await的同步代碼的方式
3.3.2.5 Promise中的then中的組合寫法
可以通過Promise.all(), Promise.allSettled() ,Promise.any(), Promise.race()等4個組合方法并發(fā)地運(yùn)行異步操作。
Promise.all([func1(), func2(), func3()]).then(([result1, result2, result3]) => {
// use result1, result2 and result3
});
Demo26. Promise.all()組合方法的使用
如果數(shù)組中的一個promise被拒絕,Promise.all()直接拒絕了返回的promise并終止其它操作。這會引起不可預(yù)測狀態(tài)和行為。Promise.allSettled()是另外一個組合方法,它保證了所有的操作在resolve之前全部完成。這些方法中promise的運(yùn)行是并發(fā)的,一系列的promise同時開始運(yùn)行,其中的一個promise不會等待另一個promise。promise組合序列化執(zhí)行可按照Demo27中的寫法。在Demo28中,我們遍歷了要異步執(zhí)行的函數(shù),將其組合成了promise鏈,代碼等同于Demo29。
[func1, func2, func3]
.reduce((p, f) => p.then(f), Promise.resolve())
.then((result3) => {
/* use result3 */
});
Demo27. promise組合序列化寫法一
Promise.resolve()
.then(func1)
.then(func2)
.then(func3)
.then((result3) => {
/* use result3 */
});
Demo28. promise組合序列化寫法二
promise組合序列化執(zhí)行也可以使用async/await來實現(xiàn),如Demo29所示。
let result;
for (const f of [func1, func2, func3]) {
result = await f(result);
}
/* use last result (i.e. result3) */
Demo29. promise組合序列化寫法三
composeAsync()函數(shù)接受任意數(shù)量的函數(shù)作為參數(shù),并返回一個函數(shù)。這個函數(shù)接收通過管道函數(shù)傳遞的一個初始參數(shù)。管道函數(shù)是按順序執(zhí)行的。
const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);
Demo30. composeAsync的使用案例
當(dāng)你將promise進(jìn)行序列化組合時,請先考慮其是否必要。promise并發(fā)地運(yùn)行是更高效的方式,一個promise不會阻塞另一個promise。
3.3.2.6 Promise中的執(zhí)行順序[18]
Promise的回調(diào)函數(shù)隊列和這些函數(shù)何時被調(diào)用是由promise的實現(xiàn)決定的,API的開發(fā)者和使用者均遵循下面的語義:
1.通過then()添加的回調(diào)在當(dāng)前javaScript事件循環(huán)結(jié)束后才會執(zhí)行;
2.通過then()依次添加的多個回調(diào)函數(shù)會依次執(zhí)行;
Demo30的執(zhí)行結(jié)果是確定的。即使是已經(jīng)resolved的promise的then()的回調(diào)函數(shù)也不會被同步執(zhí)行。如Demo31,then()的回調(diào)函數(shù)會被添加到microtask隊列,這些micortask在當(dāng)前事件循環(huán)結(jié)束后(創(chuàng)建microtask的函數(shù)退出),在控制被返回到事件循環(huán)之前執(zhí)行。而setTimeout()的任務(wù)被添加到task隊列,這些task在下一個事件循環(huán)開始時執(zhí)行,setTimeout()后的.then()方法會在setTimeout()任務(wù)執(zhí)行后添加到microtask隊列中。
Promise.resolve().then(() => console.log(2));
console.log(1);
// Logs: 1, 2
Demo30. promise中.then()的回調(diào)函數(shù)執(zhí)行順序案例一
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
wait(0).then(() => console.log(4));
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
console.log(1); // 1, 2, 3, 4
Demo31. promise中.then()的回調(diào)函數(shù)執(zhí)行順序案例二
const promise = new Promise((resolve, reject) => {
console.log("Promise callback");
resolve();
}).then((result) => {
console.log("Promise callback (.then)");
});
setTimeout(() => {
console.log("event-loop cycle: Promise (fulfilled)", promise);
}, 0);
console.log("Promise (pending)", promise);
/*Promise callback
Promise (pending) Promise {<pending>}
Promise callback (.then)
event-loop cycle: Promise (fulfilled) Promise {<fulfilled>}
Demo32. promise中.then()的回調(diào)函數(shù)執(zhí)行順序案例三
3.3.3 Fetch請求跨域時的瀏覽器策略
同jQuery.ajax()一樣,fetch請求也會有跨域問題。如Demo33,由初始域名為localhost:8080向目標(biāo)域名為127.0.0.1:8080發(fā)送fetch請求時,瀏覽器會報出如圖12的“CORS error”錯誤,錯誤的詳細(xì)信息如圖13??梢酝ㄟ^在CORSFilter對應(yīng)答進(jìn)行攔截,設(shè)置應(yīng)答頭”Access-Control-Allow-Origin:*“,再次發(fā)送跨域請求,瀏覽器不再報出”CORS error“的錯誤。
//這是localhost:8080/day05_lzs請求響應(yīng)的index.html頁面,即初始域名為localhost:8080
async function logJSONData() {
//向目標(biāo)域名127.0.0.1:8080發(fā)送跨域請求
const response = await fetch("http://127.0.0.1:8080/day05_lzs/? fname=Bill&lname=Gates");
const jsonData = await response.json();
console.log(jsonData);
}
Demo33 初始域名為localhost:8080,向目標(biāo)域名為127.0.0.1:8080發(fā)送fetch請求
圖12 跨域發(fā)送fetch請求時,瀏覽器報出“CORS error”
圖13. 跨域發(fā)送fetch請求時,瀏覽器控制臺輸出的詳細(xì)錯誤信息
3.4 重定向[19]
3.4.1 HTTP重定向
URL重定向(URL redirection),也稱為URL forwarding,它使一個頁面、表單或網(wǎng)站擁有多個URL地址。HTTP重定向是HTTP的一種應(yīng)答類型。HTTP重定向是在瀏覽器向服務(wù)端發(fā)送請求后,當(dāng)服務(wù)端進(jìn)行重定向應(yīng)答時觸發(fā)的。重定向應(yīng)答的狀態(tài)碼以“3”開頭,應(yīng)答頭“Location”的值是要重定向到的URL。當(dāng)瀏覽器接收到重定向應(yīng)答后,會立馬加載應(yīng)答頭“Location”中的URL。如圖14,在瀏覽器地址輸入"http://localhost:8080/day05_lzs/redirectTest
"并打開后,瀏覽器向服務(wù)端發(fā)送請求,服務(wù)端接收到請求后進(jìn)行了重定向應(yīng)答,狀態(tài)碼為302,應(yīng)答頭”Location“為”http://127.0.0.1:8080/day05_lzs/redirectTest2
“。瀏覽器接收到重定向應(yīng)答后,會立馬加載應(yīng)答頭“Location”中的URL。瀏覽器的地址欄也變?yōu)橹囟ㄏ虻腢RL。
//路徑“redirectTest”映射到的servlet
public class RedirectTestServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.sendRedirect("http://127.0.0.1:8080/day05_lzs/redirectTest2");
}
}
Demo34. 路徑“redirectTest”映射到的servlet
圖14. chrome瀏覽器地址欄發(fā)起請求的HTTP重定向
在xhr/fetch請求進(jìn)行HTTP重定向時,瀏覽器的地址欄不會變?yōu)橹囟ㄏ蚝蟮腢RL。如Demo35、Demo36,當(dāng)我們點(diǎn)擊按鈕觸發(fā)sendAjax()函數(shù)后,會發(fā)起URL為“redirectTest”的XMLHttpRequest請求;服務(wù)端的應(yīng)答重定向到了“ http://127.0.0.1:8080/day05_lzs/redirectTest2
”;URL為“redirectTest2”的重定向請求發(fā)起后,服務(wù)端的應(yīng)答又重定向到了“http://127.0.0.1:8080/day05_lzs/redirectTest3
”。一個xhr/fetch請求的重定向請求可視為ajax/fetch請求的延續(xù)。一方面如果XMLHttpRequest設(shè)置為同步執(zhí)行,則在XMLHttpRequest請求及該請求的所有重定向請求執(zhí)行完成后,才會繼續(xù)執(zhí)行Demo35中的“window.location”代碼;這點(diǎn)在圖15的請求順序中可以看出。另一方面xhr/fetch請求不會改變?yōu)g覽器地址欄的URL,xhr/fetch請求的重定向請求也不會改變?yōu)g覽器地址欄的URL。
<!--index.html-->
<head>
<script>
function sendAjax(){
var xhttp = new XMLHttpRequest();
//將請求設(shè)置為同步
xhttp.open("POST", "redirectTest", false);
xhttp.send()
window.location = "http://localhost:8080/day05_lzs/redirectTestAfter"
}
</script>
</head>
<body>
<button onclick="sendAjax()">發(fā)起ajax請求</button>
</body>
demo35. xhr請求的HTTP重定向案例前端代碼
//路徑“redirectTest”映射到的servlet
public class RedirectTestServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.sendRedirect("http://127.0.0.1:8080/day05_lzs/redirectTest2");
}
}
//路徑“redirectTest2”映射到的servlet
public class RedirectTestServlet2 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
response.sendRedirect("http://127.0.0.1:8080/day05_lzs/redirectTest3");
}
}
Demo36. xhr請求的HTTP重定向案例后端代碼
圖15. xhr請求的HTTP重定向案例在chrome瀏覽器開發(fā)這工具中的請求記錄
重定向分為2種類型,永久重定向和臨時重定向。永久重定向表示原始的URL不應(yīng)繼續(xù)被使用,應(yīng)使用新的URL替代。搜索引擎機(jī)器人、RSS閱讀器或其它爬蟲會更新資源中的原始URL。有時請求的資源所在的規(guī)范URL不可用,但可以通過其它的URL訪問到,此時可以使用臨時重定向。搜索引擎機(jī)器人、RSS閱讀器或其它爬蟲不會記住臨時的URL。臨時重定向也可以用于在創(chuàng)建、更新和刪除資源時,顯示臨時進(jìn)度頁面。
如表1,狀態(tài)碼301和308是永久重定向,302、303、307是臨時重定向。永久重定向為何有2個狀態(tài)碼,臨時重定向為何有3個狀態(tài)碼,它們的區(qū)別是什么?以臨時重定向的3個狀態(tài)碼為例進(jìn)行說明。在HTTP/1.0時臨時重定向只有302這一個狀態(tài)碼,http規(guī)范規(guī)定重定向時不允許改變請求方法;且當(dāng)請求方法不是GET/HEAD時,在重定向前瀏覽器需要詢問客戶是否繼續(xù),非GET/HEAD請求可能會改變請求發(fā)出時的狀態(tài)。第一條重定向不允許改變請求方法的規(guī)定,很多瀏覽器的用戶代理[20]并沒有遵守,它們將302視為303對待。為了消除302狀態(tài)碼的歧義,在HTTP/1.1將302拆分成了303和307。第二條非GET/HEAD的重定向請求需要用戶確認(rèn)的規(guī)定,瀏覽器也沒有實現(xiàn)[21]。
狀態(tài)碼 | 文本 | http規(guī)范的要求[22] | 用戶代理的實現(xiàn) | 典型使用場景 |
---|---|---|---|---|
301 | Moved Permanently | HTTP/1.0重定向時不允許改變請求方法 | GET方法不變。其它方法可能會也可能不會改變?yōu)镚ET方法。 | 網(wǎng)站重組 |
308 | Permanent Redirect | HTTP/1.1由301拆分出來 | 請求方法和請求體不變。 | 網(wǎng)站重組,非GET方法的鏈接或操作 |
302 | Found | HTTP/1.0重定向時不允許改變請求方法 | GET方法不變。其它方法可能會也可能不會改變?yōu)镚ET方法。 | web頁因為不可預(yù)見的原因臨時不可用 |
303 | See Other | HTTP/1.1由302拆分出來 | GET方法不變。其它方法改變?yōu)镚ET方法。 | 在PUT和POST方法后進(jìn)行重定向,刷新頁面不會重復(fù)觸發(fā)已執(zhí)行的操作 |
307 | Temporary Redirect | HTTP/1.1由302拆分出來 | 請求方法和請求體不變。 | web頁因為不可預(yù)見的原因臨時不可用。在使用非GET方法時使用,比302更好 |
表1. http重定向的狀態(tài)碼
如圖16,瀏覽器的用戶代理會以請求頭User_Agent發(fā)送到服務(wù)端,用戶代理是一個特殊的字符串,服務(wù)端可通過該字符串解析出客戶使用的操作系統(tǒng)及版本、CPU類型、瀏覽器及版本、瀏覽器渲染引擎、瀏覽器語言、瀏覽器插件等。圖中User-Agent字符串各部分的解釋如下:
Mozilla/5.0: Netscape Communications 開發(fā)了 web 瀏覽器 Mozilla。凡是基于 WebKit 的瀏覽器都將自己偽裝成了 Mozilla 5.0;
(Windows NT 10.0; Win64; x64): 操作系統(tǒng)windows 10;
AppleWebKit/537.36 (KHTML, like Gecko) : Apple 宣布發(fā)布首款他們自主開發(fā)的 web 瀏覽器:Safari。它的呈現(xiàn)引擎叫 WebKit;Apple公司擔(dān)心用戶不知道WebKit的兼容性,使用(KHTML, like Gecko)讓開發(fā)者知道WebKit像Gecko一樣,是兼容Mozilla瀏覽器的;
Chrome/95.0.4638.54: Google Chrome 瀏覽器及其版本號。Chrome以 WebKit 作為呈現(xiàn)引擎;
Safari/537.36: User-Agent中包括的信息既有 Apple WebKit 的版本,也有 Safari 的版本。
圖16. Chrome瀏覽器中請求頭User-Agent
3.4.2 HTML重定向
HTTP重定向是最好的重定向方式,但有時你無法控制服務(wù)端。你可以通過HTML中a標(biāo)簽的href屬性來實現(xiàn)重定向。HTML重定向是document類型的請求,請求方法式GET,如圖17、18。
<a href="/day05_lzs/redirectTestAfter">重定向測試</a></br>
<a href="http://localhost:8080/day05_lzs/redirectTestAfter">重定向測試</a></br>
Demo37. HTML重定向
圖17. HTML重定向的請求方式
圖18. HTML重定向的請求方法
3.4.3 JavaScript重定向
JavaScript的重定向通過為“window.location”設(shè)置URL來實現(xiàn)的。 JavaScript重定向是document類型的請求,請求方法式GET,如圖19、20。
//相對URL
window.location = "/day05_lzs/redirectTestAfter"
//絕對URL
window.location= "http://localhost:8080/day05_lzs/redirectTestAfter"
//window.location.href的方式
window.location.href = "http://localhost:8080/day05_lzs/redirectTestAfter"
Demo38. JavaScript重定向
圖19. JavaScript重定向的請求方式
圖20. JavaScript重定向的請求方式
引用:
[1] https://developer.mozilla.org/en-US/docs/Web/HTTP
[2]https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
[3] https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
[4] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
[5] https://www.orcode.com/question/1049930_kae50a.html
[6]https://developer.mozilla.org/en-US/docs/Web/API/Request/body
[7] https://www.w3school.com.cn/js/js_ajax_http_send.asp
[8] https://www.cnblogs.com/oxspirt/p/13096737.html
[9] https://www.w3school.com.cn/js/js_ajax_intro.asp
[10] https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
[11] https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
[12] https://blog.csdn.net/Ed7zgeE9X/article/details/124937789
[13] https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
[14] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
[15] https://web.dev/introduction-to-fetch/#why_is_no-cors_supported_in_service_workers_but_not_the_window
[16] https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_response_header_name
[17] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#advanced_example
[18]https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises#composition
[19] https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
[20] https://baike.baidu.com/item/用戶代理/1471005?fr=aladdin
[21] https://www.cnblogs.com/OpenCoder/p/16265950.html文章來源:http://www.zghlxwxcb.cn/news/detail-478330.html
[22] https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html文章來源地址http://www.zghlxwxcb.cn/news/detail-478330.html
到了這里,關(guān)于HTTP請求的幾種方式的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!