背景
近来,在微信公众号集成的 H5 页面开发工作中,遇到了需要实现 PDF 文件预览功能的需求。为了找到合适的解决方案,我们先后尝试了 VueOfficePdf 和 pdfjs-dist 这两种不同的实现方式,它们均具备在移动端进行 PDF 预览的基本能力。
项目伊始,我们选用了 VueOfficePdf 组件。该组件在功能实现上看似能够满足需求,但在实际应用时却暴露出明显问题。由于其在处理文件缩放比例(scale)时精度不足,使得 PDF 文件在 <canvas> 元素上渲染后呈现出模糊不清的效果,极大地影响了用户对文件内容的查看体验,显然无法达到项目预期的要求。 鉴于此,我们将目光转向了 pdfjs-dist。然而,使用该方案的过程并不顺利,途中遭遇了一系列棘手的问题和挑战。但凭借团队的不懈努力与反复调试,最终成功基于 pdfjs-dist 实现了稳定、清晰的 PDF 预览功能。
此外,我们还尝试了另一种同样基于 PDF.js 的实现方式,即通过访问 https://mozilla.github.io/pdf.js/web/viewer.html?url=xxxx 来预览指定 URL 的 PDF 文件。优先推荐这种方式原生组件预览方式。
最新的版本如果使用报错,尝试下载历史版本的兼容性版本:
- viewers:mozilla.github.io/pdf.js/gett…
集成pdfjs
使用的pdfjs版本:www.npmjs.com/package/pdf…
- 在项目根目录下运行以下命令
npm install pdfjs-dist
- 然后在项目代码中引入
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
import pdfjsWorker from 'pdfjs-dist/legacy/build/pdf.worker.entry';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;
注意这里的引入是 * as pdfjsLib 而不是 pdfjsLib from 'pdfjs-dist',这里我们使用legacy下的资源。
pdfjs-dist/legacy
- 代码结构可能相对复杂,因为它需要兼容多种旧环境,可能会包含一些冗余的代码或者兼容性补丁。
- 文件内容可能使用了一些旧的 JavaScript 语法和特性,例如
var声明变量、function关键字定义函数等。
pdfjs-dist/build
- 代码结构更加简洁和现代化,采用了最新的 JavaScript 模块化规范,例如 ES6 模块(
import和export)。 - 文件内容可能使用了一些新的 JavaScript 特性,如箭头函数、
const和let声明变量等,以提高代码的可读性和性能。
- 自定义预览组件完整代码如下
<template>
<div class="pdf-container">
<canvas v-for="(page, index) in pages" :key="index" :ref="'pdfCanvas' + index"></canvas>
</div>
</template>
<script>
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
import pdfjsWorker from 'pdfjs-dist/legacy/build/pdf.worker.entry';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;
export default {
name: 'PdfViewer',
props: {
pdfUrl: {
type: String,
required: true,
}
},
data() {
return {
numPages: 0,
pages: []
}
},
mounted() {
this.renderPdf()
},
methods: {
renderPdf() {
const loadingTask = pdfjsLib.getDocument(this.pdfUrl)
loadingTask.promise.then(pdf => {
this.numPages = pdf.numPages
this.pages = Array.from({ length: this.numPages }, (_, index) => index + 1)
// 使用 nextTick 确保 DOM 渲染完成后再渲染页面
this.$nextTick(() => {
for (let pageNumber = 1; pageNumber <= this.numPages; pageNumber++) {
this.renderPageHighRes(pdf, pageNumber)
}
})
}).catch(error => {
console.error('Error loading PDF:', error)
})
},
renderPageHighRes(pdf, pageNumber) {
const highScale = 4 // 设置精度
pdf.getPage(pageNumber).then(page => {
const viewport = page.getViewport({ scale: highScale })
// 获取对应的 canvas 元素
const canvasArray = this.$refs['pdfCanvas' + (pageNumber - 1)];
// 线上返回的是数组,不知道什么原因,本地返回的是:canvas对象 TODO
const canvas = Array.isArray(canvasArray) ? canvasArray[0] : canvasArray;
if (canvas) {
const context = canvas.getContext('2d')
canvas.height = viewport.height
canvas.width = viewport.width
const renderContext = {
canvasContext: context,
viewport: viewport
}
page.render(renderContext).promise.catch(error => {
console.error(`Error rendering high res PDF page ${pageNumber}:`, error)
})
} else {
console.error(`Canvas element for page ${pageNumber} not found.`)
}
}).catch(error => {
console.error(`Error getting PDF page ${pageNumber}:`, error)
})
}
}
};
</script>
<style scoped>
.pdf-container {
height: 100vh;
overflow-y: auto;
}
canvas {
width: 100%;
height: auto;
display: block;
margin-bottom: 10px;
transform: translate3d(0, 0, 0); /*触发硬件加速*/
}
</style>
- 引入与使用组件
import PdfViewer from '@/components/pdf/PdfViewer.vue';
export default {
components:{
PdfViewer
}
}
// 使用组件
<PdfViewer pdfUrl="http://www.hnsdzjy.com/tpframe/AttachStorage/202412/PDF/bbb382ea-fe2d-
4303-8716-59c9bd6b47ce/18b42914-4eba-47e4-bcaa-f659e1474bef.pdf"/>
- 效果图