線上運行的業務已經跑了一段時間了,運營須要按期導出數據做分析,領導把小D叫過來講這個需求比較緊急,須要儘快上線,小D信誓旦旦的說沒問題,一下子就搞定。javascript
小D水平還不錯,果真,用了不到2小時時間就把導出作好了。小D是這麼實現的,作了個新的接口,接口裏面循環處理數據列表而後輸出,瀏覽器收到response後根據header信息將文件下載下來,在測試環境試了下沒問題就上線了,而後處理其餘事情去了。css
次日一大早,小D正在地鐵上看着本身漲停的股票偷着樂呢,領導電話救過來了,讓小D趕忙來公司,小D還在想難道領導由於我昨天作得好要表揚我?聽剛纔領導的語氣不太像啊!html
一到公司小D就找領導去了,領導板着臉,面無表情地說,你看你昨天作的啥東西,導出的列表跟線上的列表差了好多,快看看出啥問題了。前端
小D果真水平不錯,用了不到10分鐘就查到問題了,線上數據量太大,循環超過期間以後PHP腳本就退出了,致使數據導出一半就斷掉了。小D分析了下,如今超時時間是5s,時間過短了,按如今數據量估算大概得20s左右,因而小D很快出了新的方案:導出腳本執行時間延長到60s,這樣就不會有問題了。很快新的方案上線了,領導還特地親自操做了下,果真完整的導出來了,領導臉上漏出了欣慰小笑容。java
因爲你們的共同努力,業務發展迅速,過了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刷一會兒回過神來,連連點頭說明白了明白了。jquery
小D回家以後,沒顧得吃飯就打開電腦,沿着領導的線索去解決問題。git
<!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作了下面的模擬github
<!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請求,每次請求是一頁的數據。ajax
小D在github上面搜了下,發現還真有人已經對這個操做作了封裝,開開心心的將https://github.com/eligrey/FileSaver.js項目下載到本地,結合本身的業務作了初版出來,其中結合了bootstrap,加了進度條,導出時間比較長的時候內心有個數: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臉上漏出了欣慰的笑容,終於能夠和女友開開心心了。
完
參考文章