vue前台 pdf.js瀑布流式加載大文件


一,需求背景
在.NetCore新版項目中,針對電子文件在線瀏覽的實現方案做出了調整,由於需要支持跨平台,因此摒棄掉原來使用的第三方控件的方式。目前對於PDF文件的在線瀏覽采用的開源JS框架PDF.JS。它功能還是比較強大的,支持常用的PDF預覽效果,文件下載和打印等等功能。
但如果PDF文件較大或者頁數過多時,會發現PDF.JS需要等待很長時間才能將整個文件加載出來,這樣用戶的使用體驗是不佳的,因為較長時間迷茫的等待,並且沒有任何進度條提醒,會給人一種不知道是單純的加載文件慢還是加載遇到了錯誤?到底該不該繼續等待加載?的感覺,以至於失去耐心。
那么如何解決這種“迷茫等待”的粗劣使用體驗呢?PDF.JS瀑布流加載機制應運而生......

二,前端請求機制
PDF.JS用於瀏覽文件的自帶html頁面是viewer.html,因此瀏覽文件都是需要跳轉到該頁面。
PDF.JS加載文件是有兩種方式的:
(1)如圖2-1.png所示,該前台頁面通過ajax請求后台接口,從而獲得要瀏覽的文件的base64格式的字符串,然后通過JS的一系列轉換,生成PDF.JS能識別的數據格式,最后再對DEFAULT_URL變量進行賦值即可。

 

 

(2)JS中直接通過對html中的<iframe>控件綁定地址鏈接到viewer.html頁面,並且需要將要瀏覽的文件流作為file參數傳遞過去,而這個文件流參數是通過get請求后台返回文件流的接口來獲取的。前台示例代碼如下:
document.getElementById("if1").src = '/static/pdf/web/viewer.html?file='+encodeURIComponent(window.localStorage.getItem("rooturl")+'/api/OnlineView/GetFileStreamPDF?syscode='+data.syscode+'&unitsys='+this.$store.state.user.unit+"&username="+this.$store.state.user.name);

三,利弊對比
(1)上面第一種方式相當於把整個文件的全部數據通過一次接口請求全部獲取並返回給前台PDF.JS進行渲染加載。
這種方式的優點是:前后台請求次數少只有一次,保留了文件整體性,文件的各個頁會同時加載出來;
不過該方式缺點也很明顯:如果文件很大或頁數很多時,需要等待很長時間才能加載出文件;
(2)上面第二種方式相當於把一個文件進行分段請求和加載,PDF.JS也是支持這種加載方式的。
這種方式的優點是:可以讓用戶更快的先看到一部分頁面,剩余頁面的加載進度也可以通過界面上的加載進度條直觀的體現出來。不但可以更快的瀏覽到一些文件頁面,而且還能對加載的整體進度做到心里有數;
該方式的缺點是:需要很多次的頻繁請求后台,雖然可以讓用戶提前瀏覽到一些頁面,但是從文件開始加載到最終完成加載所用到的時間總和一定是要多余第一種方式的。而且分段加載的先后順序不能很好的控制,有可能出現文件的最后一頁加載完成,但是中間的某些頁還沒有加載出來的現象;

 

四,后台核心實現機制
為了提升用戶瀏覽大文件的使用體驗,我采用了PDF.JS的第二種方式。那么后台接口的寫法和返回數據的格式都要有所講究,因為需要支持分批請求加載的機制。
整體執行過程為前台第一次請求后台接口返回的響應編碼為200,然后會連續不斷的分批次請求后台接口,且每次請求頭中會附帶一個range參數表明當前請求要讀取的起止位置,一直到整個文件全部加載完成為止。后續的每次請求都會向前台返回一段范圍的文件流供前台渲染,且返回的響應編碼均為206;
后台接口核心代碼如圖2-2.png所示,需要先將PDF文件從mongodb服務器上下載到項目運行目錄中的臨時文件中,然后通過File.Open()方法將文件轉成FileStream。

 

 

 

(1)當首次請求后台時,請求頭是不帶分段請求相關參數Range的,所以會進入else邏輯中。
這時需要將響應頭中添加AcceptRanges值為bytes;
響應狀態編號賦值成200;
最終向前台返回的是FileStreamResult文件流類型數據,並且直接把文件的FileStream作為參數返回;
(2)后續的后台請求時,請求頭中就已經附帶分段請求相關的Range參數,所以進入截圖中的if邏輯中。
Range相關參數主要有From和To,其中From表示從哪里開始讀取,To表示讀取到哪;
這里我讓每次請求默認讀取1MB大小范圍的文件流,所以讀取終止位置沒有取range.To參數(因為range.To參數默認每次只讀取64KB大小的范圍,為了減少請求后台的次數,我將終止位置設置為起始位置加上1MB的位置)。
然后通過stream的seek()方法定位到當前的起始位置,並將指定長度范圍內的文件流寫入MemoryStream中,最終也是將該MemoryStream流作為參數通過FileStreamResult類型向前台返回;
其中響應頭還是要添加AcceptRanges值為bytes;
響應狀態編號要賦值成206;
響應頭要添加ContentLength為MemoryStream的長度;
響應頭要添加ContentRange參數,這也是最核心的一個范圍類型參數,通過ContentRangeHeaderValue類型傳遞,其中三個參數分別為本次讀取的起始位置,結束位置,和讀取長度;

五,注意事項
(1)瀏覽文件時如果直接對從mongodb上獲取的文件流進行分段截取,返回到前台PDF.JS時會報JS跨域錯誤。因此解決的辦法是先將文件保存至一個項目運行的臨時目錄中,然后通過File.Open方法打開文件流,供后續的分段截取;

(2)考慮到下載到臨時目錄中的文件名可能存在重復的情況,因此采用文件在mongodb上的唯一ID值作為文件名稱,如果一個文件瀏覽過了,當再次瀏覽時不需再次下載而是直接去讀取臨時目錄中已保存過的文件。臨時目錄中的文件會定期去進行統一刪除;

(3)前台PDF.JS每次獲取后台返回的響應頭信息時,會報JS錯誤(默認會認為這些響應頭不是安全的),因此后台接口響應頭中要多加一個“Access-Control-Expose-Headers”,並將后續用到的各種響應頭名稱作為參數寫入該響應頭中,這樣前台才可以順利接收這些響應頭;

 


免責聲明!

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



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