距離上一篇文章過去了二十多天了,期間一直想把第二部分寫完,結果在測試過程中遇到了各種坑爹的問題,到今天才算基本完成,也許還有后續,但趁着今天有時間就寫出來吧,也算對這個項目的一個總結了
遇到最大問題:
項目的需求是在一個窗口里生成所有圖表,還要考慮到整套打印,所以滾動加載和分頁瀏覽不是最好的方案,這導致數據超級多的時候(大概會生成2000多頁的報告且上不封頂),會造成頁面假死,瘋狂占用電腦內存,低配置的電腦根本無法加載,甚至造成死機
在項目結構上我們采用數據分發的方式控制組件的渲染,由大致小每層組件都對數據進行過濾,重新組成新的數據傳遞給下一級,根據數據去判斷顯示與否,由於vue里v-if的機制如果該模塊數據不存在,那么組件將不被渲染
一般來說我解決問題只有兩種方式,一是找到解決問題的辦法,二是讓這個問題徹底消失,顯然第二個是在這是行不通的,所以先分析原因:
1.后端返回的是原始數據,大量代碼都需要前端進行處理,在前端進行如此大工作量的數據處理,顯然內存消耗也是巨大的,顯然這是不明智的,但后台數據暫時無法做進一步處理
2.echarts繪制圖表的同時動畫和頻繁操作dom添加canvas也是也是消耗性能的元凶之一
3.大量的圖表繪制同步進行會導致阻塞
原因已經找到接下來就是解決問題
先說動畫的問題,這個在echarts的api里已經提出的解決辦法,有兩種,我這里都用到了:
1.全部圖表繪制都有動畫渲染的情況
2.單個圖表顯示超多數據的情況
第一個可以對echarts對象設置animate屬性來關閉所有動畫
animate:false
第二個需要設置progressive屬性
progressive屬性的作用如下:
漸進式渲染時每一幀繪制圖形數量,設為 0 時不啟用漸進式渲染,支持每個系列單獨配置。
在圖中有數千圖形甚至好幾萬圖形的時候,一下子把圖形繪制出來,或者交互重繪的時候可能會造成界面的卡頓甚至假死,因此 ECharts 從 3.2.0 開始支持大量圖形的漸進式渲染(progressive rendering),渲染的時候會把創建好的圖形分到數幀中渲染,每一幀渲染只渲染指定數量的圖形。
該配置項就是用於配置該系列每一幀渲染的圖形數,默認是 400 個,可以根據圖表圖形復雜度的需要適當調整這個數字使得在不影響交互流暢性的前提下達到繪制速度的最大化。
比如在 lines 圖或者平行坐標中線寬大於 1 的 polyline 繪制會很慢,這個數字就可以設置小一點,而線寬小於等於 1 的 polyline 繪制非常快,該配置項就可以相對調得比較大。
再說頻繁操作dom導致的卡頓問題
首先感謝老大提供的的思路,這個問題可以和同步繪制一起來解決,在這里需要仔細的研究一下同步異步的問題,這個問題想清楚了,問題就解決了
在這里推薦阮一峰老師的JavaScript 運行機制詳解:再談Event Loop
JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。單線程就意味着,所有任務需要排隊,前一個任務結束,才會執行后一個任務。如果前一個任務耗時很長,后一個任務就不得不一直等着。這個時候問題就出現了,當我在處理完數據傳給圖表的執行方法的時候我是這么寫的:
var data = 處理好的數據;
for(var i=0;i<data.length;i++){
chart({id:'xxxx'+i,data:data[i]});
}
這條被循環執行的數據多的有可能是上千條,而且這還只是其中一個模塊的數據,這樣的話就是上千條的數據在主線程上排隊,一個圖表必須要等到上一個圖表繪制完畢才會繪制下一個,並且在這個時候我其他的操作都是在等待圖表繪制完成的,也就是說必須要等到所有圖表繪制完畢,所有頁面加載出來我才能去計算頁碼並將其賦值,這個期間目錄頁的大模塊頁碼定位全都是空白的,而這時候由於要等待所有操作完成,且cpu這時候被占滿,自然而然的就造成了頁面的假死狀態.既然同步渲染會造成假死,那么解決方案自然就有了:異步執行繪制圖表方法
先看一下異步的運行機制
(1)所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
(2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
(4)主線程不斷重復上面的第三步。
首先先確定哪些任務是要在主線程內執行的
數據的處理
組件的渲染(不包含圖表)
頁碼的賦值
目錄頁的定位
這些主線程的任務都是可以同步進行的,且速度非常快,這樣就避免了必須要等待所有圖表渲染完成才能確定頁碼的尷尬
接下來是異步隊列
通過es6的promise方法可以很輕松的實現圖表異步的執行,關於promise大家也可以自行百度,在這里不出詳細解釋,不明白的同學暫時只需要知道這是一種js的異步變成解決方案就可以了;
打個比方,你有一個執行柱狀圖的方法,還有n個需要繪制柱狀圖的模塊,在這里的解決方案是:
1.新建一個公共的圖表執行方法的js文件,將所有圖表方法都放在一起,然后按需引入
圖表作為一個對象有兩個字段:data和method
export const Chart = { data:[], method:function(obj){ //這里放繪制圖表的方法 } };
注意這個data,他就相當於一個任務隊列,當我處理完數據時,不是第一時間就去執行繪制的方法,而是將處理好需要圖表渲染的數據添加到這個data的隊列里,每一個用到該圖表的模塊都是如此,這樣一來等數據處理結束data隊列里就存着所有需要渲染的數據了,
這個時候組件照常渲染,頁碼照常出,不去渲染圖表,卡頓假死的問題就解決了,雖然還沒有圖表,但是起碼頁面已經加載出來了,接下來要做的就是去將隊列里的數據進行異步的執行了
最開始考慮過使用定時器延時去傳遞數據加載圖表,像下面這樣
for(var i=0;i<10000;i++){ setTimeout(function(){ chart(data[i]) },1000*i) }
其實這樣也是可行的,每一個圖表的渲染都延遲執行一秒,定時器其實也算是異步執行了,當所有的主線程走完再去執行定時器的方法,但這樣的話相當於有10000個定時器在等待執行啊,雖然相隔一秒,不會造成卡頓,但顯然不是最優方案,
所以最終使用的是 promise的方法,這樣就變成了只有一個定時器,代碼如下
// 異步執行圖表 export const parmise = obj =>{ console.log('數據加載完畢,准備生成圖表'); var p1 = new Promise(function(resolve){ resolve(0); }); p1.then(fun1); function continueFunc(_index){ var p2 = new Promise(function(resolve){ resolve(_index); }); p2.then(fun1); } function fun1(ind){ obj.chart(obj.arr[ind]); setTimeout(function(){ if(ind!=obj.arr.length-1){ ind++; continueFunc(ind); } },500); }; obj.chart(); obj.parevArrLen=obj.arr.length; };
vue的munted方法代表的是所有頁面加載完成再去執行,在app.vue里把promise放在這里在合適不過了,當頁面渲染完成異步執行圖表繪制的方法,最大程度的解決卡頓問題
//先引入
import { parmise,chart } from './assets/js/chart.js'
//在mounted里執行
parmise(chart);
ok,到這里問題解決,基本上每次滑動滾輪時圖表繪制兩個左右,出圖速度飛快,低配置機器也可正常運行;
最后接着上一篇的打印報告來說,因為之前試驗過使用HTMLtopPDF打印,所以在寫項目期間就沒有進行過測試,當項目完成調試打印的時候才發現由於是多頁面應用所以根本無法打印,由於HTMLtopPDF是后端的解決方案,我們在前端也不好調試,所以選擇了前端打印pdf的方案,
查了許多資料后決定使用html2canvas 和 jsPDF結合使用來生成pdf
html2canvas : 通過遍歷頁面DOM結構,收集所有元素信息及相應樣式,渲染出canvas image
jsPDF:可以通過文字和圖片生成pdf
看了他們的作用相信觀眾老爺們也知道要怎么結合使用了,很簡單在點擊下載按鈕時通過html2canvas將頁面轉換為canvas image然后通過jsPDF再進行pdf轉換就ok了,接下來上簡單的教程;
html2canvas
我們可以直接在瀏覽器端使用html2canvas,對整個或局部頁面進行‘截圖’。但這並不是真的截圖,而是通過遍歷頁面DOM結構,收集所有元素信息及相應樣式,渲染出canvas image。
由於html2canvas只能將它能處理的生成canvas image,因此渲染出來的結果並不是100%與原來一致。但它不需要服務器參與,整個圖片都由客戶端瀏覽器生成,使用很方便。
使用
使用的API也很簡潔,下面代碼可以將某個元素渲染成canvas:
html2canvas(element, { onrendered: function(canvas) { // canvas is the final rendered <canvas> element } });
通過onrendered方法,可以將生成的canvas進行回調,比如插入到頁面中:
html2canvas(element, { onrendered: function(canvas) { document.body.appendChild(canvas); } });
做個小例子代碼如下
<html> <head> <title>html2canvas example</title> <style type="text/css">...</style> </head> <body> <header> <nav> <ul> <li>one</li> ... </ul> </nav> </header> <section> <aside> <h3>it is a title</h3> <a href="">Stone Giant</a> ... </aside> <article> <img src="./Stone.png"> <h2>Stone Giant</h2> <p>Coming ... </p> <p>以一團石頭...</p> </article> </section> <footer>write by linwalker @2017</footer> <script type="text/javascript" src="./html2canvas.js"></script> <script type="text/javascript"> html2canvas(document.body, { onrendered:function(canvas) { document.body.appendChild(canvas) } }) </script> </body> </html>
這個例子將頁面body中的元素渲染成canvas,並插入到body中
jsPDF
jsPDF庫可以用於瀏覽器端生成PDF。
文字生成PDF
使用方法如下:
// 默認a4大小,豎直方向,mm單位的PDF var doc = new jsPDF(); // 添加文本‘Download PDF’ doc.text('Download PDF!', 10, 10); doc.save('a4.pdf');
圖片生成PDF
使用方法如下:
// 三個參數,第一個方向,第二個單位,第三個尺寸格式 var doc = new jsPDF('landscape','pt',[205, 115]) // 將圖片轉化為dataUrl var imageData = ‘data:image/png;base64,iVBORw0KGgo...’; doc.addImage(imageData, 'PNG', 0, 0, 205, 115); doc.save('a4.pdf');
文字與圖片生成PDF
// 三個參數,第一個方向,第二個尺寸,第三個尺寸格式 var doc = new jsPDF('landscape','pt',[205, 155]) // 將圖片轉化為dataUrl var imageData = ‘data:image/png;base64,iVBORw0KGgo...’; //設置字體大小 doc.setFontSize(20); //10,20這兩參數控制文字距離左邊,與上邊的距離 doc.text('Stone', 10, 20); // 0, 40, 控制文字距離左邊,與上邊的距離 doc.addImage(imageData, 'PNG', 0, 40, 205, 115); doc.save('a4.pdf')
生成pdf需要把轉化的元素添加到jsPDF實例中,也有添加html的功能,但某些元素無法生成在pdf中,因此可以使用html2canvas + jsPDF的方式將頁面轉成pdf。通過html2canvas將遍歷頁面元素,並渲染生成canvas,然后將canvas圖片格式添加到jsPDF實例,生成pdf。
html2canvas + jsPDF
單頁
將demo1的例子修改下:
<script type="text/javascript" src="./js/jsPdf.debug.js"></script> <script type="text/javascript"> var downPdf = document.getElementById("renderPdf"); downPdf.onclick = function() { html2canvas(document.body, { onrendered:function(canvas) { //返回圖片dataURL,參數:圖片格式和清晰度(0-1) var pageData = canvas.toDataURL('image/jpeg', 1.0); //方向默認豎直,尺寸ponits,格式a4[595.28,841.89] var pdf = new jsPDF('', 'pt', 'a4'); //addImage后兩個參數控制添加圖片的尺寸,此處將頁面高度按照a4紙寬高比列進行壓縮 pdf.addImage(pageData, 'JPEG', 0, 0, 595.28, 592.28/canvas.width * canvas.height ); pdf.save('stone.pdf'); } }) } </script>
關於打印大概就寫這些吧,詳細的教程大家可以去自行百度超多的;