pdf預覽本來打算粗暴點,一次性查看全部,但是一個pdf四五百頁導致手機端查看超出內存直接崩掉,崩掉會導致頁面瘋狂刷新,所以不得不進行優化
解決思路大致如下:
- canvas轉為blob格式以圖片的形式加載在頁面(Blob URL 是基于磁盤的臨時文件,可以減少內存占用)
- 分段按需加載,根據頁面滑動位置決定加載哪頁數據
- 歷史pdf加載數據緩存,避免一直調用獲取pdf邏輯,但不可緩存全部歷史數據,變量數據過多也會導致崩掉
- 及時移除畫布和圖片,確保內存被釋放
具體實現看代碼吧!對了,我這里用的是vue3框架,方案大致都差不多,可供參考
- 安裝 pdfjs-dist包(版本為4.10.38)
npm install pdfjs-dist
- 使用
<template><div class="pdf-box"><van-loading v-if="pdfLoading && !isHasFirstPage" size="24px">內容正在玩命加載中,請稍后...</van-loading><div :style="(pdfLoading && !isHasFirstPage)?'position:fixed;transform: translateX(-200%);':''"><div v-for="page in pdfPages" :id="'__pdf_canvas_page_' + page" :key="page" /></div><van-loading v-if="pdfItemLoading && isHasFirstPage" size="24px" style="padding-top:0">滑慢點~,小的加載不過來啦...</van-loading></div>
</template><script setup>
import { ref, onMounted, nextTick, onUnmounted } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'
import Worker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker'const pdfjsWorker = new Worker()
let pdf = null
const pdfPages = ref(0) // pdf總頁數
const pdfLoading = ref(true) // pdf 總頁數獲取loading
let cachedPages = new Map() // pdf歷史 圖片信息緩存
const pdfItemLoading = ref(false) // pdf 單獨頁面加載
const MAX_CACHED_PAGES = 6 // 最大緩存頁面數
const lastScrollY = ref(0) // 最后一次滾動位置
const scrollDirection = ref('') // 頁面滾動方向 up:上 down:下
const isHasFirstPage = ref(false) // 第一頁是否已加載出來// 在組件掛載后加載 PDF
onMounted(() => {const pdfUrl = 'https://example.com/sample.pdf' // 替換為你的 PDF 文件 URLloadPdf(pdfUrl)window.addEventListener('scroll', handleScroll) // 滾動變化時更新條件
})
// 清理事件監聽器
onUnmounted(() => {window.removeEventListener('scroll', handleScroll)if (pdf) {pdf.destroy()}
})
// 加載pdf
async function loadPdf(url) {pdfjsLib.GlobalWorkerOptions.workerPort = pdfjsWorkertry {const loadingTask = pdfjsLib.getDocument(url)pdf = await loadingTask.promisepdfPages.value = Number(pdf.numPages)pdfLoading.value = falseawait nextTick(() => {handleScroll()})} catch (err) {pdfLoading.value = falseconsole.error(err)}
}
// 渲染指定頁面pdf
const renderPage = (pageNumber) => {return new Promise(() => {(async () => {const page = await pdf.getPage(pageNumber)const viewport = page.getViewport({ scale: 1.5 })const canvas = document.createElement('canvas')const ctx = canvas.getContext('2d')canvas.height = viewport.heightcanvas.width = viewport.widthawait page.render({ canvasContext: ctx, viewport }).promise// 將畫布內容緩存為圖片const blobURL = await canvasToBlobURL(canvas)const image = new Image()image.src = blobURL// 將畫布內容轉換為 Blob URLimage.id = `page-image-${pageNumber}`image.style.width = '100%'// 將圖片添加到容器中const container = document.getElementById(`__pdf_canvas_page_${pageNumber}`)container.innerHTML = ''container.appendChild(image)// 緩存 Blob URL 和 Image 標簽cachePage(pageNumber, { blobURL, image: image.src })pdfItemLoading.value = false// resolve()})()})
}
// 緩存頁面
const cachePage = (pageNumber, data) => {cachedPages.set(pageNumber, data)if (cachedPages.size > 0) { // 第一頁是否已加載出來isHasFirstPage.value = true}if (cachedPages.size > MAX_CACHED_PAGES) {// 如果緩存數量超過限制cachedPages = new Map([...cachedPages.entries()].sort((a, b) => a[0] - b[0])) // 排序let oldestPage = ''if (scrollDirection.value === 'up') { // 往上滑動 移除最后的頁面const keysArray = [...cachedPages.entries()]oldestPage = keysArray[keysArray.length - 1][0]} else { // 往上滑動 移除最早的頁面oldestPage = cachedPages.keys().next().value}if (oldestPage && pageNumber !== oldestPage) {unloadPage(oldestPage)}}
}
// 將畫布內容轉換為 Blob URL
const canvasToBlobURL = (canvas) => {return new Promise((resolve) => {canvas.toBlob((blob) => {const url = URL.createObjectURL(blob)resolve(url)})})
}
// 清除非可視pdf信息
const unloadPage = (pageNumber) => {const container = document.getElementById(`__pdf_canvas_page_${pageNumber}`)const canvas = document.getElementById(`page-${pageNumber}`)const image = document.getElementById(`page-image-${pageNumber}`)const cachedData = cachedPages.get(pageNumber)if (cachedData) {const rect = container.getBoundingClientRect()container.innerHTML = ''const div = document.createElement('div')div.style.height = rect.height + 'px'container.appendChild(div)if (cachedData.blobURL) {URL.revokeObjectURL(cachedData.blobURL)} // 釋放 Blob URL}if (canvas) {canvas.remove()} // 移除畫布if (image) {image.remove()} // 移除圖片// 從緩存中移除頁面cachedPages.delete(pageNumber)
}
// 處理滾動事件
function handleScroll() {// pdfconst windowHeight = window.innerHeightconst currentScrollY = window.scrollY// 判斷往上滑動還是往下scrollDirection.value = currentScrollY > lastScrollY.value ? 'down' : 'up'lastScrollY.value = currentScrollYconst pdfPagesArr = Array.from({ length: pdfPages.value }, (_, i) => i + 1)pdfPagesArr.reduce((accumulatorPromise, next) => {return accumulatorPromise.then(() => { // 上一個接口執行完畢再執行下一個const pageElement = document.getElementById('__pdf_canvas_page_' + next)if (pageElement) {const rect = pageElement.getBoundingClientRect()if (!pdfItemLoading.value) {if ((scrollDirection.value === 'up' ? rect.top < windowHeight + 200 : rect.top < windowHeight) && rect.bottom > 0) {if (cachedPages.has(next)) { // 已加載則跳過return}pdfItemLoading.value = truereturn renderPage(next) // 如果頁面未加載,則加載該頁}}}})}, Promise.resolve())
}
</script>