uniapp (h5)使用pdf.js
uniapp(h5)使用pdf.js
一、背景与问题
在H5开发中,经常会遇到需要展示PDF文档的场景。传统做法是通过<iframe>或第三方插件,但这些方案存在诸多限制:
- 无法直接操作PDF内容
- 无法实现自定义渲染
- 无法兼容复杂PDF格式(如嵌入字体、图像等)
- 无法实现跨平台一致性渲染
pdf.js作为Mozilla开发的开源PDF渲染库,提供了完整的PDF解析和渲染能力。在uniapp中使用pdf.js可以实现:
- 自定义PDF渲染
- 支持PDF/A、PDF/X等格式
- 实现PDF缩放、翻页、书签等功能
- 与uniapp的跨平台特性深度结合
但需要注意:
- 需要处理PDF文件的加载和解析
- 需要处理不同PDF版本的兼容性
- 需要处理跨平台渲染差异
- 需要处理内存占用和性能优化
二、基本原理
pdf.js的工作原理分为三个核心阶段:
- PDF文件解析
使用pdf.js的getDocument()方法加载PDF文件,返回一个PDFDocumentProxy对象。通过pdf.DocumentProxy的promise获取完整的PDF文档对象。 - 页面渲染
通过PDFPageProxy对象获取每个页面的渲染信息,使用getOperatorList()获取渲染指令,最终通过render()方法将页面内容绘制到canvas上。 - 交互控制
通过监听page、scale、rotation等参数变化,动态更新canvas的渲染内容。
在uniapp中使用时,需要处理:
- PDF文件的本地路径或网络路径
- canvas的尺寸适配
- 多平台渲染差异(如iOS和Android的canvas渲染差异)
- 大文件的内存占用问题
三、环境准备
1. 项目依赖
在manifest.json中添加网络权限:
{
"plus": {
"distribute": {
"web": {
"enable": true
}
}
}
}安装pdf.js依赖:
npm install pdfjs-dist2. 引入pdf.js
在pages.json中配置页面:
{
"pages": [
{
"path": "pages/pdf-view/index",
"style": {
"navigationBarTitleText": "PDF Viewer"
}
}
]
}四、核心实现
1. PDF文件加载
<template>
<view class="container">
<canvas
id="pdfCanvas"
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
ref="canvasRef"
></canvas>
<button @click="loadPDF">加载PDF</button>
</view>
</template>
<script>
import { getPDFDocument } from '@/utils/pdfUtils.js'
export default {
data() {
return {
pdf: null,
currentPage: 1,
canvasWidth: 800,
canvasHeight: 1050
}
},
methods: {
async loadPDF() {
try {
const file = await uni.chooseFile({
type: 'file',
extension: 'pdf'
})
const filePath = file.tempFilePath
this.pdf = await getPDFDocument(filePath)
this.renderPage()
} catch (err) {
console.error('加载PDF失败:', err)
}
},
renderPage() {
if (!this.pdf || this.currentPage > this.pdf.numPages) return
const page = this.pdf.getPage(this.currentPage)
const canvas = this.$refs.canvasRef
const ctx = canvas.getContext('2d')
const viewport = page.getViewport({
scale: 1.5,
rotation: 0
})
const renderContext = {
canvasContext: ctx,
viewport: viewport
}
await page.render(renderContext)
}
}
}
</script>关键点解释:
- 使用
getPDFDocument封装了pdf.js的加载逻辑 - 使用
getPage()获取当前页的PDFPageProxy - 通过
getViewport()计算渲染区域 - 使用
render()方法进行页面渲染 - 设置
scale参数控制渲染比例
2. 页面翻页交互
<template>
<view class="toolbar">
<button @click="prevPage">上一页</button>
<span>第 {{ currentPage }} / {{ totalPages }} 页</span>
<button @click="nextPage">下一页</button>
</view>
</template>
<script>
export default {
computed: {
totalPages() {
return this.pdf ? this.pdf.numPages : 0
}
},
methods: {
prevPage() {
if (this.currentPage > 1) {
this.currentPage--
this.renderPage()
}
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++
this.renderPage()
}
}
}
}
</script>3. 缩放功能实现
<template>
<view class="toolbar">
<slider
:value="scale"
min="1"
max="3"
step="0.1"
@change="setScale"
></slider>
<span>缩放: {{ scale }}x</span>
</view>
</template>
<script>
export default {
data() {
return {
scale: 1.5
}
},
methods: {
setScale(e) {
this.scale = e.detail.value
this.renderPage()
}
}
}
</script>五、完整案例
1. 项目结构
src/
├── pages/
│ └── pdf-view/
│ ├── index.vue
│ └── pdfUtils.js
├── utils/
│ └── pdfUtils.js
├── App.vue
└── main.js2. pdfUtils.js
import { getWorker, getDocument } from 'pdfjs-dist'
export async function getPDFDocument(filePath) {
// 设置pdf.js的worker路径
const workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.dist.version}/build/pdf.worker.min.js`
const worker = await getWorker(workerSrc)
// 配置pdf.js参数
const pdf = await getDocument({
url: filePath,
worker: worker
}).promise
return pdf
}3. 完整页面实现
<template>
<view class="container">
<canvas
id="pdfCanvas"
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
ref="canvasRef"
></canvas>
<view class="toolbar">
<button @click="prevPage">上一页</button>
<span>第 {{ currentPage }} / {{ totalPages }} 页</span>
<button @click="nextPage">下一页</button>
<slider
:value="scale"
min="1"
max="3"
step="0.1"
@change="setScale"
></slider>
<span>缩放: {{ scale }}x</span>
</view>
</view>
</template>
<script>
import { getPDFDocument } from '@/utils/pdfUtils.js'
export default {
data() {
return {
pdf: null,
currentPage: 1,
scale: 1.5,
canvasWidth: 800,
canvasHeight: 1050
}
},
computed: {
totalPages() {
return this.pdf ? this.pdf.numPages : 0
}
},
methods: {
async loadPDF() {
try {
const file = await uni.chooseFile({
type: 'file',
extension: 'pdf'
})
const filePath = file.tempFilePath
this.pdf = await getPDFDocument(filePath)
this.renderPage()
} catch (err) {
console.error('加载PDF失败:', err)
}
},
renderPage() {
if (!this.pdf || this.currentPage > this.pdf.numPages) return
const page = this.pdf.getPage(this.currentPage)
const canvas = this.$refs.canvasRef
const ctx = canvas.getContext('2d')
const viewport = page.getViewport({
scale: this.scale,
rotation: 0
})
const renderContext = {
canvasContext: ctx,
viewport: viewport
}
await page.render(renderContext)
},
prevPage() {
if (this.currentPage > 1) {
this.currentPage--
this.renderPage()
}
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++
this.renderPage()
}
},
setScale(e) {
this.scale = e.detail.value
this.renderPage()
}
}
}
</script>六、源码解析
1. pdf.js核心流程
// pdf.js核心流程
const workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.dist.version}/build/pdf.worker.min.js`
const worker = await getWorker(workerSrc)
const pdf = await getDocument({
url: filePath,
worker: worker
}).promise
const page = await pdf.getPage(pageNumber)
const viewport = page.getViewport({ scale: scale })
const renderContext = {
canvasContext: ctx,
viewport: viewport
}
await page.render(renderContext)关键点:
- 使用Worker线程处理PDF解析,避免阻塞主线程
getDocument()返回的PDF对象包含所有页面信息getPage()获取单个页面对象getViewport()计算渲染区域render()执行实际渲染
2. canvas绘制细节
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.setTransform(1, 0, 0, 1, 0, 0) // 重置变换矩阵
ctx.scale(scale, scale) // 设置缩放
ctx.translate(0, 0) // 调整位置七、进阶使用
1. 动态加载PDF
async function loadPDF(url) {
const worker = await getWorker(workerSrc)
const pdf = await getDocument({
url: url,
worker: worker
}).promise
return pdf
}2. 缓存优化
const pageCache = new Map()
async function renderPage(pageNumber) {
if (pageCache.has(pageNumber)) {
return pageCache.get(pageNumber)
}
const page = await pdf.getPage(pageNumber)
const viewport = page.getViewport({ scale: scale })
const renderContext = {
canvasContext: ctx,
viewport: viewport
}
await page.render(renderContext)
pageCache.set(pageNumber, page)
return page
}3. 多PDF管理
class PDFManager {
constructor() {
this.pdfs = new Map()
}
addPDF(pdf, id) {
this.pdfs.set(id, pdf)
}
getPDF(id) {
return this.pdfs.get(id)
}
}八、性能与工程实践
1. 性能优化策略
| 优化点 | 方法 | 效果 |
|---|---|---|
| 内存占用 | 使用Worker线程 | 降低主线程负载 |
| 渲染性能 | 分页渲染 | 减少canvas重绘 |
| 响应速度 | 缓存页面对象 | 提高翻页速度 |
| 资源加载 | 压缩PDF | 减少网络请求 |
2. 安全注意事项
- 避免直接渲染用户上传的PDF文件
- 对PDF文件进行校验(文件类型、大小)
- 使用沙箱环境运行PDF解析
- 对用户输入的PDF内容进行过滤
3. 异常处理
try {
const pdf = await getPDFDocument(filePath)
} catch (err) {
console.error('PDF解析失败:', err)
uni.showToast({
title: 'PDF解析失败',
icon: 'none'
})
}九、常见问题与踩坑
1. 常见错误及解决办法
| 错误 | 原因 | 解决办法 |
|---|---|---|
| PDF加载失败 | 文件路径错误 | 检查文件路径 |
| 渲染不全 | canvas尺寸不足 | 调整canvas尺寸 |
| 缩放失效 | 未设置canvas变换 | 检查canvas绘制代码 |
| 内存溢出 | 大PDF文件 | 分页加载 |
2. 典型问题分析
问题:PDF渲染不全
// 错误代码
const canvas = this.$refs.canvasRef
canvas.width = 800
canvas.height = 1050原因: 未正确设置canvas的尺寸
解决办法:
// 正确代码
const canvas = this.$refs.canvasRef
canvas.width = 800 * scale
canvas.height = 1050 * scale十、最佳实践
1. 推荐使用场景
- 需要展示PDF文档的H5应用
- 需要自定义PDF渲染的场景
- 需要支持PDF/A、PDF/X等格式的项目
- 需要实现PDF缩放、翻页等交互功能
2. 不推荐使用场景
- 需要处理大量PDF文件的场景
- 需要高性能PDF处理的场景(如PDF转Word)
- 需要处理PDF内容编辑的场景
- 需要处理PDF加密、签名等安全功能的场景
十一、总结
在uniapp中使用pdf.js可以实现完整的PDF文档渲染功能,但需要关注以下几个关键点:
- PDF解析:需要正确配置Worker线程和解析参数
- 渲染控制:需要处理缩放、翻页等交互功能
- 性能优化:需要处理大文件加载和内存占用问题
- 安全风险:需要对用户输入的PDF文件进行校验
- 跨平台适配:需要处理不同平台的canvas渲染差异
通过合理的设计和实现,可以构建一个功能完善、性能稳定的PDF查看器。对于需要深度定制PDF处理功能的项目,pdf.js是值得推荐的解决方案。但需要注意,在处理复杂PDF文档或需要高性能处理时,可能需要结合其他工具(如PDF.js的高级API或专用PDF处理库)来实现更复杂的功能。
评论已关闭