前端單兵基本教練 - 徒手製作 AJAX 載入中遮罩
4 |
今天這篇屬於前端開發的單兵基本教練,練習為耗時的 AJAX 呼叫製作載入中狀態顯示,如以下效果:
針對較耗時的 AJAX 呼叫,在等待結果回傳期間顯示轉圈圈之類的載入動畫,安撫使用者情緒提供明確的執行狀態回饋,同時封鎖網頁介面並防止使用者在等待期間繼續操作造成動線混亂。這種效果相信大家一定都看過,類似套件程式庫很多(例如:blockUI、Kendo UI progress、Bootstrap Spinner,但這次打算用最基本的 HTML、CSS、JavaScript 自己動手做一個,目標是搞懂原理,從只會用套件,提升到有修改甚至開發套件的能力。
剖析這種載入中效果,大致分為兩部分:遮罩 + 載入中動畫或文字。遮罩是一層蓋在現有網頁正上方的元素,利用 CSS width: 100%; height: 100%; 讓面積與整張網頁相同,再透過 position: absolute; z-index: 9999; 蓋在網頁上方,最後 background-color: #444; opacity: 0.5; 做出半透明灰色效果;至於載入中動畫,可以用 GIF、SVG 甚至用純 CSS 動畫製作,網路上有不少免費素材可資利用。整個遮罩跟動畫的 HTML 跟 CSS 類似這樣:
<!-- CSS -->
<style>
html,body { height: 100%; margin: 0; }
.loading {
position: absolute; z-index: 9999;
top: 0; left: 0;
width: 100%; height: 100%;
display: none;
}
.loading .mask {
position: absolute;
width: 100%; height: 100%;
background-color: #444; opacity: 0.5;
}
.loading .animation {
width: 64px; height: 64px;
margin: auto; margin-top: 40px;
background: url('https://i.imgur.com/6pCtQAW.gif');
}
</style>
<!-- HTML -->
<div class="loading">
<div class="mask"></div>
<div class="animation">
</div>
</div>
網頁載入時 .loading DIV 預設 display: none 不顯示,呼叫 AJAX 時將 display 改為 block,載入中 DIV 便會蓋在現有網頁上並顯示轉圈 GIF,阻擋使用者繼續操作網頁(注意:此時仍可用鍵盤控制,但正常使用者不致誤用,若要防範惡意破解,則需加入其他安全鎖),待收到結果後再改回 none 恢復正常。
接著來寫一個後端範例配合測試,用一支 getGuid.aspx 模擬耗時數秒的 Web API,並加入模擬執行出錯功能:
<%@ Page Language="C#" %>
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
int delaySec;
var d = Request["d"];
if (d == "E") throw new ApplicationException("ERROR");
if (int.TryParse(Request["d"], out delaySec))
{
System.Threading.Thread.Sleep(delaySec * 1000);
}
Response.Write(Guid.NewGuid().ToString());
}
</script>
前端部分,我們先不要用 jQuery 或 axios,試寫香草口味 JavaScript,確認我們知道 XHR 原理,具備用 JavaScript 實作的能力,用 jQuery/axios 是為了省時省力,而非沒有工具就什麼都不會的廢人。關於 XHR (XMLHttpRequest),MDN Web Docs 有權威且完整的說明。以下是用純 JavaScript 實作的完整範例:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Loading animation demo</title>
<style>
html,body { height: 100%; margin: 0; }
.loading {
position: absolute; z-index: 9999;
top: 0; left: 0;
width: 100%; height: 100%;
display: none;
}
.loading .mask {
position: absolute;
width: 100%; height: 100%;
background-color: #444; opacity: 0.5;
}
.loading .animation {
width: 64px; height: 64px;
margin: auto; margin-top: 40px;
background: url(https://i.imgur.com/6pCtQAW.gif);
}
</style>
</head>
<body>
<select id="selAction">
<option value="1">Delay 1s</option>
<option value="3">Delay 3s</option>
<option value="D">No Host</option>
<option value="N">HTTP 404</option>
<option value="E">HTTP 500</option>
</select>
<button id='btnTest'>Run Test</button><br />
Result = <span id="txtResult"></span>
<div class="loading">
<div class="mask"></div>
<div class="animation">
</div>
</div>
<script>
function toggleLoading(show) {
document.querySelector('.loading').style.display = show ? 'block' : 'none';
}
function callAjax() {
var req = new XMLHttpRequest();
req.addEventListener("load", function () {
toggleLoading(false);
if (req.status == 200)
document.getElementById('txtResult').innerText = req.responseText;
else {
alert("XHR Status=" + req.status);
}
});
req.addEventListener("error", function () {
//failed to get response from remote server
alert("ERROR");
toggleLoading(false);
});
var act = document.getElementById('selAction').value;
if (act == "D") {
req.open("POST", "http://no-such-host.net/null.html");
}
else if (act == "N") {
req.open("POST", "no-such-page.aspx");
}
else {
req.open("POST", "getGuid.aspx?d=" + act);
}
toggleLoading(true);
document.getElementById('txtResult').innerText = "";
req.send();
}
document.getElementById('btnTest')
.addEventListener('click', callAjax);
</script>
</body>
</html>
XHR 的使用方式是先 new XMLHttpRequest() 建立物件,在 load 事件檢查傳回結果,由 status 屬性判斷結果,responseText、responseXML 讀取結果,若連不上伺服器或未得到 HTTP 回應(例如:DNS 有錯、TLS 憑證無效、網站無回應... 等等),則會觸發 error 事件。.open() 指定 GET/POST 及網址,.send() 送出 HTTP 請求,在送出請求前將 .loading 的 style.display 設為 block,load/error 事件執行時代表呼叫已結束,此時再將 .loading style.display 設成 none,就這麼簡單。
品嚐完香草口味,順便溫習一下 jQuery 練練手感:
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$('#btnTest').click(function() {
var act = $('#selAction').val();
var url = "getGuid.aspx?d=" + act;
if (act == "D") url = "http://no-such-host.net/null.html";
else if (act == "N") url = "no-such-page.aspx";
$('.loading').show();
$.post(url)
.done(function (guid) { $('#txtResult').text(guid); })
.fail(function (xhr) { alert("ERROR-" + xhr.status); })
.always(function () { $('.loading').hide(); });
});
</script>
打完收工。
Example of creating a loading mask from scratch when call long-running AJAX.
Comments
# by Alex
你好, 小小離題一下, 我正在作一個 AJAX 文件上傳, 可是發現了一些狀況不知道怎麼解決。 1. 使用此套件作AJAX 文件上傳 https://valums-file-uploader.github.io/file-uploader/ 2. 當網頁閒置一段時間之後再呼叫, 就會有ERROR (XMLHttpRequest: Network Error 0x80030019, An error occurred during a seek operation.) 3. 上網查了一下, 好像沒有什麼解決辦法 不知黑大有沒有一些方法或頭緒, 謝謝
# by Jeffrey
to Alex, 感覺是瀏覽器跟網站的連線中斷造成,建議用 F12 開發工具、Fiddler 或 Wireshark 觀察,看看網路連線是否有異常。(愈進階的工具愈複雜,但能看到的細節愈多)
# by ByTim
載入遮罩可以作在按鈕上,請問跟這個覆蓋全畫面的,有什麼差別?還是看情況使用?
# by Jeffrey
to ByTim, 端看需要,如果 UI 上還有日期選擇器、下拉選單、Checkbox 等元素,如果使用者在等待期間去更動它們會形成困擾的話(甚至可能觸發其他 AJAX 呼叫更新部分頁面元件),若等待時間不長(例如: 3-5秒內),我傾向一口氣封鎖整個網頁比較省事且乾脆。