环境准备
1. 安装依赖
bash
npm install pdfjs-dist
2. 配置 Worker
PDF.js 需要 Worker 来处理 PDF 解析。推荐使用本地 Worker 文件:
步骤 1:复制 Worker 文件到 public 目录
bash
# 从 node_modules 复制 worker 文件到 public 目录
copy node_modules\pdfjs-dist\build\pdf.worker.min.mjs public\pdf.worker.min.mjs
步骤 2:在代码中配置 Worker
typescript
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 中添加:
typescript
export default defineConfig({
// ... 其他配置
build: {
target: 'es2015',
rollupOptions: {
output: {
format: 'es'
}
}
},
optimizeDeps: {
include: ['pdfjs-dist', 'pdfjs-dist/web/pdf_viewer.mjs']
}
})
方式一:单页查看器(基础版)
特点
- ✅ 单页显示,分页导航
- ✅ 支持缩放功能
- ✅ 代码简单,易于理解
- ❌ 不支持文本选择和复制
- ❌ 一次只显示一页
核心代码
html
<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'
// 设置 worker
pdfjsLib.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)
// 加载 PDF
const 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 实现懒加载
核心代码
html
<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 官方推荐的生产环境使用方式
核心代码
html
<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'
// 设置 worker
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs'
let pdfViewer: PDFViewer | null = null
let eventBus: EventBus | null = null
const 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 必须是绝对定位:
typescript
scrollContainer.style.position = 'absolute'
- 需要两个容器:
-
container: 外层滚动容器(绝对定位)viewer: 内层查看器容器(.pdfViewer)
- 导入样式:
typescript
import 'pdfjs-dist/web/pdf_viewer.css'
演示

适用场景
- ⭐ 生产环境(最推荐)
- 需要完整功能(文本选择、注释、链接)
- 需要官方维护和更新
- 需要自定义样式和行为
方式四:iframe 嵌入官方 HTML
特点
- ✅ 直接使用官方 HTML 文件
- ✅ 功能完整,无需额外开发
- ❌ CDN 版本无法访问 Blob URL(跨域限制)
- ❌ 样式隔离,难以自定义
- ❌ 无法深度集成到 Vue 应用
- ⚠️ 不推荐用于生产环境
核心代码
html
<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
}
}
// 加载 PDF
const 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 官方查看器:
-
- 访问 github.com/mozilla/pdf...
- 下载最新版本的预构建包(例如:
pdfjs-5.4.449-dist.zip) - 解压后找到
web目录
- 复制到项目:
bash
# 将 web 目录复制到 public/pdfjs-dist/web/
# 最终结构应该是:
public/
└── pdfjs-dist/
├── build/
│ └── pdf.worker.min.mjs
└── web/
├── viewer.html
├── viewer.js
├── viewer.css
└── ... (其他文件)
- 验证配置:
-
- 启动开发服务器后,访问
http://localhost:xxxx/pdfjs-dist/web/viewer.html应该能看到 PDF.js 官方查看器界面 - 代码会自动检测本地查看器是否存在,如果不存在会提示配置步骤
- 启动开发服务器后,访问
演示

适用场景
- 快速原型
- 演示和测试
- 不需要深度集成
- ⚠️ 不推荐用于生产环境
最佳实践
1. Worker 配置
推荐:使用本地 Worker 文件
typescript
// ✅ 推荐方式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 文件:
typescript
// 验证文件类型
if (file.type !== 'application/pdf' && !file.name.endsWith('.pdf')) {
throw new Error('请选择 PDF 格式的文件')
}
// 验证 PDF 文件头
const header = String.fromCharCode(...uint8Array.slice(0, 4))
if (header !== '%PDF') {
throw new Error('无效的 PDF 文件格式')
}
3. 错误处理
提供详细的错误信息:
typescript
try {
await loadPdf(data)
} catch (error: any) {
let errorMessage = 'PDF 加载失败'
if (error?.name === 'InvalidPDFException') {
errorMessage = '无效的 PDF 文件结构,请确认文件是否损坏'
} else if (error?.name === 'MissingPDFException') {
errorMessage = 'PDF 文件缺失或无法访问'
}
$message?.error(errorMessage)
}
4. 性能优化
- 懒加载:使用 Intersection Observer 只渲染可见页面
- 分批渲染:缩放时按批次重新渲染
- 内存管理:及时清理 Blob URL 和查看器实例
typescript
// 清理 Blob URL
onUnmounted(() => {
if (pdfUrl.value) {
URL.revokeObjectURL(pdfUrl.value)
}
if (pdfViewer) {
pdfViewer.cleanup()
}
})
5. 文本选择支持
如果需要文本选择,必须添加文本层:
typescript
// 获取文本内容
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/ - 代码会自动检测本地查看器是否存在
- 路径配置:
typescript
// ✅ 正确:使用本地查看器
const viewerPath = '/pdfjs-dist/web/viewer.html'
// ❌ 错误:CDN 版本无法访问 Blob URL
const viewerPath = 'https://mozilla.github.io/pdf.js/web/viewer.html'
- 验证配置:
-
-
访问
http://localhost:3016/pdfjs-dist/web/viewer.html应该能看到查看器界面 -
如果不存在,代码会提示配置步骤
-
总结
选择建议
| 场景 | 推荐方式 |
|---|---|
| 生产环境 | 方式三(官方组件化) |
| 需要查看所有页面 | 方式二(多页查看器) |
| 简单预览 | 方式一(单页查看器) |
| 快速原型 | 方式四(iframe) |
核心要点
- ✅ Worker 配置:使用本地 worker 文件
- ✅ 文件验证:始终验证 PDF 文件格式
- ✅ 错误处理:提供详细的错误信息
- ✅ 性能优化:使用懒加载和分批渲染
- ✅ 内存管理:及时清理资源
- ✅ 生产环境:推荐使用官方组件化方式