aurelia_Aurelia历险记:创建自定义PDF查看器
aurelia
本文由Vildan Softic同行評審。 感謝所有SitePoint的同行評審員使SitePoint內容達到最佳狀態!
在Web應用程序中處理PDF文件一直很麻煩。 如果幸運的話,您的用戶只需要下載文件。 但是,有時您的用戶需要更多。 過去,我很幸運,但是這次,我們的用戶需要我們的應用程序顯示PDF文檔,以便他們可以保存與每個頁面相關的元數據。 以前,可能是通過運行在瀏覽器中的昂貴的PDF插件(例如Adobe Reader)來實現的。 但是,經過一段時間的試驗,我發現了將PDF查看器集成到Web應用程序中的更好方法。 今天,我們來看看如何使用Aurelia和PDF.js簡化PDF處理。
概述:目標
今天,我們的目標是在Aurelia中構建PDF查看器組件,該組件允許查看器和我們的應用程序之間雙向數據流。 我們有三個主要要求。
您可以在我們的GitHub存儲庫中找到本教程的代碼,并在此處演示完成的代碼 。
引入PDF.js
PDF.js是由Mozilla Foundation編寫JavaScript庫。 它加載PDF文檔,解析文件和關聯的元數據,并將頁面輸出呈現到DOM節點(通常是<canvas>元素)。 該項目隨附的默認查看器為Chrome和Firefox中的嵌入式PDF查看器提供了強大的支持,并且可以用作獨立頁面或用作資源(嵌入在iframe中)。
誠然,這很酷。 這里的問題是,默認查看器雖然具有很多功能,但被設計為可作為獨立網頁使用。 這意味著,盡管可以將其集成到Web應用程序中,但實際上它必須在iframe沙箱中運行。 默認查看器旨在通過其查詢字符串獲取配置輸入,但是在初始加載后我們無法輕松更改配置,也無法輕松地從查看器獲取信息和事件。 為了將其與Aurelia Web應用程序集成(完成事件處理和雙向綁定),我們需要創建Aurelia自定義組件。
注意:如果需要有關PDF.js的復習,請查看我們的教程: 使用Mozilla的PDF.js在JavaScript中進行自定義PDF渲染
實施
為了實現我們的目標,我們將創建一個Aurelia自定義元素 。 但是,我們不會將默認查看器放入組件中。 相反,我們將創建連接到PDF.js核心和查看器庫的自己的查看器,以便可以最大程度地控制可綁定屬性和渲染。 對于我們的初始概念驗證,我們將從框架Aurelia應用程序開始 。
樣板
如您所見,如果您單擊上面的鏈接,則骨架應用程序中包含很多文件,我們將不需要其中的許多文件。 為了簡化生活,我們準備了簡化版的framework ,并在其中添加了以下內容:
- Gulp任務,將我們的PDF文件復制到dist文件夾(Aurelia用于捆綁)。
- PDF.js依賴項已添加到package.json 。
- 在應用程序的根目錄中, index.html和index.css已收到一些初始樣式。
- 我們將要處理的文件的空副本已添加。
- 文件src/resources/elements/pdf-document.css包含一些自定義元素CSS樣式。
因此,讓我們啟動并運行該應用程序。
首先,確保在全球范圍內安裝了gulp和jspm:
npm install -g gulp jspm然后克隆骨架并將其放入cd 。
git clone git@github.com:sitepoint-editors/aurelia-pdfjs.git -b skeleton cd aurelia-pdfjs然后安裝必要的依賴項:
npm install jspm install -y最后運行gulp watch并導航到http:// localhost:9000 。 如果一切按計劃進行,您應該會看到一條歡迎消息。
一些更多的設置
接下來要做的是查找幾個PDF并將它們放在src/documents 。 將它們命名為one.pdf和two.pdf 。 為了最大程度地測試我們的自定義組件,如果其中一個PDF確實很長,例如在Gutenberg Project上可以找到《戰爭與和平》,那將是一個很好的選擇。
放置好PDF之后,打開src/app.html和src/app.js (按照慣例, App組件是root或Aurelia應用),并用這兩個文件的內容替換其中的代碼: src / app.html和src / app.js。 在本教程中,我們不會涉及這些文件,但是對代碼進行了很好的注釋。
Gulp會自動檢測到這些更改,您應該會看到我們應用渲染的用戶界面。 設置就是這樣。 現在節目開始了……
創建一個Aurelia自定義元素
我們想要創建一個可在任何Aurelia視圖中使用的嵌入式組件。 由于Aurelia視圖只是包裝在HTML5 模板標簽內HTML片段,因此示例如下所示:
<template><require from="resources/elements/pdf-document"></require><pdf-document url.bind="document.url"page.bind="document.pageNumber"lastpage.bind="document.lastpage"scale.bind="document.scale"></pdf-document> </template><pdf-document>標記是自定義元素的示例。 它及其屬性(例如scale和page )不是HTML固有的,但是我們可以使用Aurelia自定義元素來創建它。 使用Aurelia的基本構建塊,可以輕松創建自定義元素:Views和ViewModels。 這樣,我們將首先構建名為pdf-document.js ViewModel,如下所示:
// src/resources/elements/pdf-document.jsimport {customElement, bindable, bindingMode} from 'aurelia-framework';@customElement('pdf-document')@bindable({ name: 'url' }) @bindable({ name: 'page', defaultValue: 1, defaultBindingMode: bindingMode.twoWay }) @bindable({ name: 'scale', defaultValue: 1, defaultBindingMode: bindingMode.twoWay }) @bindable({ name: 'lastpage', defaultValue: 1, defaultBindingMode: bindingMode.twoWay })export class PdfDocument {constructor () {// Instantiate our custom element.}detached () {// Aurelia lifecycle method. Clean up when element is removed from the DOM.}urlChanged () {// React to changes to the URL attribute value.}pageChanged () {// React to changes to the page attribute value.}scaleChanged () {// React to changes to the scale attribute value.}pageHandler () {// Change the current page number as we scroll}renderHandler () {// Batch changes to the DOM and keep track of rendered pages} }免費學習PHP!
全面介紹PHP和MySQL,從而實現服務器端編程的飛躍。
原價$ 11.95 您的完全免費
這里要注意的主要是@bindable裝飾器; 通過使用配置defaultBindingMode: bindingMode.twoWay創建可綁定屬性,并通過在ViewModel中創建處理程序方法( urlChanged , pageChanged等),我們可以監視對放置在自定義元素上的相關屬性的更改并對它們做出React。 這將使我們能夠簡單地通過更改元素的屬性來控制PDF查看器。
然后,我們將創建初始視圖以與ViewModel配對。
// src/resources/elements/pdf-document.html<template><require from="./pdf-document.css"></require><div ref="container" class="pdf-container">My awesome PDF viewer.</div> </template>整合PDF.js
PDF.js分為三個部分。 有一個核心庫,用于處理PDF文檔的解析和解釋。 顯示庫,它在核心層之上構建了可用的API; 最后是Web查看器插件,它是我們之前提到的預構建網頁。 為了我們的目的,我們將通過顯示API使用核心庫。 我們將建立自己的查看器。
顯示API導出一個名為PDFJS的庫對象,該對象允許我們設置一些配置變量并使用PDFJS.getDocument(url)加載文檔。 該API是完全異步的-它發送和接收來自Web Worker的消息,因此它很大程度上基于JavaScript承諾。 我們將主要處理從PDFJS.getDocument()方法異步返回的PDFDocumentProxy對象和從PDFDocumentProxy.getPage()異步返回的PDFPageProxy對象。
盡管文檔有些稀疏,但是PDF.js 在此處 ( demo )和here ( demo )都有一些用于創建基本查看器的示例 。 我們將在這些示例的基礎上構建自定義組件。
網絡工作者集成
PDF.js使用Web Worker卸載其渲染任務。 由于網絡工作者在瀏覽器環境中運行的方式(它們已被有效地沙盒化),我們被迫使用JavaScript文件的直接文件路徑而不是通常的模塊加載器來加載網絡工作者。 幸運的是,Aurelia提供了一個加載器抽象,因此我們不必引用靜態文件路徑(當捆綁應用程序時,它可能會更改)。
如果您正在遵循我們的倉庫版本,那么您將已經安裝了pdfjs-dist軟件包,否則,您現在需要這樣做(例如,使用jspm jspm install npm:pdfjs-dist@^1.5.391 )。 然后,我們將使用Aurelia的依賴項注入模塊注入Aurelia的加載器抽象,并使用加載器將Web Worker文件加載到構造函數中,如下所示:
// src/resources/elements/pdf-document.jsimport {customElement, bindable, bindingMode, inject, Loader} from 'aurelia-framework'; import {PDFJS} from 'pdfjs-dist';@customElement('pdf-document')... // all of our @bindables@inject(Loader) export class PdfDocument {constructor (loader) {// Let Aurelia handle resolving the filepath to the worker.PDFJS.workerSrc = loader.normalizeSync('pdfjs-dist/build/pdf.worker.js');// Create a worker instance for each custom element instance.this.worker = new PDFJS.PDFWorker();}detached () {// Release and destroy our worker instance when the the PDF element is removed from the DOM.this.worker.destroy();}... }加載我們的頁面
PDF.js庫處理加載,解析和顯示PDF文檔。 它具有對部分下載和身份驗證的內置支持。 我們要做的就是提供相關文檔的URI,PDF.js將返回一個Promise對象,解析為一個表示PDF文檔及其元數據JavaScript對象。
加載和顯示PDF將由我們的可綁定屬性驅動; 在這種情況下,它將是url屬性。 實質上,當URL更改時,自定義元素應要求PDF.js對該文件進行請求。 我們將在urlChanged處理程序中進行此操作,對構造函數進行一些更改以初始化一些屬性,并對detached方法進行一些更改以進行清理。
對于文檔的每一頁,我們將在DOM中創建一個<canvas>元素,該元素位于具有固定高度的可滾動容器中。 為了實現這一點,我們將使用Repeater來使用Aurelia的基本模板功能。 由于每個PDF頁面可以具有自己的大小和方向,因此我們將基于PDF頁面視口設置每個畫布元素的寬度和高度。
這是我們的觀點:
// src/resources/elements/pdf-document.html<template><require from="./pdf-document.css"></require><div ref="container" id.bind="fingerprint" class="pdf-container"><div repeat.for="page of lastpage" class="text-center"><canvas id="${fingerprint}-page${(page + 1)}"></canvas></div></div> </template>加載PDF文檔后,我們需要獲取PDF中每一頁的大小,以便我們可以將每個canvas大小與其頁面大小進行匹配。 (此時,我們可以進行滾動查看器的設置;如果現在不這樣做,則每個頁面的高度都不正確。)因此,在加載每個頁面之后,我們將一個任務排隊使用Aurelia的TaskQueue抽象來調整canvas元素的大小。 (這是出于DOM性能的原因。您可以在此處閱讀有關微任務的更多信息 )。
這是我們的ViewModel:
// src/resources/elements/pdf-document.jsimport {customElement, bindable, bindingMode, inject, Loader} from 'aurelia-framework'; import {TaskQueue} from 'aurelia-task-queue'; import {PDFJS} from 'pdfjs-dist';@customElement('pdf-document')... // all of our @bindables@inject(Loader, TaskQueue) export class PdfDocument {constructor (loader, taskQueue) {PDFJS.workerSrc = loader.normalizeSync('pdfjs-dist/build/pdf.worker.js');this.worker = new PDFJS.PDFWorker();// Hold a reference to the task queue for later use.this.taskQueue = taskQueue;// Add a promise property.this.resolveDocumentPending;// Add a fingerprint property to uniquely identify our DOM nodes.// This allows us to create multiple viewers without issues.this.fingerprint = generateUniqueDomId();this.pages = [];this.currentPage = null;}urlChanged (newValue, oldValue) {if (newValue === oldValue) return;// Load our document and store a reference to PDF.js' loading promise.var promise = this.documentPending || Promise.resolve();this.documentPending = new Promise((resolve, reject) => {this.resolveDocumentPending = resolve.bind(this);});return promise.then((pdf) => {if (pdf) {pdf.destroy();}return PDFJS.getDocument({ url: newValue, worker: this.worker });}).then((pdf) => {this.lastpage = pdf.numPages;pdf.cleanupAfterRender = true;// Queue loading of all of our PDF pages so that we can scroll through them later.for (var i = 0; i < pdf.numPages; i++) {this.pages[i] = pdf.getPage(Number(i + 1)).then((page) => {var viewport = page.getViewport(this.scale);var element = document.getElementById(`${this.fingerprint}-page${page.pageNumber}`);// Update page canvas elements to match viewport dimensions. // Use Aurelia's TaskQueue to batch the DOM changes.this.taskQueue.queueMicroTask(() => {element.height = viewport.height;element.width = viewport.width;});return {element: element,page: page,rendered: false,clean: false};});}// For the initial render, check to see which pages are currently visible, and render them./* Not implemented yet. */this.resolveDocumentPending(pdf);});}detached () {// Destroy our PDF worker asynchronously to avoid any race conditions.return this.documentPending.then((pdf) => {if (pdf) {pdf.destroy();}this.worker.destroy();}).catch(() => {this.worker.destroy();});} }// Generate unique ID values to avoid any DOM conflicts and allow multiple PDF element instances. var generateUniqueDomId = function () {var S4 = function() {return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);};return `_${S4()}${S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`; }保存您的工作,Gulp應該重新渲染頁面。 您會注意到該容器顯示了相應PDF的正確頁面數。 唯一的問題是它們為空。 讓我們解決這個問題!
渲染頁面
現在,我們已經加載了頁面,我們需要能夠將它們呈現為DOM元素。 為此,我們將依靠PDF.js的呈現功能。 PDF.js查看器庫具有專用于呈現頁面的異步API。 他們網站上有一個很棒的示例,展示了如何創建renderContext對象并將其傳遞給PDF.js render方法。 我們將從示例中刪除此代碼,并將其包裝在render函數中:
src / resources / elements / pdf-document.js
... export class PdfDocument { ... }var generateUniqueDomId = function () { ... }var render = function (renderPromise, scale) {return Promise.resolve(renderPromise).then((renderObject) => {if (renderObject.rendered) return Promise.resolve(renderObject);renderObject.rendered = true;var viewport = renderObject.page.getViewport(scale);var context = renderObject.element.getContext('2d');return renderObject.page.render({canvasContext: context,viewport: viewport}).promise.then(() => {return renderObject;});}); };PDF.JS中的渲染有些昂貴。 因此,我們要限制負載。 我們只想渲染當前可見的內容,因此我們將渲染限制在可見邊界內的頁面上,而不是一次渲染所有內容。 我們將做一些簡單的數學運算來檢查視口中的內容:
// src/resources/elements/pdf-document.jsexport class PdfDocument { ... }var generateUniqueDomId = function () { ... }var render = function (...) { ... }var checkIfElementVisible = function (container, element) {var containerBounds = {top: container.scrollTop,bottom: container.scrollTop + container.clientHeight};var elementBounds = {top: element.offsetTop,bottom: element.offsetTop + element.clientHeight};return (!((elementBounds.bottom < containerBounds.top && elementBounds.top < containerBounds.top)|| (elementBounds.top > containerBounds.bottom && elementBounds.bottom > containerBounds.bottom))); }首次加載文檔時以及滾動時,我們將運行這些視口檢查。 現在,在加載時,我們將像這樣簡單地渲染可見的內容。
// src/resources/elements/pdf-document.jsexport class PdfDocument { ...urlChanged (newValue, oldValue) {...// For the initial render, check to see which pages are currently visible, and render them.this.pages.forEach((page) => {page.then((renderObject) => {if (checkIfElementVisible(this.container, renderObject.element)){if (renderObject.rendered) return;render(page, this.scale);}});});this.resolveDocumentPending(pdf);});}重新加載該應用程序,您將看到每個PDF的第一頁都呈現出來。
實施滾動
為了提供熟悉和無縫的體驗,我們的組件應將頁面顯示為完全可滾動文檔的各個部分。 我們可以通過CSS使容器具有固定的高度(帶有滾動溢出)來實現此目的。
為了最大程度地處理較大的文檔,我們將做一些事情。 首先,我們將利用Aurelia的TaskQueue批量更改DOM。 其次,我們將跟蹤PDF.js已經呈現的頁面,因此它不必重做已經完成的工作。 最后,我們只會在滾動停止后使用Aurelia的debounce 綁定行為呈現可見頁面。 滾動時將運行以下方法:
// src/resources/elements/pdf-document.jsexport class PdfDocument { ...renderHandler () {Promise.all(this.pages).then((values) => {values.forEach((renderObject) => {if (!renderObject) return;if (!checkIfElementVisible(this.container, renderObject.element)){if (renderObject.rendered && renderObject.clean) {renderObject.page.cleanup();renderObject.clean = true;}return;}this.taskQueue.queueMicroTask(() => {if (renderObject.rendered) return;render(renderObject, this.scale);});});});} ... }這是我們的觀點; 我們使用定義的方法在scroll.trigger利用Aurelia的事件綁定,以及去抖動綁定行為。
// src/resources/elements/pdf-document.html<template><require from="./pdf-document.css"></require><div ref="container" id.bind="fingerprint" class="pdf-container" scroll.trigger="pageHandler()" scroll.trigger2="renderHandler() & debounce:100"><div repeat.for="page of lastpage" class="text-center"><canvas id="${fingerprint}-page${(page + 1)}"></canvas></div></div> </template>我們正在將page屬性綁定到查看器中。 當它更改時,我們要更新滾動位置以顯示當前頁面。 我們也希望這能以其他方式起作用。 當我們滾動瀏覽文檔時,我們希望當前頁碼更新為當前正在查看的頁面。 因此,我們將以下兩個方法添加到ViewModel中:
export class PdfDocument { ...// If the page changes, scroll to the associated element.pageChanged (newValue, oldValue) {if (newValue === oldValue || isNaN(Number(newValue)) || Number(newValue) > this.lastpage || Number(newValue) < 0) {this.page = oldValue;return;}// Prevent scroll update collisions with the pageHandler method.if (Math.abs(newValue - oldValue) <= 1) return;this.pages[newValue - 1].then((renderObject) => {this.container.scrollTop = renderObject.element.offsetTop;render(this.pages[newValue - 1], this.scale);});}...// Change the current page number as we scroll.pageHandler () {this.pages.forEach((page) => {page.then((renderObject) => {if ((this.container.scrollTop + this.container.clientHeight) >= renderObject.element.offsetTop&& (this.container.scrollTop <= renderObject.element.offsetTop)){this.page = renderObject.page.pageNumber;}});});} ... }我們將在容器中的scroll.trigger事件中調用pageHandler方法。
注意:由于Aurelia模板的當前限制,因此無法在事件處理程序中使用單獨的綁定行為聲明多個方法。 我們通過將這些行添加到ViewModel的頂部來解決此問題。
import {SyntaxInterpreter} from 'aurelia-templating-binding'; SyntaxInterpreter.prototype.trigger2 = SyntaxInterpreter.prototype.trigger;…并將新方法放在scroll.trigger2事件上。
Gulp應該重新加載應用程序,您會看到PDF的新頁面在滾動到視圖時將呈現。 好極了!
實施縮放
縮放時,我們想更新當前的縮放級別。 我們在scaleChanged屬性處理程序中執行此操作。 本質上,我們調整所有畫布元素的大小以反映具有給定比例的每個頁面的新視口大小。 然后,我們重新渲染當前視口中的內容,重新開始循環。
// src/resources/elements/pdf-document.jsexport class PdfDocument { ...scaleChanged (newValue, oldValue) {if (newValue === oldValue || isNaN(Number(newValue))) return;Promise.all(this.pages).then((values) => {values.forEach((renderObject) => {if (!renderObject) return;var viewport = renderObject.page.getViewport(newValue);renderObject.rendered = false;this.taskQueue.queueMicroTask(() => {renderObject.element.height = viewport.height;renderObject.element.width = viewport.width;if (renderObject.page.pageNumber === this.page) {this.container.scrollTop = renderObject.element.offsetTop;}});});return values;}).then((values) => {this.pages.forEach((page) => {page.then((renderObject) => {this.taskQueue.queueMicroTask(() => {if (checkIfElementVisible(this.container, renderObject.element)) {render(page, this.scale);}});});});});} ... }最終結果
讓我們回顧一下我們的目標:
最終代碼可以在我們的GitHub存儲庫中找到,以及此處完成代碼的演示 。 盡管仍有改進的空間,但我們已經達到了目標!!
項目后分析和改進
總是有改進的余地,進行項目后分析并確定在將來的迭代中要解決的領域始終是一個好習慣。 根據PDF查看器的實現,以下是我要升級的一些內容:
各個頁面組件
當前,此概念驗證僅允許滾動視口。 理想情況下,即使在查看器外部,我們也可以在任何地方渲染任何頁面,例如,將PDF縮略圖生成為單獨的元素。 創建<pdf-page>自定義元素或類似的東西可以提供此功能,而查看者可以通過合成簡單地使用這些元素。
API優化
PDF.js具有廣泛的API。 盡管有使用PDF.js的良好示例,但其顯示API可能會使用更多文檔。 使用查看器API可能會有更清潔,更優化的方法來實現我們的目標。
虛擬滾動和性能優化
當前,文檔查看器內部的canvas元素數量等于文檔中的頁面數量。 所有畫布都存在于DOM中,這對于大型文檔而言可能非常昂貴。
存在一個Aurelia插件-ui虛擬化插件 ( demo )-通過動態添加和刪除DOM中的元素以與活動視口相對應,極大地提高了超大型數據集的性能。 理想情況下,PDF查看器可以將其合并以提高性能(避免在DOM中包含成千上萬的畫布,這確實會損害性能)。 這種優化與單獨的頁面組件結合在一起,對于大型文檔確實可以產生巨大的變化。
創建一個插件
Aurelia提供了一個插件系統。 將此概念驗證轉換為Aurelia插件將使其成為任何Aurelia應用程序的直接資源。 Aurelia Github存儲庫提供了一個插件框架項目 ,這將是開始開發的好地方。 這樣,其他人可以使用此功能而不必重建它!
向前走
在Web應用程序中處理PDF文件一直很麻煩。 但是,利用當今可用的資源,通過組合庫及其功能,我們可以做的比以往更多。 今天,我們已經看到了一個基本的PDF查看器示例-可以通過自定義功能進行擴展,因為我們可以完全控制它。 可能性是無止境! 您準備好要建造東西了嗎? 在下面的評論中讓我知道。
翻譯自: https://www.sitepoint.com/aurelia-custom-pdf-viewer-component/
aurelia
總結
以上是生活随笔為你收集整理的aurelia_Aurelia历险记:创建自定义PDF查看器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 药店管理系统设计方案开发
- 下一篇: 八种ADSL接入情况中断流现象分析