pdf预览本来打算粗暴点,一次性查看全部,但是一个pdf四五百页导致手机端查看超出内存直接崩掉,崩掉会导致页面疯狂刷新,所以不得不进行优化
解决思路大致如下:
- canvas转为blob格式以图片的形式加载在页面(Blob URL 是基于磁盘的临时文件,可以减少内存占用)
- 分段按需加载,根据页面滑动位置决定加载哪页数据
- 历史pdf加载数据缓存,避免一直调用获取pdf逻辑,但不可缓存全部历史数据,变量数据过多也会导致崩掉
- 及时移除画布和图片,确保内存被释放
具体实现看代码吧!对了,我这里用的是vue3框架,方案大致都差不多,可供参考
- 安装 pdfjs-dist包(版本为4.10.38)
npm install pdfjs-dist
- 使用
javascript
<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 文件 URL
loadPdf(pdfUrl)
window.addEventListener('scroll', handleScroll) // 滚动变化时更新条件
})
// 清理事件监听器
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
if (pdf) {
pdf.destroy()
}
})
// 加载pdf
async function loadPdf(url) {
pdfjsLib.GlobalWorkerOptions.workerPort = pdfjsWorker
try {
const loadingTask = pdfjsLib.getDocument(url)
pdf = await loadingTask.promise
pdfPages.value = Number(pdf.numPages)
pdfLoading.value = false
await nextTick(() => {
handleScroll()
})
} catch (err) {
pdfLoading.value = false
console.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.height
canvas.width = viewport.width
await page.render({ canvasContext: ctx, viewport }).promise
// 将画布内容缓存为图片
const blobURL = await canvasToBlobURL(canvas)
const image = new Image()
image.src = blobURL// 将画布内容转换为 Blob URL
image.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() {
// pdf
const windowHeight = window.innerHeight
const currentScrollY = window.scrollY
// 判断往上滑动还是往下
scrollDirection.value = currentScrollY > lastScrollY.value ? 'down' : 'up'
lastScrollY.value = currentScrollY
const 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 = true
return renderPage(next) // 如果页面未加载,则加载该页
}
}
}
})
}, Promise.resolve())
}
</script>