PDF.js 在 Vue 中的使用指南
什么是 PDF.js
PDF.js 是 Mozilla 开源的、用 JavaScript 编写的 PDF 渲染库,用于在浏览器中解析并渲染 PDF 文档,而无需依赖 Adobe Reader 等原生插件。它被 Firefox 内置使用,也广泛应用于各类 Web 应用中。
主要特点:
-
纯前端实现:在浏览器中解析和渲染 PDF,不依赖服务端或本地插件 -
跨平台:支持现代浏览器,可与 Vue、React 等框架集成 -
功能完整:支持文本选择与复制、缩放、分页、链接、注释等 -
开源维护:由 Mozilla 维护,文档和社区资源丰富
典型用途: 在线文档预览、电子书阅读、报表/合同查看、打印预览等。本文介绍如何在 Vue 项目中集成 PDF.js,并给出几种常见使用方式。
环境准备
1. 安装依赖
npm install pdfjs-dist
2. 配置 Worker
PDF.js 需要 Worker 来处理 PDF 解析。推荐使用本地 Worker 文件:
步骤 1:复制 Worker 文件到 public 目录
# 从 node_modules 复制 worker 文件到 public 目录copy node_modules\pdfjs-dist\build\pdf.worker.min.mjs public\pdf.worker.min.mjs
步骤 2:在代码中配置 Worker
import * as pdfjsLib from'pdfjs-dist'// 方式1: 使用本地 worker 文件(推荐,稳定可靠)pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs'// 方式2: 使用 CDN(备选方案,需要网络连接)// pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`
3. Vite 配置(可选)
如果需要支持 ES2015 打包场景,在 vite.config.ts 中添加:
exportdefaultdefineConfig({// ... 其他配置build: {target: 'es2015',rollupOptions: {output: {format: 'es' } } },optimizeDeps: {include: ['pdfjs-dist', 'pdfjs-dist/web/pdf_viewer.mjs'] }})
方式一:单页查看器(基础版)
特点
-
✅ 单页显示,分页导航 -
✅ 支持缩放功能 -
✅ 代码简单,易于理解 -
❌ 不支持文本选择和复制 -
❌ 一次只显示一页
核心代码
<template> <div class="pdf-viewer-container"> <!-- 文件上传 --> <el-upload :on-change="handleFileChange"> <el-button>选择 PDF 文件</el-button> </el-upload> <!-- 控制按钮 --> <div v-if="pdfDoc"> <el-button @click="previousPage">上一页</el-button> <el-button>{{ currentPage }} / {{ totalPages }}</el-button> <el-button @click="nextPage">下一页</el-button> <el-button @click="zoomIn">放大</el-button> <el-button @click="zoomOut">缩小</el-button> </div> <!-- Canvas 渲染 --> <div class="pdf-viewer"> <canvas ref="pdfCanvas"></canvas> </div> </div></template><script setup lang="ts">import { ref, onMounted } from 'vue'import * as pdfjsLib from 'pdfjs-dist'// 设置 workerpdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs'// 响应式数据const pdfDoc = ref<any>(null)const currentPage = ref(1)const totalPages = ref(0)const scale = ref(1.5)const pdfCanvas = ref<HTMLCanvasElement | null>(null)// 加载 PDFconst loadPdf = async (data: Uint8Array) => { const loadingTask = pdfjsLib.getDocument({ data }) pdfDoc.value = await loadingTask.promise totalPages.value = pdfDoc.value.numPages await renderPage(1)}// 渲染页面const renderPage = async (pageNum: number) => { const page = await pdfDoc.value.getPage(pageNum) const viewport = page.getViewport({ scale: scale.value }) const canvas = pdfCanvas.value if (!canvas) return canvas.height = viewport.height canvas.width = viewport.width const context = canvas.getContext('2d') await page.render({ canvasContext: context, viewport: viewport }).promise}</script>
演示

适用场景
-
简单的 PDF 预览需求 -
只需要单页查看 -
不需要文本选择功能
方式二:多页查看器(性能优化版)
特点
-
✅ 显示所有页面,垂直滚动 -
✅ 懒加载优化(只渲染可见页面) -
✅ 支持文本选择和复制 -
✅ 性能优化,适合大文档 -
✅ 使用 Intersection Observer 实现懒加载
核心代码
<template> <div class="pdf-viewer-container"> <!-- 文件上传 --> <el-upload :on-change="handleFileChange"> <el-button>选择 PDF 文件</el-button> </el-upload> <!-- 所有页面容器 --> <div class="pdf-viewer" ref="pdfViewer"> <div v-for="pageNum in totalPages" :key="pageNum" :data-page="pageNum"> <div class="page-header">第 {{ pageNum }} 页</div> <div class="page-content"> <canvas class="page-canvas"></canvas> <div class="text-layer"></div> </div> </div> </div> </div></template><script setup lang="ts">import { ref, onMounted, onUnmounted } from 'vue'import * as pdfjsLib from 'pdfjs-dist'// 懒加载相关const renderingPages = ref<Set<number>>(new Set())const renderedPages = ref<Set<number>>(new Set())let intersectionObserver: IntersectionObserver | null = null// 初始化懒加载const initLazyLoading = async () => { intersectionObserver = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const pageNum = parseInt(entry.target.getAttribute('data-page') || '0') if (pageNum > 0 && !renderedPages.value.has(pageNum)) { renderPage(pageNum) } } }) }, { rootMargin: '200px 0px', // 提前 200px 开始加载 threshold: 0.01 } ) // 观察所有页面容器 for (let pageNum = 1; pageNum <= totalPages.value; pageNum++) { const pageWrapper = pdfViewer.value?.querySelector(`[data-page="${pageNum}"]`) if (pageWrapper) { intersectionObserver.observe(pageWrapper) } } // 立即渲染前几页 for (let pageNum = 1; pageNum <= Math.min(3, totalPages.value); pageNum++) { renderPage(pageNum) }}// 渲染单个页面(包含文本层)const renderPage = async (pageNum: number) => { if (renderingPages.value.has(pageNum) || renderedPages.value.has(pageNum)) { return } renderingPages.value.add(pageNum) const pageWrapper = pdfViewer.value?.querySelector(`[data-page="${pageNum}"]`) const canvas = pageWrapper?.querySelector('canvas') as HTMLCanvasElement const textLayer = pageWrapper?.querySelector('.text-layer') as HTMLElement const page = await pdfDoc.value.getPage(pageNum) const viewport = page.getViewport({ scale: scale.value }) // 渲染 Canvas canvas.height = viewport.height canvas.width = viewport.width const context = canvas.getContext('2d') await page.render({ canvasContext: context, viewport: viewport }).promise // 渲染文本层(用于文字选择和复制) const textContent = await page.getTextContent() // ... 文本层渲染逻辑 renderingPages.value.delete(pageNum) renderedPages.value.add(pageNum)}</script>
性能优化要点
-
懒加载:使用 Intersection Observer 只渲染可见页面 -
分批渲染:缩放时按批次重新渲染,避免阻塞 UI -
状态管理:跟踪已渲染页面,避免重复渲染
演示

适用场景
-
需要查看所有页面 -
大文档(100+ 页) -
需要文本选择和复制 -
需要性能优化
方式三:官方组件化查看器(推荐)
特点
-
✅ 使用 PDF.js 官方组件(PDFViewer、PDFLinkService) -
✅ 自动文本层和注释层渲染 -
✅ 完整的事件系统 -
✅ 官方维护,稳定可靠 -
✅ 可自定义样式和行为 -
⭐ 这是 PDF.js 官方推荐的生产环境使用方式
核心代码
<template> <div class="pdf-viewer-container"> <div class="pdf-controls"> <el-upload :on-change="handleFileChange"> <el-button>选择 PDF 文件</el-button> </el-upload> </div> <div class="pdf-viewer-wrapper"> <div ref="pdfViewerContainer" class="pdf-viewer-container-inner"></div> </div> </div></template><script setup lang="ts">import { ref, onMounted, onUnmounted } from 'vue'import * as pdfjsLib from 'pdfjs-dist'import { PDFViewer, EventBus, PDFLinkService } from 'pdfjs-dist/web/pdf_viewer.mjs'import 'pdfjs-dist/web/pdf_viewer.css'// 设置 workerpdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs'let pdfViewer: PDFViewer | null = nulllet eventBus: EventBus | null = nullconst pdfViewerContainer = ref<HTMLElement | null>(null)// 初始化 PDF 查看器const initPDFViewer = async (pdfDocument: any) => { const container = pdfViewerContainer.value // 创建滚动容器(必须是绝对定位) const scrollContainer = document.createElement('div') scrollContainer.className = 'pdf-viewer-scroll' scrollContainer.style.position = 'absolute' scrollContainer.style.top = '0' scrollContainer.style.left = '0' scrollContainer.style.width = '100%' scrollContainer.style.height = '100%' scrollContainer.style.overflow = 'auto' // 创建查看器容器 const viewer = document.createElement('div') viewer.className = 'pdfViewer' scrollContainer.appendChild(viewer) container.appendChild(scrollContainer) // 创建事件总线和链接服务 eventBus = new EventBus() const linkService = new PDFLinkService({ eventBus: eventBus, externalLinkTarget: 2 }) // 创建 PDFViewer 实例 pdfViewer = new PDFViewer({ container: scrollContainer, // 外层滚动容器(必须绝对定位) viewer: viewer, // 内层查看器容器 eventBus: eventBus, linkService: linkService }) // 设置 PDF 文档 pdfViewer.setDocument(pdfDocument) // 监听事件 eventBus.on('pagesinit', () => { totalPages.value = pdfViewer?.pagesCount || 0 }) eventBus.on('scalechanging', (evt: any) => { currentScale.value = evt.scale })}// 清理onUnmounted(() => { if (pdfViewer) { pdfViewer.cleanup() }})</script>
重要配置
-
Container 必须是绝对定位:
scrollContainer.style.position = 'absolute'
-
需要两个容器: -
container: 外层滚动容器(绝对定位) -
viewer: 内层查看器容器(.pdfViewer) -
导入样式:
import'pdfjs-dist/web/pdf_viewer.css'
演示

适用场景
-
⭐ 生产环境(最推荐) -
需要完整功能(文本选择、注释、链接) -
需要官方维护和更新 -
需要自定义样式和行为
方式四:iframe 嵌入官方 HTML
特点
-
✅ 直接使用官方 HTML 文件 -
✅ 功能完整,无需额外开发 -
❌ CDN 版本无法访问 Blob URL(跨域限制) -
❌ 样式隔离,难以自定义 -
❌ 无法深度集成到 Vue 应用 -
⚠️ 不推荐用于生产环境
核心代码
<template> <div class="pdf-viewer-container"> <el-upload :on-change="handleFileChange"> <el-button>选择 PDF 文件</el-button> </el-upload> <div class="pdf-viewer-wrapper"> <iframe v-if="viewerUrl" :src="viewerUrl" class="pdf-viewer-iframe" frameborder="0" ></iframe> </div> </div></template><script setup lang="ts">import { ref } from 'vue'import * as pdfjsLib from 'pdfjs-dist'// 设置 worker(注意:路径已更新)pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs-dist/build/pdf.worker.min.mjs'const pdfUrl = ref<string>('')const viewerUrl = ref<string>('')// 检查本地查看器是否存在const checkLocalViewer = async (): Promise<boolean> => { try { const response = await fetch('/pdfjs-dist/web/viewer.html', { method: 'HEAD' }) return response.ok } catch { return false }}// 加载 PDFconst loadPdf = async (file: File) => { // 清理旧的 URL if (pdfUrl.value) { URL.revokeObjectURL(pdfUrl.value) } // 创建 Blob URL const blob = new Blob([file], { type: 'application/pdf' }) const url = URL.createObjectURL(blob) pdfUrl.value = url // 检查本地查看器是否存在 const hasLocalViewer = await checkLocalViewer() if (!hasLocalViewer) { // 本地查看器不存在,提示用户配置 console.error('本地查看器未找到,请按照以下步骤配置:') console.error('1. 访问 https://github.com/mozilla/pdf.js/releases 下载最新版本') console.error('2. 解压后将 web 目录复制到 public/pdfjs-dist/web/') console.error('3. 确保 viewer.html 的路径是 /pdfjs-dist/web/viewer.html') return } // ✅ 必须使用本地查看器(避免跨域问题) // ⚠️ CDN 版本无法访问 Blob URL,会导致跨域错误 const viewerPath = '/pdfjs-dist/web/viewer.html' const encodedUrl = encodeURIComponent(url) viewerUrl.value = `${viewerPath}?file=${encodedUrl}`}// 清理资源onUnmounted(() => { if (pdfUrl.value) { URL.revokeObjectURL(pdfUrl.value) }})</script>
使用本地查看器
⚠️** 重要:必须使用本地查看器,CDN 版本无法访问 Blob URL(跨域限制)**
-
下载 PDF.js 官方查看器: -
访问 https://github.com/mozilla/pdf.js/releases[1] -
下载最新版本的预构建包(例如: pdfjs-5.4.449-dist.zip) -
解压后找到 web目录 -
复制到项目:
# 将 web 目录复制到 public/pdfjs-dist/web/# 最终结构应该是:public/ └── pdfjs-dist/ ├── build/ │ └── pdf.worker.min.mjs └── web/ ├── viewer.html ├── viewer.js ├── viewer.css └── ... (其他文件)
-
验证配置: -
启动开发服务器后,访问 http://localhost:3016/pdfjs-dist/web/viewer.html应该能看到 PDF.js 官方查看器界面 -
代码会自动检测本地查看器是否存在,如果不存在会提示配置步骤
演示

适用场景
-
快速原型 -
演示和测试 -
不需要深度集成 -
⚠️ 不推荐用于生产环境
最佳实践
1. Worker 配置
推荐:使用本地 Worker 文件
// ✅ 推荐方式1: 放在 public 根目录pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs'// ✅ 推荐方式2: 放在 public/pdfjs-dist/build/ 目录(与官方查看器结构一致)pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs-dist/build/pdf.worker.min.mjs'// ❌ 不推荐(可能有跨域问题)pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/...`
注意:如果使用方式四(iframe),建议将 worker 文件放在 public/pdfjs-dist/build/ 目录,与官方查看器结构保持一致。
2. 文件验证
始终验证 PDF 文件:
// 验证文件类型if (file.type !== 'application/pdf' && !file.name.endsWith('.pdf')) {thrownewError('请选择 PDF 格式的文件')}// 验证 PDF 文件头const header = String.fromCharCode(...uint8Array.slice(0, 4))if (header !== '%PDF') {thrownewError('无效的 PDF 文件格式')}
3. 错误处理
提供详细的错误信息:
try {awaitloadPdf(data)} catch (error: any) {let errorMessage = 'PDF 加载失败'if (error?.name === 'InvalidPDFException') { errorMessage = '无效的 PDF 文件结构,请确认文件是否损坏' } elseif (error?.name === 'MissingPDFException') { errorMessage = 'PDF 文件缺失或无法访问' } $message?.error(errorMessage)}
4. 性能优化
-
懒加载:使用 Intersection Observer 只渲染可见页面 -
分批渲染:缩放时按批次重新渲染 -
内存管理:及时清理 Blob URL 和查看器实例
// 清理 Blob URLonUnmounted(() => {if (pdfUrl.value) {URL.revokeObjectURL(pdfUrl.value) }if (pdfViewer) { pdfViewer.cleanup() }})
5. 文本选择支持
如果需要文本选择,必须添加文本层:
// 获取文本内容const textContent = await page.getTextContent()// 创建文本层元素textContent.items.forEach((item: any) => {const span = document.createElement('span') span.textContent = item.str span.style.position = 'absolute' span.style.color = 'transparent'// 透明,不影响视觉效果 span.style.cursor = 'text' textLayer.appendChild(span)})
常见问题
Q1: Worker 加载失败
错误:Setting up fake worker failed
解决方案:
-
确保 worker 文件在 public目录 -
使用本地 worker 文件而不是 CDN -
检查路径是否正确
Q2: PDF 文件无法加载
错误:Invalid PDF structure
解决方案:
-
验证文件是否为有效的 PDF 格式 -
检查文件是否损坏 -
添加文件头验证
Q3: 打包后无法运行
错误:CORS 错误
解决方案:
-
不要直接用 file://打开打包后的文件 -
使用 npm run preview预览 -
或使用本地服务器(http-server、Python 等)
Q4: PDFViewer 初始化失败
错误:Invalid container and/or viewer option
解决方案:
-
确保 container是绝对定位 -
提供 container和viewer两个参数 -
等待 DOM 更新后再初始化
Q5: iframe 方式跨域错误
错误:SecurityError: Failed to read a named property 'document' from 'Window': Blocked a frame with origin "https://mozilla.github.io" from accessing a cross-origin frame
原因:
-
使用 CDN 版本的官方查看器( https://mozilla.github.io) -
Blob URL 无法在跨域 iframe 中访问
解决方案:
-
必须使用本地查看器(推荐) -
下载 PDF.js 官方查看器 -
将 web目录复制到public/pdfjs-dist/web/ -
代码会自动检测本地查看器是否存在 -
路径配置:
// ✅ 正确:使用本地查看器const viewerPath = '/pdfjs-dist/web/viewer.html'// ❌ 错误:CDN 版本无法访问 Blob URLconst viewerPath = 'https://mozilla.github.io/pdf.js/web/viewer.html'
-
验证配置: -
访问 http://localhost:3016/pdfjs-dist/web/viewer.html应该能看到查看器界面 -
如果不存在,代码会提示配置步骤
总结
选择建议
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
核心要点
-
✅ Worker 配置:使用本地 worker 文件 -
✅ 文件验证:始终验证 PDF 文件格式 -
✅ 错误处理:提供详细的错误信息 -
✅ 性能优化:使用懒加载和分批渲染 -
✅ 内存管理:及时清理资源 -
✅ 生产环境:推荐使用官方组件化方式
参考资源
-
PDF.js 官方文档[2] -
PDF.js GitHub[3] -
PDF.js API 文档[4]
引用链接
[1]https://github.com/mozilla/pdf.js/releases
[2]PDF.js 官方文档: https://mozilla.github.io/pdf.js/
[3]PDF.js GitHub: https://github.com/mozilla/pdf.js
[4]PDF.js API 文档: https://mozilla.github.io/pdf.js/api/
夜雨聆风
