基於Vue.js的大型報告頁專案實現過程及問題總結(二)

李文楊發表於2017-11-24

距離上一篇文章過去了二十多天了,期間一直想把第二部分寫完,結果在測試過程中遇到了各種坑爹的問題,到今天才算基本完成,也許還有後續,但趁著今天有時間就寫出來吧,也算對這個專案的一個總結了

遇到最大問題:

專案的需求是在一個視窗裡生成所有圖表,還要考慮到整套列印,所以滾動載入和分頁瀏覽不是最好的方案,這導致資料超級多的時候(大概會生成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>

關於列印大概就寫這些吧,詳細的教程大家可以去自行百度超多的;

 

相關文章