ionic3中實現瀏覽PDF文件

說實話這個問題還挺難得,搞了兩天才出結果,身心疲憊啊。導致身心疲憊的還有另外一件事,項目組的實習生想要追求“美好”的生活,決定跑到上海去試試。看著他高興期待興奮的樣子實在不忍潑冷水。去了一個月就回來了,沒找到合適的工作不說還被對方的“技術主管”氣勢洶洶的一通打壓嘲諷。

這讓我想起來了一個不是笑話的笑話。程序猿相親程序媛,一見面挺對眼的。聊著聊著就出事了,雖然男方已經極力避免出現編程界割席斷袍的幾個致命話題,例如“PHP是世界上最好的語言”之類的。可惜還是中招了。導致相親失敗的原因很可笑,是關于一個三元運算符的問題。女方覺得男的連三元運算符都掌握不好,技術肯定很差,兩個人不適合,然后就沒有然后了……其實吧,我覺得挺好的分了就分了唄,不然真成事了,白天干程序員,晚上還程序員,估計是要瘋了的!

what???.jpg

人總是會有優越感的,健康的優越感應該是來自努力奮斗并且成功之后的充實感,而不是建立在打壓,嘲笑別人。算是為了我的小兄弟鳴個不平吧。開始說重點吧,不然各位要抄起板磚拍人了。老規矩手打文章妹子鎮樓,不為別的,只為活血化瘀……

工作細胞—血小板

不放性感妹子了,來個萌物。一樣活血化瘀!

話說ionic在國內還真是不火啊,不過這不影響我們使用的熱情。在ionic中瀏覽PDF是個比較難的需求,不過我們始終堅信技術上的難題從來都不會成為真正的難題。下面是我的探索之路的回溯!也是所謂的裝逼時間。

其實在官方文檔上已經有現成的插件可以實現閱讀PDF——Document Viewer。不過這貨有個致命的缺點,就是只能閱讀本地PDF文件,對于網絡PDF文件支持的不是很好。如果想要閱讀遠程PDF還必須使用FileFile Transfer(或者File Opener)這兩個插件。前者操作本地文件,后者用于把遠程文件下載到本地。這種方案在理論上可以很完美的實現需求。

各位……請注意!是理論上,因為遠程下載文件到本地這個操作在實際APP運行環境中,幾乎是不可能的。為什么?因為各個手機廠商會在此階段接管下載文件的進程。反應到現實情況就是當你點擊查看PDF的時候,手機系統會彈出對話框讓你選擇“始終用此方式”或“僅此一次”來打開,備選的打開方式中都是各自平臺的應用市場。就算手機上有下載工具類的功能,但下載路徑就不是我們可以掌控的了。這也是為什么很多APP不能實現“熱更新”的原因。我們的業務流程直接被打斷夭折掉。放棄吧,尋找別的方法吧。

想起來以前寫管理后臺的時候用過的一個叫pdf.js的插件挺好用的,可以想個辦法集成到ionic中來。知道你們最想看的是代碼,這樣可以直接copy到你們自己的項目直接用,我并不是反對“拿來主義”,做項目么就是要第一時間滿足客戶需求為首位,不過在閑暇時間研究下還是有很多樂趣的。下面就放出代碼,不過事先聲明,這個是我結合國外的大神的一篇文章的來的,不過我發現并不能很好的實現功能。所以這是被我摒棄的方案,具體原因會在后面說明。

祭出大神的文章Using PDF.JS with Ionic 3.x,可以參考下。打開速度有點慢,耐心等待。下面是我的方法。

第一步,在項目目錄中安裝pdf.js組件

npm install ??save pdfjs?dist@2.0.489  //最新版本可能會報錯
npm install ??save?dev @types/pdfjs?dist

第二步,copy你的PDF文件到項目\src\assets

第三步,在home.html中添加以下代碼

<ion-content padding>
    <canvas #viewerContainer></canvas>
</ion-content>

第四步,在home.ts中添加以下代碼,比較簡單的實現核心功能代碼,翻頁縮放什么的就不放了,不然太長。

import * as pdfjs from "pdfjs-dist/webpack.js";
import { PDFDocumentProxy, PDFPageProxy, PDFPageViewport, PDFRenderTask } from 'pdfjs-dist';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
  scale = 1;
  pdfDoc = null;
  canvas = null;
  curPage: number = 1;
  maxPage: number = 0;
//上面是添加的代碼

  @ViewChild('viewerContainer') canvasRef: ElementRef;

  constructor(public navCtrl: NavController) { }

  ionViewDidLoad() {
    this.canvas = this.canvasRef.nativeElement;
    pdfjs.getDocument('https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.pdf').then(pdf => {
      this.curPage = 1;
      this.pdfDoc = pdf;
      this.maxPage = pdf.numPages;
      this.viewPage();
    });
  }

  viewPage() {
    this.pdfDoc.getPage(this.curPage).then(page => {
      var viewport = page.getViewport(this.scale);
      var context = this.canvas.getContext('2d');
      this.canvas.height = viewport.height;
      this.canvas.width = viewport.width;
      var renderContext = {
        canvasContext: context,
        viewport: viewport
      };
      page.render(renderContext);
    });
  }
}

這種方法可以實現遠程PDF文件的訪問。但實現的結果是整個PDF文件在頁面中渲染出來不會自適應頁面大小,要想實現需要查閱pdf.js的接口文檔。吐槽下,就和“在技術人員眼里所有人都應該懂技術和在技術大神眼里所有人都應該與自己水平差不多”這種操蛋邏輯一樣。pdf.js的官方文旦太操蛋了,需要去查看源碼才能了解到他的具體接口都有什么。

雖然最終實現的代碼可能很少,但是這個查閱量對于沒有閱讀外文文檔能力和閱讀源碼能力的人來說無意是噩夢,看一眼的勇氣都沒有直接放棄。這就是所謂的操作顆粒度太細了。我們需要的是給我們一堆樂高積木讓我們拼城堡而不是給我們一堆沙子讓我們去堆城堡。

看了一點點官方的文檔,大致明白了些,畢竟不是直接調用JS來實現,在外層還嵌套了ionic。這個實現起來就需要繞個小圈圈。正在看操蛋源碼的時候,突然腦海中一閃而過在早期我放棄掉的一個方案。這個方案因為沒能提供“總頁數”的獲取方式,無法實現一些功能被我放棄了,當我意識到這個被放棄的插件也是基于pdf.js開發的時候,天!晴!了!我在官方文檔中找到了這個突破點。一切都迎刃而解了。看了看表,還有點時間才下班。爭取在下班之前搞定這個需求,我們的口號是:不加班!

ng2-pdf-viewer還是比較給力的,它提供了一些接口能讓我們在ionic中方便的瀏覽PDF,而且預先解決了一些底層的問題,讓我們無須考慮這些無關項目的問題。以下在是我的最終方案,代碼完全可以用。跑的起來而且沒毛病,可以直接拷貝拿走的。需要事先說明是,我并沒有按照官方示例的方式去做,有些安裝順序和代碼可能與官方提供的有些差異。

重點:這個插件不支持ionic的 懶加載 模式,會出錯!整個APP崩掉!!!

第一步,安裝ng2-pdf-viewer

npm install ng2-pdf-viewer --save

第二步,在項目中新建頁面

ionic g page pdf-viewerPage

第三步,在app.module.ts添加代碼,只貼出添加的代碼

import {PdfViewerModule} from 'ng2-pdf-viewer';  
import {PdfViewerPage} from "../pages/pdf-viewer/pdf-viewer";

@NgModule({
    declarations: [
        PdfViewerPage
    ],
    imports: [      
        PdfViewerModule,
    ],
    entryComponents: [
        PdfViewerPage
    ],
})

第四步,實現方式是點擊觸發模態框,讓PDF文件在模態框中渲染。先在需要響應點擊事件的頁面寫代碼

//html文件
 <ion-list class="topList">
     <ion-list-header>
        <div class="headerBock"></div>
           某某列表
     </ion-list-header>
     <ion-item *ngFor="let n of news;" (click)="openPDF()">
        <ion-thumbnail item-start>
           <img src="{{n.thumbnail}}" class="smallThumbnail"/>
        </ion-thumbnail>
        <h2>{{n.listTitle}}</h2>
        <p text-right>{{n.created}}</p>
      </ion-item>
</ion-list>
//ts文件
import {PdfViewerPage} from "../pdf-viewer/pdf-viewer";
@Component({
    selector: 'xxxx-home',
    templateUrl: 'xxxx.html'
})
export class XxxxPage {

URL= "https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.pdf";

constructor(public modalCtrl: ModalController,) {}

openPDF(){
 let modal: Modal = this.modalCtrl.create(PdfViewerPage, {
      displayData: {
         pdfSource: {
              url: this.URL
          }
      },
                              
   });
   modal.present();
  }
}

第五步,寫響應模態框請求的頁面pdf-viewer.html

<ion-content padding  >
  <pdf-viewer [src]="displayData.pdfSource"
              [show-all]="true"
              [original-size]="false"
              [zoom]=1
              [render-text]="false"
              [autoresize]="true"
              style="display: block" >
  </pdf-viewer>
</ion-content>

第六步,pdf-viewer.ts

 displayData: any = {};

 ionViewDidLoad() {
        this.displayData = this.navParams.get('displayData');
}

沒了,顯示結果很是不錯。完美自適應屏幕大小,也能遠程訪問PDF文件。需要注意的是此插件會調用
pdf.worker.js這個文件,我很納悶安裝這個插件的時候,會自動安裝pdfjs?dist插件,他卻通過CDN遠程調用pdf.worker.js。還好插件的說明文檔給了相關提示,查閱源碼的時候發現果然如此。

ng2-pdf-viewer

好吧,構造函數constructor是整個頁面中最先被執行到的在這里面添加本地化方法吧。先把pdf.worker.jscopy到assets/public/下面。然后在pdf-viewer.ts寫上。

constructor(public navCtrl: NavController, public navParams: NavParams,) {
        window.prototype.pdfWorkerSrc = 'assets/public/pdf.worker.js';
    }

發現cmaps也能本地化,好吧一起來吧。在node_modules找到pdfjs-dist/cmaps,把整個cmaps文件夾拷貝到assets目錄下。然后在pdf-viewer.html<pdf-viewer>添加自定義屬性[c-maps-url]="'assets/cmaps/'"

 <pdf-viewer [src]="displayData.pdfSource"
           .
           .
          [c-maps-url]="'assets/cmaps/'"
           .
           .  
  ></pdf-viewer>

這時候出現了一個十分十分操蛋的問題,整個頁面如果沒有render結束,就去關閉模態框會報錯。告訴DIV找不到屬性之類的。奶奶個腿啊,如果一個PDF的頁數比較多,這可能要了老命了。為了不加班加速干起來。能精簡的就精簡吧!不計后果了。

解決方案就是不全部加載,每次只render一頁。然后添加控制按鈕或者是手勢響應事件,來控制翻頁。這種情況必須知道頁面的總數,沒關系pdf.js的文檔中已經告訴了我們方法。

ng2-pdf-viewer中提供的事件(after-load-complete)。會在文檔加載完成之后執行回調函數,回調函數的參數類型就是來自于pdf.jsPDFDocumentProxy。它返回的屬性有個numPages就是PDF文檔的總頁數,繼續進行。

<!--html-->
 <pdf-viewer (after-load-complete)="afterLoad($event)"></pdf-viewer>
//ts
afterLoad(pdf: PDFDocumentProxy) {
        this.totalPage = pdf.numPages;
    }

這樣我們就能得到總頁數了,之后的的翻頁功能我就不寫出來了,對你來說應該是小case了。

最后的最后,還有一個坑,需要去填的就是。這個插件訪問的遠程PDF文件如果是不存在的,整個APP會崩掉。這就尷尬了!


尷尬.jpg

如何判斷遠程文件是不是存在,還需要一些方法。想來想去也就是用http組件的get方法了。不過這個玩意一般都是用來獲取json的,能用到PDF文件么?兩者就后綴名不一樣,都是文件啊。還是試驗下……

this.http.get('https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.pdf')
.subscribe(
  data => {console.log(data)},
  error => {console.log(error)}
);

全部都是走的error這條路!還讓不讓人活啦!

驚訝

等等!我好像遺漏了什么東西,仔細觀察下。發現了不得了的東西。error對象里面有一個只讀屬性status它顯示的是200,這個數很熟悉。我把遠程PDF的名字后面隨便加了一個字母,status變成了404。我擦我草我勒個去。不會吧!可以這樣么?那么我可以利用get遠程PDF文件必然走錯誤路徑,在根據status的數值來判斷遠程是否存在。有點奇葩了吧!

好吧 不管了為了早下班。

     let URL = 'https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.pdf';
          this.http.get(URL).subscribe(data => {},
            error2 => {
              if (error2.status == 200) {
                let modal: Modal = this.modalCtrl.create(PdfViewerPage, {
                     displayData: {
                       pdfSource: {
                         url: URL
                       }
                     }});
                 modal.present();
              } else {
                //彈出alert告訴用戶遠程PDF不存在。
                }
     });
一個挑事的表情

總覺得有什么地方不對勁啊,算了就這吧。用戶使用體驗還可以。目前還沒出什么亂子,暫時這樣吧。很多項目都是BUG驅動,沒辦法啊。

遠程訪問PDF有個瓶頸就是如果遠程文件過大,打開的時候速度會很慢。這個沒辦法啊!可以利用(on-progress)加個進度條提示加載進度。可惜這個事件的回調函數返回的對象中有兩個屬性,一個是當前加載文件大小這個很正常,另一個總大小顯示是undefined。可惡啊,懶的再去看插件源碼了。

就這吧!抽空再搞一搞吧。
(報告完畢)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容