公司有個業務需求,要求後臺傳pdf的base64編碼給前端,前端顯示到界面上,後來在網上搜索了不少關於base64轉pdf的文章,都寫的不是很是的詳細,在實現的過程當中遇到不少坑,通過一天的研究終於實現了這個功能,分享一下我在這個功能中遇到的問題和解決方法前端
要註明的是這裏用到的核心插件是pdf.js,原理是動態生成canvas標籤,而後經過pdf.js生成一個能渲染出pdf的對象,隨後渲染每一個canvas,而且生成的pdf是畫面的形式,並無pdf之類的控件git
這裏不少博客都是使用JavaScript原生的方法引入pdf.js,例如使用script標籤引入外部的js腳本,或者直接把pdf.js的源碼複製到項目中,可是我嘗試這些方法的時候都不是特別好用,並且引入後致使項目體積過於龐大, github
隨後我去github上尋找經過包管理器引入pdf.js的方法,在pdf.js的github上官方說明的是用gulp如何使用pdf.js,可是對於npm來講並無詳細說明,終於我在字裏行間發現了這麼一句話web
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
大體的意思就是若是使用npm包管理器或者bower的話,引入的名字爲pdfjs-dist,那麼咱們使用npm的方法引入這個pdfjs-dist,引入的名字就隨意取名了這裏我叫PDFJSgulp
import PDFJS from 'pdfjs-dist'
複製代碼
這裏後臺傳給個人是一個由pdf文件名字和pdf的base64編碼組成的對象的數組,我取名爲pdfDataList canvas
能夠看到fileName是pdf的名字,fileVale是pdf文件的base64編碼,thumbnail是pdf縮略圖的base64編碼這裏用不到先無論,以前說到須要動態生成canvas節點(這裏不會canvas也沒關係,只須要根據代碼一步步作就能渲染canvas)數組
首先咱們建立一個承載全部canvas節點的父節點,取名爲pdfList promise
而後建立一個異步函數showPdf(不懂什麼是異步函數的能夠去查一下async/await,這裏不用異步函數也可使用promise.then的方法,可是async/await做爲異步操做的終極方案最好仍是學習一下)瀏覽器
async showPdf() {
}
複製代碼
你可使用
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文件的頁數
}
}
複製代碼
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節點的最後
}
}
複製代碼