公司有個業務需求,要求後臺傳pdf的base64編碼給前端,前端顯示到介面上,後來在網上搜尋了很多關於base64轉pdf的文章,都寫的不是非常的詳細,在實現的過程中遇到很多坑,經過一天的研究終於實現了這個功能,分享一下我在這個功能中遇到的問題和解決方法
要註明的是這裡用到的核心外掛是pdf.js,原理是動態生成canvas標籤,然後通過pdf.js生成一個能渲染出pdf的物件,隨後渲染每個canvas,並且生成的pdf是畫面的形式,並沒有pdf之類的控制元件
引入外掛
這裡很多部落格都是使用JavaScript原生的方法引入pdf.js,例如使用script標籤引入外部的js指令碼,或者直接把pdf.js的原始碼複製到專案中,但是我嘗試這些方法的時候都不是特別好用,而且引入後導致專案體積過於龐大,
隨後我去github上尋找通過包管理器引入pdf.js的方法,在pdf.js的github上官方說明的是用gulp如何使用pdf.js,但是對於npm來說並沒有詳細說明,終於我在字裡行間發現了這麼一句話
To use PDF.js in a web application you can choose to use a pre-built version of the library or to build it from source. We supply pre-built versions for usage with NPM and Bower under the
pdfjs-dist
name. For more information and examples please refer to the wiki page on this subject.
大致的意思就是如果使用npm包管理器或者bower的話,引入的名字為pdfjs-dist,那麼我們使用npm的方法引入這個pdfjs-dist,引入的名字就隨意取名了這裡我叫PDFJS
import PDFJS from 'pdfjs-dist'
複製程式碼
使用pdfjs-dist
這裡後臺傳給我的是一個由pdf檔名字和pdf的base64編碼組成的物件的陣列,我取名為pdfDataList
可以看到fileName是pdf的名字,fileVale是pdf檔案的base64編碼,thumbnail是pdf縮圖的base64編碼這裡用不到先不管,之前說到需要動態生成canvas節點(這裡不會canvas也不要緊,只需要根據程式碼一步步做就能渲染canvas)
-
首先我們建立一個承載所有canvas節點的父節點,取名為pdfList
-
然後建立一個非同步函式showPdf(不懂什麼是非同步函式的可以去查一下async/await,這裡不用非同步函式也可以使用promise.then的方法,但是async/await作為非同步操作的終極方案最好還是學習一下)
async showPdf() {
}
複製程式碼
- 使用querySelector選擇類名為pdfList的dom節點,隨後遍歷後臺傳過來的pdfDataList陣列的每一項,這裡用到一個瀏覽器自帶的atob()方法解碼base64,MDN上是這麼解釋的:
你可以使用
window.btoa()
方法來編碼一個可能在傳輸過程中出現問題的資料,並且在接受資料之後,使用 atob() 方法再將資料解碼。
語法: var decodedData = scope.atob(encodedData);
隨後呼叫pdf.js外掛的getDocument方法,getDocument是一個promise,所以使用非同步函式的話前面需要加await關鍵字(不使用非同步函式的話在方法後面加.then((pdf)=>{.......}),這個pdf物件和我這個pdf物件是同一個,同時這裡暫時也沒考慮非同步操作出錯的情況,有要求的話可以在加個catch捕獲錯誤) getDocument方法的引數是一個物件,物件鍵名為data,值為base64解碼後的值,此方法返回一個pdf物件,這個物件有幾個屬性,可以列印出來觀察一下
這裡我們先用到的是numPages屬性,它指的是當前pdf檔案有多少頁
async showPdf() {
let pdfList = document.querySelector('.pdfList') //通過querySelector選擇DOM節點,使用document.getElementById()也一樣
for(let value of this.pdfDataList){ //遍歷後臺傳過來的pdfDataList
let base64 = value.fileValue //獲得bas464編碼
let decodedBase64 = atob(base64) //使用瀏覽器自帶的方法解碼
let pdf = await PDFJS.getDocument({data: decodedBase64}) //返回一個pdf物件
let pages = pdf.numPages //宣告一個pages變數等於當前pdf檔案的頁數
}
}
複製程式碼
- 獲取當前pdf檔案的物件和當前pdf檔案的所有頁數後,迴圈遍歷每個頁數,執行如下操作:
1)動態建立canvas節點
2)呼叫pdf物件原型上的getPage()方法和getViewport()方法,依次傳入當前迴圈的頁數和canvas的縮放大小(這裡不懂的可以直接複製黏貼)
3)渲染當前的canvas節點
4)呼叫page物件的render()方法渲染當前頁,此方法也是一個promise,需要使用await關鍵字等到狀態為resolve後再執行之後的程式碼
5)給顯示當前頁面的canvas節點一個className為canvas方便修改樣式,最後把這個canvas節點插入到pdfList節點中
async showPdf() {
let pdfList = document.querySelector('.pdfList') //通過querySelector選擇DOM節點,使用document.getElementById()也一樣
for(let value of this.pdfDataList){ //遍歷後臺傳過來的pdfDataList
let base64 = value.fileValue //獲得bas464編碼
let decodedBase64 = atob(base64) //使用瀏覽器自帶的方法解碼
let pdf = await PDFJS.getDocument({data: decodedBase64}) //返回一個pdf物件
let pages = pdf.numPages //宣告一個pages變數等於當前pdf檔案的頁數
for (let i = 1; i <= pages; i++) { //迴圈頁數
let canvas = document.createElement('canvas')
let page = await pdf.getPage(i) //呼叫getPage方法傳入當前迴圈的頁數,返回一個page物件
let scale = 1;//縮放倍數,1表示原始大小
let viewport = page.getViewport(scale);
let context = canvas.getContext('2d'); //建立繪製canvas的物件
canvas.height = viewport.height; //定義canvas高和寬
canvas.width = viewport.width;
let renderContext = {
canvasContext: context,
viewport: viewport
};
await page.render(renderContext)
canvas.className = 'canvas' //給canvas節點定義一個class名,這裡我取名為canvas
pdfList.appendChild(canvas) //插入到pdfList節點的最後
}
}
}
複製程式碼
至此頁面上就會多出一個canvas節點並且顯示當前pdf檔案的第一頁的圖片,如果當前pdf檔案有多頁就會渲染出多個canvas節點,有多個pdf檔案就會先迴圈外層,然後再迴圈內層,把每個pdf檔案的每一頁都生成一個canvas節點
修改樣式
渲染出頁面後還有個要注意的點,Vue框架會給每個元件的DOM節點生成一個自定義屬性,而節點動態生成的canvas節點,並沒有data-v-xxxxx這樣的自定義屬性
而Vue會給每個元件裡面的樣式新增這個自定義屬性,Vue框架這樣做可以防止樣式的相互汙染(也就是style旁邊的scoped屬性)我們這裡可以在這個style下面再建立一個style寫入樣式來達到修改canvas樣式的效果,但是記得這樣做你整個專案裡面類名叫canvas的都會獲得這個樣式,需要注意
寫在最後
這裡使用的是動態生成canvas節點然後渲染這個節點生成的圖片,然而直接使用createElement生成一個節點並且頻繁操作DOM會對效能有一定的影響,如果有更好的方法歡迎留言交流,感謝觀看
後記
在之前的程式碼中,我們遍歷生成pdf物件的每一頁,之後動態生成canvas節點,而這樣做會讓瀏覽器反覆渲染新資訊,可以使用documentFragment來優化canvas節點的渲染
語法: let fragment = document.createDocumentFragment();
documentFragment會建立一個空的文件片段,它類似一個'倉庫',可以暫時儲存我們生成的節點,然後一次性新增到父節點中,這樣減少了渲染次數可以一定程度上提高效能,我們修改一下之前的程式碼,新增documentFragment
async showPdf() {
let pdfList = document.querySelector('.pdfList')
let fragment = document.createDocumentFragment() //生成一個空的documentFragment文件片段 //建立documentFragment儲存canvas節點一次性渲染//通過querySelector選擇DOM節點,使用document.getElementById()也一樣
for(let value of this.pdfDataList){ //遍歷後臺傳過來的pdfDataList
let base64 = value.fileValue //獲得bas464編碼
let decodedBase64 = atob(base64) //使用瀏覽器自帶的方法解碼
let pdf = await PDFJS.getDocument({data: decodedBase64}) //返回一個pdf物件
let pages = pdf.numPages //宣告一個pages變數等於當前pdf檔案的頁數
for (let i = 1; i <= pages; i++) { //迴圈頁數
let canvas = document.createElement('canvas')
let page = await pdf.getPage(i) //呼叫getPage方法傳入當前迴圈的頁數,返回一個page物件
let scale = 1;//縮放倍數,1表示原始大小
let viewport = page.getViewport(scale);
let context = canvas.getContext('2d'); //建立繪製canvas的物件
canvas.height = viewport.height; //定義canvas高和寬
canvas.width = viewport.width;
let renderContext = {
canvasContext: context,
viewport: viewport
};
await page.render(renderContext)
canvas.className = 'canvas' //給canvas節點定義一個class名,這裡我取名為canvas
fragment.appendChild(canvas) //新增canvas節點到fragment文件片段中
}
pdfList.appendChild(fragment) //將fragment插入到pdfList節點的最後
}
}
複製程式碼