在Web应用中,PDF预览是一个常见需求。今天我将分享如何使用vue-pdf实现一个支持多页同时预览的PDF查看器组件。
功能需求
我们需要实现一个PDF查看器,具有以下功能:
- 支持同时显示多页(本文以3页为例)
- 支持前后翻页浏览
- 显示加载状态
技术选型
我们使用以下技术:
- Vue.js:前端框架
- vue-pdf:pdf.js的Vue封装
- lodash,throttle:用于函数节流
核心代码实现
组件结构
<div class="multi-pdf-viewer">
<div v-if="loading" class="pdf-loading-mask">
<div class="loading-spinner"></div>
PDF加载中...
</div>
<button
class="left-btn"
@click="prevSet"
:disabled="currentSet === 1 || loading"
>
<i class="iconfont icon-zuojiantou"></i>
</button>
<div class="pdf-pages">
<pdf
:src="pdfSrc"
:page="startPage"
class="pdf-page"
@num-pages="numPages = $event"
@page-loaded="handlePageLoaded(startPage)"
></pdf>
<pdf
:src="pdfSrc"
:page="startPage + 1"
class="pdf-page"
@page-loaded="handlePageLoaded(startPage + 1)"
></pdf>
<pdf
:src="pdfSrc"
:page="startPage + 2"
class="pdf-page"
@page-loaded="handlePageLoaded(startPage + 2)"
></pdf>
</div>
<button
class="right-btn"
@click="nextSet"
:disabled="currentSet === totalSets || loading"
>
<i class="iconfont icon-youjiantou"></i>
</button>
</div>
</template>
JavaScript逻辑
import { throttle } from 'lodash'
import pdf from 'vue-pdf'
import { GlobalWorkerOptions } from 'pdfjs-dist'
export default {
name: 'VuePdfViewer',
components: { pdf },
props: {
fileUrl: {
type: String,
default: '',
},
},
data() {
return {
loading: false,
currentComp: 'pdf',
pdfSrc: null,
numPages: 0,
currentSet: 1, //当前组
pagesPerSet: 3, //每组页数
forceUpdateKey: 0 loadedPages: new Set(), // 记录已加载页面
loadTimeout: null,
}
},
//切换PDF文件
watch: {
fileUrl() {
this.loadPdf()
},
},
created() {
GlobalWorkerOptions.workerSrc =
'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.10.377/build/pdf.worker.min.js'
this.loadPdf()
},
computed: {
// 计算总组数
totalSets() {
return Math.ceil(this.numPages / this.pagesPerSet)
},
// 当前组的起始页
startPage() {
return (this.currentSet - 1) * this.pagesPerSet + 1
},
// 当前组的结束页
endPage() {
return Math.min(this.currentSet * this.pagesPerSet, this.numPages)
},
},
methods: {
// 加载PDF文件
loadPdf() {
this.currentSet = 1
this.loadedPages = new Set()
this.pdfSrc = pdf.createLoadingTask({
url: this.fileUrl,
cMapPacked: true,
})
this.pdfSrc.promise
.then((pdf) => {
this.numPages = pdf.numPages
console.log(' this.numPages:', this.numPages)
})
.catch((error) => {
console.error('PDF加载失败:', error)
this.$baseMessage('PDF加载失败', 'error', 'vab-hey-message2-error')
})
}, // 使用 throttle 控制 1s 内只能触发一次翻页
// 上一组
prevSet: throttle(function () {
if (this.currentSet > 1 && !this.loading) {
this.loading = true
this.loadedPages = new Set() // 重置加载状态
this.currentSet--
}
}, 1000), // 节流间隔-1s
// 下一组
nextSet: throttle(function () {
if (this.currentSet < this.totalSets && !this.loading) {
this.loading = true
this.loadedPages = new Set()
this.currentSet++
}
}, 1000), //加载页面的回调
handlePageLoaded(page) {
clearTimeout(this.loadTimeout)
if (page >= this.startPage && page <= this.endPage) {
this.loadedPages.add(page)
const expectedPageCount = Math.min(
this.pagesPerSet,
this.numPages - (this.currentSet - 1) * this.pagesPerSet
)
if (this.loadedPages.size >= expectedPageCount) {
this.$nextTick(() => {
this.loading = false
})
} else {
// 设置超时,防止某些页面永远不触发loaded事件
this.loadTimeout = setTimeout(() => {
if (this.loading) {
console.warn(
`加载超时,已加载 ${this.loadedPages.size}/${expectedPageCount} 页`
)
this.loading = false
}
}, 5000) // 5秒超时
}
}
}, },
}
</script>
样式部分
.multi-pdf-viewer {
position: relative;
width: 100%;
margin: 0 auto;
background: #f5f5f5;
// padding: 20px;
border-radius: 4px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
.left-btn {
position: absolute;
top: 50%;
left: 0;
}
.right-btn {
position: absolute;
top: 50%;
right: 0;
}
button {
width: 45px;
height: 45px;
border-radius: 50%;
background: #409eff;
border: none;
color: #fff;
cursor: pointer;
z-index: 999;
i {
font-size: 35px;
font-weight: 600;
}
}
button:disabled {
background: #c0c4cc;
cursor: not-allowed;
}
}
.pdf-pages {
position: relative;
display: flex;
justify-content: center;
gap: 20px;
padding: 10px;
background: white;
border-radius: 4px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.05);
}
.pdf-page {
flex: 1;
min-width: 0;
transform-origin: top center;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.pdf-controls {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.page-indicator {
font-size: 16px;
color: #333;
}
.pdf-loading-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
font-size: 16px;
color: #333;
border-radius: 4px;
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
实现要点解析
多页渲染:
- 通过
currentPages计算属性动态生成需要渲染的页码数组
翻页控制:
- 使用
currentSet记录当前组数 - 通过
totalSets计算总组数 - 翻页按钮使用节流函数(throttle)控制点击频率
加载状态管理:
- 使用
loading状态控制加载蒙层显示 - 通过
loadedPages集合记录已加载完成的页面 - 设置超时机制防止页面加载卡死
性能优化:
- 使用
forceUpdateKey强制重新渲染PDF组件 - 每次翻页时清空已加载页面记录
错误处理:
- 捕获PDF加载错误并显示友好提示
- 加载超时处理
使用示例:
<div>
<vue-pdf-viewer :file-url="pdfUrl" />
</div>
</template>
<script>
import VuePdfViewer from './components/VuePdfViewer.vue'
export default {
components: {
VuePdfViewer
},
data() {
return {
pdfUrl: '/example.pdf'
}
}
}
</script>
当前实现存在的痛点与思考
在我的多页PDF预览组件实现中,还存在一些亟待优化的技术问题:
- 硬编码问题
目前采用固定3页为一组的展示方式,直接在模板中编写了三个<pdf>组件。这种实现方式虽然简单,但缺乏灵活性。如果需要调整为10页一组预览,这种硬编码的方式显然无法满足需求。 - 动态渲染挑战
理论上这部分应该使用v-for循环来实现动态渲染,但在实际尝试过程中遇到了组件销毁相关的报错("... destroyed"错误)。经过多次调试仍未能解决,最终不得不暂时采用编写三个独立组件的方案。 - 寻求更好的解决方案
非常希望能与各位开发者交流,如果您有处理类似场景的经验或更好的实现思路,欢迎在评论区分享您的见解。特别是关于如何优雅地实现动态多页PDF渲染的方案,期待您的宝贵建议!
v-for="n in visiblePageCount"
:key="`${currentPage + n - 1}-${refreshKey}`"
:src="pdfSrc"
:page="currentPage + n - 1"
class="pdf-page"
@page-loaded="onPageLoaded"
></pdf>
...
computed: {
visiblePageCount() {
return Math.min(this.pageSize, this.numPages - this.currentPage + 1)
}
},
关于节流功能的使用说明
在实现翻页功能时,我采用了节流(throttle)机制,主要是为了解决以下核心问题:
- 频繁点击导致的渲染异常
当用户快速连续点击翻页按钮时,会出现PDF容器节点已经渲染完成,但实际内容却未能正确加载显示的情况。这种异步渲染的竞态条件会导致用户体验的不连贯。 - 性能优化考虑
不加控制的频繁翻页请求会对浏览器造成不必要的渲染压力,特别是对于大型PDF文档,可能引起界面卡顿或内存问题。 - 当前解决方案
通过lodash的throttle函数将翻页操作限制为每秒最多触发一次(1000ms间隔),这样既保证了操作的流畅性,又避免了上述问题的发生。
当然,这只是一个折中的解决方案。如果有更优雅的处理方式,非常期待各位技术同行的分享和建议。毕竟在用户体验和性能优化方面,总有值得改进的空间。