同事提問,某報表匯出作業執行很耗時,長達數十秒到一分鐘,為避免使用者分不清作業是否在執行中陷入焦慮(或狂點滑鼠猛按 F5,你懂的), 打算在下載匯出檔過程顯示下載中動畫,但要如何在檔案下載完成時精準結束動畫是個問題。

這個需求用 AJAX 不難解決,當下我便提供了建議。不過,身為程式魔人光用嘴寫程式總覺不夠踏實, 回家後手癢難耐,最後還是寫出會跑的範例才甘心,哈!

先看執行效果。

按鈕後網頁出現蓋版載入中動畫(這裡用的是 busy-load 套件), 後端模擬的匯出檔案動作刻意延遲 3-5 秒才傳回檔案,執行完成時立即關閉動畫下載檔案。

現在來看程式碼。前端很簡單,Button 點擊時以 jQuery.post 呼叫 Export Action, AJAX 呼叫之前執行 .busyLoad("show", { spinner: "accordion" }) 顯示載入中動晝。 Export 若出錯會傳回以 ERROR 起首的錯誤訊息,以 alert() 顯示之。 Export 執行成功回傳的則是下載檔案用的唯一序號(格式為 GUID), 使用以前介紹過的 IFrame 下載檔案技巧以 Download?token=檔案序號 載回檔案。 $.post() 不管成功失敗,在 .always() 時都會關閉載入中動畫。

Index.cshtml

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
    <link href="~/lib/busy-load/busy-load.min.css" rel="stylesheet" />
    <style>
        html,body { height: 100%; font-size: 10pt; }
    </style>
</head>
<body>
    <div>
        <button id="btnExport">匯出檔案</button> (耗時3-5秒)
    </div>
    <script src="~/lib/jquery/jquery.min.js"></script>
    <script src="~/lib/busy-load/busy-load.min.js"></script>
    <script>
        $("#btnExport").click(function () {
            $("body").busyLoad("show", { spinner: "accordion" });
            $.post("@Url.Action("Export")").done(function (res) {
                if (res.indexOf("ERROR") > -1) {
                    alert(res);
                }
                else {
                    var url = "@Url.Action("Download")?token=" + res;
                    //REF: https://blog.darkthread.net/blog/ajax-download-with-iframe/
                    var frm = $("<iframe style='display:none' />");
                    frm.attr("src", url);
                    frm.appendTo("body");
                    frm.on("load", function () {
                        //if the download link return a page
                        //load event will be triggered
                        alert("Error while downloading " + url);
                    });

                }
            }).always(function () {
                $("body").busyLoad("hide");
            });
        });
    </script>
</body>
</html>

伺服器端程式(HomeController.cs)如下,有三個 Action,Index 僅用於帶出 View;Export 內部以亂數延遲 3-5 秒再產生一個模擬檔案及 Guid token, 以 token 為 Key 存入 MemoryCache (保留一分鐘,逾時未取作廢);Download 時接收 token 參數,從 MemoryCache 取出檔案傳回。

using System;
using System.Runtime.Caching;
using System.Text;
using System.Web.Mvc;

namespace SlowExport.Controllers
{
    public class HomeController : Controller
    {
        // GET: Home
        public ActionResult Index()
        {
            return View();
        }

        class FileData
        {
            public string Name;
            public byte[] Content;
        }
        static MemoryCache cache = MemoryCache.Default;
        public ActionResult Export()
        {
            try
            {
                //裝忙,Delay 3-5 秒再傳回結果
                var rnd = new Random();
                System.Threading.Thread.Sleep(rnd.Next(2000) + 3000);
                //胡亂生個檔案
                var file = new FileData
                {
                    Name = $"Hello{DateTime.Now:mmssfff}.txt",
                    Content = Encoding.UTF8.GetBytes("Hello World!")
                };
                //產生取檔Token
                var token = Guid.NewGuid();
                //存入Cache待下載(限一分鐘內有效)
                cache.Add(token.ToString(), file, DateTime.Now.AddMinutes(1));
                return Content(token.ToString());
            }
            catch (Exception ex)
            {
                return Content("ERROR:" + ex.Message);
            }
        }

        public ActionResult Download(string token)
        {
            var file = cache[token] as FileData;
            if (file == null) return HttpNotFound();
            cache.Remove(token);
            return File(file.Content, "application/octet-stream", file.Name);
        }
    }
}

練功完畢。

A sample code to provide better export and download experience with AJAX call and loading animation.


Comments

Be the first to post a comment

Post a comment