煩人的運營后台導出大批量數據


線上運行的業務已經跑了一段時間了,運營需要定期導出數據作分析,領導把小D叫過來說這個需求比較緊急,需要盡快上線,小D信誓旦旦的說沒問題,一會兒就搞定。

小D水平還不錯,果然,用了不到2小時時間就把導出做好了。小D是這么實現的,做了個新的接口,接口里面循環處理數據列表然后輸出,瀏覽器收到response后根據header信息將文件下載下來,在測試環境試了下沒問題就上線了,然后處理其他事情去了。

第二天一大早,小D正在地鐵上看着自己漲停的股票偷着樂呢,領導電話救過來了,讓小D趕緊來公司,小D還在想難道領導因為我昨天做得好要表揚我?聽剛才領導的語氣不太像啊!

一到公司小D就找領導去了,領導板着臉,面無表情地說,你看你昨天做的啥東西,導出的列表跟線上的列表差了好多,快看看出啥問題了。

小D果然水平不錯,用了不到10分鍾就查到問題了,線上數據量太大,循環超過時間之后PHP腳本就退出了,導致數據導出一半就斷掉了。小D分析了下,現在超時時間是5s,時間太短了,按現在數據量估算大概得20s左右,於是小D很快出了新的方案:導出腳本執行時間延長到60s,這樣就不會有問題了。很快新的方案上線了,領導還特意親自操作了下,果然完整的導出來了,領導臉上漏出了欣慰小笑容。

由於大家的共同努力,業務發展迅速,過了2個月,導出不完整的問題又出現了。領導很生氣,下班前,把小D交到辦公室,讓小D把剛他的實現思路講一遍。小D巴拉巴拉.....一會兒就說完了。領導聽完之后,沉思了幾秒,對小D說,我給你講個故事吧:”小李生病了,耳朵不太好,自己放屁的聲音也聽不見,就跑到醫院去看大夫,大夫給小李開了3頓葯,讓小李每頓飯后吃。小李問大夫,醫生,我吃完葯耳朵就好了是吧,醫生微微抬起頭,扶了扶眼鏡說,不是,吃完這個葯之后屁聲兒大。“。聽到這里,不爭氣的小李沒憋住,噗嗤笑出來了,領導氣不打一處來,正要發火,這個時候小李手機摔地上了,碰到了手機按鍵,屏幕亮了,領導看到了小D的屏保,也就是小D的女朋友,心里一緊一顫一哆嗦,剛才的火又完全消下去了,心想,小D這個朋友我交定了。領導又語重心長的對小D說,我給你講這個故事是想讓你知道解決問題不能治標不治本,不能因為業務的發展,功能直接done掉,最好能做到不受業務發展影響,這樣吧,我給你個思路,html中有個標簽<a download="downlaod.txt" href="data:text/txt;charset=utf-8,download Test Data">download</a>,點擊它就可以將這個文件下載下來,你可以在前端做個buffer,將所有內容請求完之后再一次下載下來。小D半信半疑的點點頭,領導皺着眉頭說你聽明白了嗎,如果沒明白晚上去你家里給你輔導輔導。小D刷一下子回過神來,連連點頭說明白了明白了。

小D回家之后,沒顧得吃飯就打開電腦,沿着領導的線索去解決問題。

<!DOCTYPE html>
<html>
<head>
  <title></title>
</head>
<body>
  <a download="測試.txt" href="data:text/txt;charset=utf-8,你好,Hello world, 這是一個測試">下載</a>
</body>
</html>

當運行上面的代碼后,點擊下載,果然下載了一個名叫測試.txt的,文件內容是你好,Hello world, 這是一個測試的文件,小D想,要是我把循環移到前端來,每次請求結果放到buffer中,請求完之后,將結果寫入a標簽的herf中,然后觸發點擊動作,不是就下載下來了嗎,這樣就不會因為數據量的增大而導出中斷了。小D竊喜,很快,小D做了下面的模擬

<!DOCTYPE html>
<html>
<head>
  <title></title>
  <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
  <script type="text/javascript">
    $(function() {
      var data = "";
      for (var i =0; i< 100; i++) {
        data += "" + i +"\n";
      }
      $("#test").attr("href", "data:text/csv;charset=utf-8,"+data);
      $("#test")[0].click();
    });
  </script>
</head>
<body>
  <a id="test" download="測試.txt" href="#">下載</a>
</body>
</html>

當我們打開頁面的時候就會自動下載一個名為測試.txt的文件,其中for循環模擬的是ajax請求,每次請求是一頁的數據。

小D在github上面搜了下,發現還真有人已經對這個操作做了封裝,開開心心的將https://github.com/eligrey/FileSaver.js項目下載到本地,結合自己的業務做了第一版出來,其中結合了bootstrap,加了進度條,導出時間比較長的時候心里有個數:

# 需包含FileServer.js,並修改,去掉代碼最前面的var
function ExportData(dataUrl, params, paramPageName, pageStart, pageEnd, exportName) {
    this.dataUrl = dataUrl;
    this.params = params;
    this.paramPageName = paramPageName;
    this.pageStart = pageStart;
    this.pageEnd = pageEnd;
    this.exportName = exportName;
    this.buffers = [];
}

ExportData.prototype.toggleProgressBar = function (show, percent) {
    show = !!show;
    var id = "export-progress-bar";
    if (show) {
        if (percent) {
            var showPrecent = "" + percent + "%";
            $("#"+id + " div.progress-bar").css("width", showPrecent).html(showPrecent);
        } else {
            var html = "<div id='" + id + "' style='position:absolute;padding: 100px;width: 100%;height: 100%;top: 0;left: 0;background: #000;opacity: 0.8;z-index: 99'>" +
                "<div class='progress' style='margin-top: 200px'>" +
                "<div class='progress-bar' style='width: 0%'>" +
                "0%" +
                "</div>" +
                "</div>" +
                "</div>";
            $("body").append($(html));
        }

    } else {
        $("#"+id).remove();
    }
}

/**
 * 導出
 */
ExportData.prototype.export = function () {
    var _ExportData = this;
    function sleep (time) {
        return new Promise((resolve) => setTimeout(resolve, time));
    }
    (async function () {
        _ExportData.toggleProgressBar(true);
        await sleep(50);
        _ExportData.params[_ExportData.paramPageName] = _ExportData.pageStart;
        while (true) {
            if (_ExportData.params[_ExportData.paramPageName] > _ExportData.pageEnd) {
                break;
            }
            var showPercent = 100 * Math.ceil(_ExportData.params[_ExportData.paramPageName] - _ExportData.pageStart + 1) / (_ExportData.pageEnd - _ExportData.pageStart + 1);
            showPercent = showPercent.toFixed(2);

            console.log("開始處理第["+_ExportData.params[_ExportData.paramPageName]+"]頁數據");
            $.ajax({
                url: _ExportData.dataUrl,
                async: false,
                type: "get",
                dataType:"text",
                data: _ExportData.params,
                success: function(data) {
                    _ExportData.buffers.push(data);
                }
            });
            _ExportData.toggleProgressBar(true, showPercent);
            await sleep(500);

            _ExportData.params[_ExportData.paramPageName]++
        }

        fileSaver(new Blob(
            _ExportData.buffers,
            {
                type:"application/vnd.ms-excel"
            }),
            _ExportData.exportName
        );
        _ExportData.toggleProgressBar(false);
    })();
};

使用的時候也比較簡單,如下所示:

var exportData = new ExportData(
    "/path/to/api",
    {
		pageSize: 100
	},
    "pageNum",
    1,
    100,
    "export-org-list.csv"
);
exportData.export();

這里要注意,我封裝為了統一,接口返回的時候類型必須是text/plain,純文本,不然解析可能會失敗,導致最終處理失敗,另外,如果是csv文件的話,需要在文件最前面加上BOMecho chr(239).chr(187).chr(191),不然可能會解析亂碼。


第二天一大早,小D就拿着這個方案去找領導,巴拉巴拉把原理和實現講了一遍,聽完之后,領導微微點了點頭,漏出了欣慰又遺憾的表情,欣慰小D朽木可雕,遺憾沒有機會接觸小D的女朋友了。

晚上回到家里,小D仔細一想,這個方案里面用到的新瀏覽器特性比較多,可能會有瀏覽器不支持,馬上查了下新特性的兼容性。


Blob是用來保存大量數據的,如果數據全放到url后面,可能有問題,比如雙引號,單引號之類的,Blob中就不會,它是以二進制方式存儲,herf后面只會是一個用createObjectURL生成的指向Blob內容的uri,比如blob:https://www.baidu.com/5d6222a7-cb7b-5f4b-8381-bde1cbed1b31


async function是用來實現sleep的,讓瀏覽器做到真正的sleep,不然進度條沒法再多個ajax請求之間刷新。

function sleep (time) {
    return new Promise((resolve) => setTimeout(resolve, time)); }

結合上面的信息可以看到主流瀏覽器基本都是支持的,而且是后台運營使用的,可以滿足條件了,如果要所有瀏覽器都支持,這個可能不是一個很好的方案。

至此,問題基本解決了,小D臉上漏出了欣慰的笑容,終於可以和女朋友開開心心了。

參考文章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM