HTTP(Hypertest Transfer Protocol)是用於傳輸像HTML這樣的超文字檔案的應用層協定。它被設計用於WEB瀏覽器端和WEB伺服器端的互動,但也有其它用途。HTTP遵循經典的client-server模型,使用者端發起請求嘗試建立連線,然後等待伺服器端的應答。HTTP是無狀態協定,這意味著伺服器端在兩次請求間不會記錄任何狀態。
每個請求有一個請求URL。
HTTP定義了一系列請求方法,這些方法表明要對給定資源所做的操作。HTTP請求方法包含GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH等8中型別。
HTTP應答狀態碼錶名一個HTTP請求是否成功完成。應答狀態碼被分為5類:
資訊應答(100
– 199
)
成功應答(200
– 299
)
重定向資訊(300
– 399
)
使用者端錯誤(400
– 499
)
伺服器端錯誤(500
– 599
)
HTTP頭使得使用者端和伺服器端之間可以通過HTTP請求和應答傳遞資訊。HTTP頭包含大小寫敏感的名稱,後面跟一個「:」,然後是http頭的值。HTTP的值前面的空格會被忽略。
請求體(body)是Request介面的一個屬性,它是包含請求體內容的可讀流。GET和HEAD請求不能攜帶請求體,如果攜帶請求體會返回null。
Demo1發起了一個POST型別的請求,在Chrome的開發者工具中可以看到請求URL、請求方法、應答狀態碼、HTTP頭和請求體等資訊,如圖1。
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://localhost:8080/day05_lzs/", true);
xhttp.send("fname=Bill&lname=Gates");
Demo1. 使用XMLHttpRequest發起一個簡單的POST請求
圖1. Chrome瀏覽器中使用XMLHttpRequest發起的一個簡單的POST請求的請求資訊
如圖2,從Chrome瀏覽器的開發者工具可以看出,請求的型別分為Fetch/XHR、JS、CSS、Img、Media、Font、Doc、WS (WebSocket)、 Wasm (WebAssembly)、Manifest和Other(不屬於前面列出型別中的一種)。通過瀏覽器位址列發起的請求和form表單請求的型別都是document。
圖2. Chrome瀏覽器開發者工具中請求的幾種型別
當我們存取一個web頁面時,在瀏覽器位址列輸入存取的地址並確認後,會發起document型別的請求,如圖3。
圖3. 使用Chrome瀏覽器在位址列發起的document型別的請求
Demo2是一個form表單。當form表單提交時,會傳送一個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表單提交時發起document型別的請求
XMLHttpRequest(XHR) 物件用於和伺服器端互動。它可以從URL取回資料而不用重新整理整個頁面,這使得可以只更新頁面的區域性而不影響整個頁面。[7]
如Demo3所示,使用XMLHttpRequest傳送GET請求,在URL中新增「?fname=Bill&lname=Gates」實現傳送資訊。XMLHttpResponse 物件的 onreadystatechange
屬性中定義了請求接收到應答是所執行的操作。該GET請求的在瀏覽器中的請求資訊如圖5所示。
//GET請求
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
console.log("請求成功回撥")
}
};
xhttp.open("GET", "http://localhost:8080/day05_lzs/?fname=Bill&lname=Gates", true);
xhttp.send();
Demo3. 使用XMLHttpRequest傳送GET請求
圖5. 使用XMLHttpRequest傳送GET請求
如Demo3所示,使用XMLHttpRequest傳送POST請求,傳送資料的方式有4種[8]。4中POST請求在瀏覽器中的請求資訊如圖6-9所示,他們具有不同的請求頭content_type,請求體的格式分為「Request Load」和「Form Data」兩種。
//POST請求,傳送資料方式一
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "http://localhost:8080/day05_lzs/", true);
xhttp.send("fname=Bill&lname=Gates");
//POST請求,傳送資料方式二
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請求,傳送資料方式三
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請求,傳送資料方式四
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傳送POST請求時傳送資料的四種方式
(https://img2023.cnblogs.com/blog/1389306/202306/1389306-20230610223139651-880342569.png)
圖6. 使用XMLHttpRequest傳送POST請求時傳送資料方式一
圖7.使用XMLHttpRequest傳送POST請求時傳送資料方式二
圖8.使用XMLHttpRequest傳送POST請求時傳送資料方式三
圖9. 使用XMLHttpRequest傳送POST請求時傳送資料方式四
AJAX 組合了XMLHttpRequest 物件、JavaScript 和 HTML DOM;XMLHttpRequest 物件用於從 web 伺服器請求資料,JavaScript 和 HTML DOM用於顯示或使用資料。不同的瀏覽器對於AJAX的使用方式可能有所不同,JQUERY中解決了這個問題。在JQUERY使用ajax只需一行程式碼。
//方式一
$.ajax({ url: "redirectTest",method:"GET",async:true,
success: function(){
console.log("ajax請求成功回撥")
}});
//方式二
$.get("redirectTest",function(){console.log("ajax請求成功回撥")});
Demo4. JQuery中傳送ajax請求的2中方式
為了減少跨域請求的風險(比如csrf),瀏覽器對從指令碼中發起的跨域HTTP請求有嚴格限制。比如,XMLHttpRequest和Fetch這兩個API都遵循同源策略(same-origin policy),而對form表單提交的、瀏覽器位址列發起的、HTML重定向和JavaScript重定向(詳見3.4.2和3.4.3節)等document型別的請求則沒有限制。同源策略是指,瀏覽器只能載入相同初始域名(origin domain)的應答,如果要載入其它域名的應答,應答頭中必須包含必要的CORS頭,比如「Access-Control-Allow-Origin」[10]。如Demo5,由初始域名為localhost:8080向目標域名為127.0.0.1:8080傳送ajax請求時,瀏覽器會報出如圖10的「CORS error」錯誤,錯誤的詳細資訊如圖11。可以通過在CORSFilter對應答進行攔截(如Demo6),設定應答頭」Access-Control-Allow-Origin:*「,再次傳送跨域請求,瀏覽器不再報出」CORS error「的錯誤。
//這是localhost:8080/day05_lzs請求響應的index.html頁面,即初始域名為localhost:8080
var xhttp = new XMLHttpRequest();
//向目標域名127.0.0.1:8080傳送跨域請求
xhttp.open("GET", "http://127.0.0.1:8080/day05_lzs/?fname=Bill&lname=Gates", true);
xhttp.send();
Demo5. 初始域名為localhost:8080,向目標域名為127.0.0.1:8080傳送ajax請求
圖10. 跨域傳送ajax請求時,瀏覽器報出「CORS error」
圖11. 跨域傳送ajax請求時,瀏覽器控制檯輸出的詳細錯誤資訊
public class CORSFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//允許所有的初始域名(origin domain)載入該應答
rep.setHeader("Access-Control-Allow-Origin","*");
}
}
Demo6. 設定應答頭"Access-Control-Allow-Origin",已解決跨域問題
Fetch API提供了獲取資源的介面,它比XMLHttpRequest更強大和靈活。使用fetch()方法可以發起請求並獲取資源。fetch()是一個在Window和Worker的context中的全域性方法,這使得可以在任何的context下使用fetch方法。fetch()方法有一個引數必須要有,需要獲取資源的路徑。它應答一個Promise,Promise會被解析為Response。
與XMLHttpRequest屬於callback-based API不同,Fetch是promise-based的並可以很容易地在service worker中[12][13]使用。Fetch也整合了前沿的HTTP概念,像HTTP中的CORS及其它擴充套件。
一個簡單的fetch請求看起來如下圖:範例中使用了async/await。「async function」宣告了一個async函數,在函數體中可以使用await。async/await使得非同步的、基於promise的程式碼實現更加簡潔,避免了額外設定複雜的promise鏈[14]。
async function logJSONData() {
const response = await fetch("http://example.com/movies.json");
const jsonData = await response.json();
console.log(jsonData);
}
Demo7. 發起一個簡單的fetch請求
如Demo8,fetch()方法可以接收包含多個設定的物件作為第二個引數。mode(請求跨源模式)中可取值為 no-cors, cors, same-origin,預設值是cors。出於安全原因,在Chromium中,no-cors
曾一度只在Service Worker中可用,其餘環境會直接拒絕相應的promise[15](後來已經重新可用[16])。credentials表示請求是否需要帶上認證憑據,可取值為omit
、same-origin
、include
,預設值為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()傳送請求案例一
如Demo9,使用HTML標籤<input type="file" />
、FormData()和fetch()進行檔案上傳。
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()傳送請求案例二
與jQuery.ajax()請求不同,fetch()返回的promise,即使狀態碼為404或500,也不會被判斷為網路錯誤而被拒絕。當網路錯誤發生或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()傳送請求案例三
如Demo11,除了直接將請求路徑直接傳遞到fetch()方法之外,也可以先建立Request()構造器,並將該構造器作為fetch()方法的引數傳遞。
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()傳送請求案例四
fetch()與jQuery.ajax()的主要區別有以下2點:
1)當應答的HTTP狀態碼為404或500時,fetch()返回的promise不會被作為HTTP錯誤而被拒絕。在伺服器應答後,當應答的狀態碼在200-299之間時,則reponse.ok的值為true,否則為false。promise被拒絕僅在網路錯誤或CORS設定錯誤等而導致請求無法完成時發生。
2)除非fetch()方法中的credentials設定項設定為include,否則fetch()將:
Promise物件代表一個非同步操作的完成狀態和結果。Promise代表的完成狀態和結果可以在將來某個時間點用到。在Demo12中建立了一個簡單的Promise,這個Promise的resolve()方法的引數值被方法.then()中的成功狀態下的回撥函數所用到。Promise的.then()方法有兩個引數,第一個引數是成功狀態下的回撥函數,第二個引數是失敗狀態下的回撥函數,第一個引數是必須要有的。.then()方法的返回值還是一個Promise,如果.then()中回撥函數的完成狀態為成功,則可以出現像Demo2中的Promise鏈。[17]
const myFirstPromise = new Promise((resolve, reject) => {
resolve("Success!");
});
myFirstPromise.then((successMessage) => {
console.log(`Yay! ${successMessage}`);
});
//輸出結果為:Yay! Success!
Demo12. 建立一個簡單的Promise和Promise鏈
Demo13將Promise的.then()方法中的回撥函數抽取獨立的函數了。最下面是Promise鏈,它包含3個.then()呼叫,1個.catch()呼叫,最後是finally()呼叫。只有1個.catch()是通常的做法,將promise鏈中所有的失敗狀狀態的回撥函數去掉,只需要在promise的最後新增1個.catch()即可。Promise鏈開頭建立Promise時的引數為函數tetheredGetNumber,函數中resolve()表示完成狀態為成功,reject()表示完成狀態為失敗。可以看出Promise代表的完成狀態和結果是不確定的。第一個.then()方法中的失敗回撥函數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鏈是魔法一般的存在,Promise鏈的傳統寫法是金字塔式的,看著很不優雅。一個簡單的Promise如Demo14所示,這裡不考慮Promise是如何建立的,所以用doSomething()表示一個promise物件。Demo14中的createAudioFileAsync()根據給定的引數記錄生成了音像檔案,並且有2個回撥函數,1個在音像檔案建立成功時使用,1個在音像檔案建立失敗後使用。範例的傳統寫法如Demo15所示。可以看出,單個.then()方法的promise寫法與傳統寫法差別很小,但使用2個及以上.then()方法的Promise鏈與傳統寫法的差異很大。
//promise呼叫的基本結構
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呼叫的基本結構與範例
//傳統寫法
createAudioFileAsync(audioSettings, successCallback, failureCallback);
Demo15. promise呼叫範例(Demo14)的傳統寫法
在一次執行多個非同步操作,且下一個非同步操作在上一個非同步操作成功後被執行,並使用上一個非同步操作成功後的結果,是一個常見的需求。連續進行幾個非同步操作傳統的寫法是金子塔式的,如Demo16所示。你也可以使用Promise鏈來實現這個需求,如圖Demo17所示。使用Promise鏈使多個非同步操作更簡潔。Demo17也可以使用箭頭函數來實現,如Demo18。
doSomething(function (result) {
doSomethingElse(result, function (newResult) {
doThirdThing(newResult, function (finalResult) {
console.log(`Got the final result: ${finalResult}`);
}, failureCallback);
}, failureCallback);
}, failureCallback);
Demo16. 連續多個操作的傳統寫法,下一個非同步操作在上一個非同步操作後執行且使用上一個非同步操作的結果
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鏈實現連續多個操作,下一個非同步操作在上一個操作後執行且使用上一個非同步操作的結果
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
Demo18. Demo17使用箭頭函數實現的程式碼
Promise使用的常見錯誤一是在Promise的.then()中忘記return了,這時使用使用這個.then()方法的返回值作為引數的下一個.then()方法將不能正常執行。由於無法獲取上一個.then()方法的返回值,這個.then()方法將不會等待上一個.then()方法執行完再執行,兩個.then()方法的執行時非同步的,有競爭關係,如Demo19。如Demo20,如果上一個的.then()方法沒有返回值,那麼下一個.then()方法將會更早被呼叫,這導致控制檯輸出的listOfIngredients總是為[]。
doSomething()
.then((url) => {
// 忘記return了
fetch(url);
})
.then((result) => {
// 由於上一個.then()方法沒有返回值,這個.then()不會等待上一次.then()方法執行完再執行,且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()方法沒有返回值的錯誤,已經介紹過。第二個錯誤是Promise的巢狀是不必要的。第三個錯誤是Promise鏈的結尾沒有加上.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()方法不用寫引數
.then((newResult) => doThirdThing(newResult))
.then((/* 引數不用寫*/) => doFourthThing())
.catch((error) => console.error(error));
Demo22. Demo21中常見的3個錯誤修改後的程式碼
如果一個promise的拒絕事件沒有被任何處理程式處理,他將會冒泡到呼叫棧的頂部,主機需要將其丟擲。在web上,當一個promise被拒絕,有兩個事件中一個會被傳送到全域性範圍(通常是通過window,如果使用了webworker,則通過worker或基於worker的介面)。這2個事件是:
unhandledrejection
當一個promise被拒絕但沒有可用的拒絕事件的處理程式時,傳送該事件。
rejectionhandled
當一個被拒絕的promise已引起unhandledrejection事件的傳送,再次被拒絕時一個處理程式被附加到被拒絕的promise時傳送該事件。
這兩種情況下,型別為PromiseRejectionEvent的事件都有一個promise的屬性作為成員,這個屬性表明promise已被拒絕;還包括一個reason屬性,這個屬性提供了promise被拒絕的原因。
這使得為promises提供錯誤處理稱為可能,也為你的promise管理的debug問題提供幫助。這些處理程式在每個context中是全域性的,因此所有的錯誤不管源頭在哪,都會去到同樣的事件處理程式。
在Node.js中,promise拒絕的處理有些許不同。你可以通過為Node.js的unhandledRejection(注意與js中unhandledrejection事件的大小寫區別)事件新增處理程式,來捕獲未被處理的拒絕。
process.on("unhandledRejection", (reason, promise) => {
// Add code here to examine the "promise" and "reason" values
});
Demo22. nodejs中promise拒絕的處理
對於node.js,為了阻止錯誤輸出到控制檯,你可以新增process.on監聽器,取代瀏覽器執行時的preventDefault() 方法。如果你新增了process.on監聽器,但是並沒有對被拒絕的promise進行處理,那這個被拒絕的資訊將被默默忽略掉。你應該在監聽器中新增程式碼以檢查每個被拒絕的promise並確認是否實際上由程式碼的bug引起。
談到Promise的例外處理,你可能會想到早期金字塔式的連續非同步操作範例中的failurecallback方法,如Demo23所示。在Promise中的例外處理通常如Demo24的方式,在promise鏈的最後呼叫.catch()方法。當異常發生時,瀏覽器會沿著Promise鏈從上到下查詢.catch()或onrejected處理器。這很大程度上模仿瞭如Demo25中的同步程式碼的工作方式,Demo25的程式碼中使用了await/async。Promises解決了一個金字塔式呼叫的底層缺陷,它會捕獲所有的錯誤,包括丟擲的異常和編碼錯誤。Promise的例外處理是一個非同步操作必要的功能組成部分。
doSomething(function (result) {
doSomethingElse(result, function (newResult) {
doThirdThing(newResult, function (finalResult) {
console.log(`Got the final result: ${finalResult}`);
}, failureCallback);
}, failureCallback);
}, failureCallback);
Demo23. 早期金字塔式的連續非同步操作,且下一個非同步操作使用上一個非同步操作的結果
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);
Demo24. Promise鏈中使用.catch()進行例外處理
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的同步程式碼的方式
可以通過Promise.all(), Promise.allSettled() ,Promise.any(), Promise.race()等4個組合方法並行地執行非同步操作。
Promise.all([func1(), func2(), func3()]).then(([result1, result2, result3]) => {
// use result1, result2 and result3
});
Demo26. Promise.all()組合方法的使用
如果陣列中的一個promise被拒絕,Promise.all()直接拒絕了返回的promise並終止其它操作。這會引起不可預測狀態和行為。Promise.allSettled()是另外一個組合方法,它保證了所有的操作在resolve之前全部完成。這些方法中promise的執行是並行的,一系列的promise同時開始執行,其中的一個promise不會等待另一個promise。promise組合序列化執行可按照Demo27中的寫法。在Demo28中,我們遍歷了要非同步執行的函數,將其組合成了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組合序列化執行也可以使用async/await來實現,如Demo29所示。
let result;
for (const f of [func1, func2, func3]) {
result = await f(result);
}
/* use last result (i.e. result3) */
Demo29. promise組合序列化寫法三
composeAsync()函數接受任意數量的函數作為引數,並返回一個函數。這個函數接收通過管道函數傳遞的一個初始引數。管道函數是按順序執行的。
const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);
Demo30. composeAsync的使用案例
當你將promise進行序列化組合時,請先考慮其是否必要。promise並行地執行是更高效的方式,一個promise不會阻塞另一個promise。
Promise的回撥函數佇列和這些函數何時被呼叫是由promise的實現決定的,API的開發者和使用者均遵循下面的語意:
1.通過then()新增的回撥在當前javaScript事件迴圈結束後才會執行;
2.通過then()依次新增的多個回撥函數會依次執行;
Demo30的執行結果是確定的。即使是已經resolved的promise的then()的回撥函數也不會被同步執行。如Demo31,then()的回撥函數會被新增到microtask佇列,這些micortask在當前事件迴圈結束後(建立microtask的函數退出),在控制被返回到事件迴圈之前執行。而setTimeout()的任務被新增到task佇列,這些task在下一個事件迴圈開始時執行,setTimeout()後的.then()方法會在setTimeout()任務執行後新增到microtask佇列中。
Promise.resolve().then(() => console.log(2));
console.log(1);
// Logs: 1, 2
Demo30. promise中.then()的回撥函數執行順序案例一
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()的回撥函數執行順序案例二
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()的回撥函數執行順序案例三
同jQuery.ajax()一樣,fetch請求也會有跨域問題。如Demo33,由初始域名為localhost:8080向目標域名為127.0.0.1:8080傳送fetch請求時,瀏覽器會報出如圖12的「CORS error」錯誤,錯誤的詳細資訊如圖13。可以通過在CORSFilter對應答進行攔截,設定應答頭」Access-Control-Allow-Origin:*「,再次傳送跨域請求,瀏覽器不再報出」CORS error「的錯誤。
//這是localhost:8080/day05_lzs請求響應的index.html頁面,即初始域名為localhost:8080
async function logJSONData() {
//向目標域名127.0.0.1:8080傳送跨域請求
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,向目標域名為127.0.0.1:8080傳送fetch請求
圖12 跨域傳送fetch請求時,瀏覽器報出「CORS error」
圖13. 跨域傳送fetch請求時,瀏覽器控制檯輸出的詳細錯誤資訊
URL重定向(URL redirection),也稱為URL forwarding,它使一個頁面、表單或網站擁有多個URL地址。HTTP重定向是HTTP的一種應答型別。HTTP重定向是在瀏覽器向伺服器端傳送請求後,當伺服器端進行重定向應答時觸發的。重定向應答的狀態碼以「3」開頭,應答頭「Location」的值是要重定向到的URL。當瀏覽器接收到重定向應答後,會立馬載入應答頭「Location」中的URL。如圖14,在瀏覽器地址輸入"http://localhost:8080/day05_lzs/redirectTest
"並開啟後,瀏覽器向伺服器端傳送請求,伺服器端接收到請求後進行了重定向應答,狀態碼為302,應答頭」Location「為」http://127.0.0.1:8080/day05_lzs/redirectTest2
「。瀏覽器接收到重定向應答後,會立馬載入應答頭「Location」中的URL。瀏覽器的位址列也變為重定向的URL。
//路徑「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瀏覽器位址列發起請求的HTTP重定向
在xhr/fetch請求進行HTTP重定向時,瀏覽器的位址列不會變為重定向後的URL。如Demo35、Demo36,當我們點選按鈕觸發sendAjax()函數後,會發起URL為「redirectTest」的XMLHttpRequest請求;伺服器端的應答重定向到了「 http://127.0.0.1:8080/day05_lzs/redirectTest2
」;URL為「redirectTest2」的重定向請求發起後,伺服器端的應答又重定向到了「http://127.0.0.1:8080/day05_lzs/redirectTest3
」。一個xhr/fetch請求的重定向請求可視為ajax/fetch請求的延續。一方面如果XMLHttpRequest設定為同步執行,則在XMLHttpRequest請求及該請求的所有重定向請求執行完成後,才會繼續執行Demo35中的「window.location」程式碼;這點在圖15的請求順序中可以看出。另一方面xhr/fetch請求不會改變瀏覽器位址列的URL,xhr/fetch請求的重定向請求也不會改變瀏覽器位址列的URL。
<!--index.html-->
<head>
<script>
function sendAjax(){
var xhttp = new XMLHttpRequest();
//將請求設定為同步
xhttp.open("POST", "redirectTest", false);
xhttp.send()
window.location = "http://localhost:8080/day05_lzs/redirectTestAfter"
}
</script>
</head>
<body>
<button onclick="sendAjax()">發起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瀏覽器開發這工具中的請求記錄
重定向分為2種型別,永久重定向和臨時重定向。永久重定向表示原始的URL不應繼續被使用,應使用新的URL替代。搜尋引擎機器人、RSS閱讀器或其它爬蟲會更新資源中的原始URL。有時請求的資源所在的規範URL不可用,但可以通過其它的URL存取到,此時可以使用臨時重定向。搜尋引擎機器人、RSS閱讀器或其它爬蟲不會記住臨時的URL。臨時重定向也可以用於在建立、更新和刪除資源時,顯示臨時進度頁面。
如表1,狀態碼301和308是永久重定向,302、303、307是臨時重定向。永久重定向為何有2個狀態碼,臨時重定向為何有3個狀態碼,它們的區別是什麼?以臨時重定向的3個狀態碼為例進行說明。在HTTP/1.0時臨時重定向只有302這一個狀態碼,http規範規定重定向時不允許改變請求方法;且當請求方法不是GET/HEAD時,在重定向前瀏覽器需要詢問客戶是否繼續,非GET/HEAD請求可能會改變請求發出時的狀態。第一條重定向不允許改變請求方法的規定,很多瀏覽器的使用者代理[20]並沒有遵守,它們將302視為303對待。為了消除302狀態碼的歧義,在HTTP/1.1將302拆分成了303和307。第二條非GET/HEAD的重定向請求需要使用者確認的規定,瀏覽器也沒有實現[21]。
狀態碼 | 文字 | http規範的要求[22] | 使用者代理的實現 | 典型使用場景 |
---|---|---|---|---|
301 | Moved Permanently | HTTP/1.0重定向時不允許改變請求方法 | GET方法不變。其它方法可能會也可能不會改變為GET方法。 | 網站重組 |
308 | Permanent Redirect | HTTP/1.1由301拆分出來 | 請求方法和請求體不變。 | 網站重組,非GET方法的連結或操作 |
302 | Found | HTTP/1.0重定向時不允許改變請求方法 | GET方法不變。其它方法可能會也可能不會改變為GET方法。 | web頁因為不可預見的原因臨時不可用 |
303 | See Other | HTTP/1.1由302拆分出來 | GET方法不變。其它方法改變為GET方法。 | 在PUT和POST方法後進行重定向,重新整理頁面不會重複觸發已執行的操作 |
307 | Temporary Redirect | HTTP/1.1由302拆分出來 | 請求方法和請求體不變。 | web頁因為不可預見的原因臨時不可用。在使用非GET方法時使用,比302更好 |
表1. http重定向的狀態碼
如圖16,瀏覽器的使用者代理會以請求頭User_Agent傳送到伺服器端,使用者代理是一個特殊的字串,伺服器端可通過該字串解析出客戶使用的作業系統及版本、CPU型別、瀏覽器及版本、瀏覽器渲染引擎、瀏覽器語言、瀏覽器外掛等。圖中User-Agent字串各部分的解釋如下:
Mozilla/5.0: Netscape Communications 開發了 web 瀏覽器 Mozilla。凡是基於 WebKit 的瀏覽器都將自己偽裝成了 Mozilla 5.0;
(Windows NT 10.0; Win64; x64): 作業系統windows 10;
AppleWebKit/537.36 (KHTML, like Gecko) : Apple 宣佈釋出首款他們自主開發的 web 瀏覽器:Safari。它的呈現引擎叫 WebKit;Apple公司擔心使用者不知道WebKit的相容性,使用(KHTML, like Gecko)讓開發者知道WebKit像Gecko一樣,是相容Mozilla瀏覽器的;
Chrome/95.0.4638.54: Google Chrome 瀏覽器及其版本號。Chrome以 WebKit 作為呈現引擎;
Safari/537.36: User-Agent中包括的資訊既有 Apple WebKit 的版本,也有 Safari 的版本。
圖16. Chrome瀏覽器中請求頭User-Agent
HTTP重定向是最好的重定向方式,但有時你無法控制伺服器端。你可以通過HTML中a標籤的href屬性來實現重定向。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重定向的請求方法
JavaScript的重定向通過為「window.location」設定URL來實現的。 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/
[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
[18]https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
[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
[22] https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html