乐于分享
好东西不私藏

PDF.js 在 Vue 中的使用指南

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>

性能优化要点

  1. 懒加载:使用 Intersection Observer 只渲染可见页面
  2. 分批渲染:缩放时按批次重新渲染,避免阻塞 UI
  3. 状态管理:跟踪已渲染页面,避免重复渲染

演示

适用场景

  • 需要查看所有页面
  • 大文档(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>

重要配置

  1. Container 必须是绝对定位
scrollContainer.style.position = 'absolute'
  1. 需要两个容器
    • container: 外层滚动容器(绝对定位)
    • viewer: 内层查看器容器(.pdfViewer
  2. 导入样式
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(跨域限制)**

  1. 下载 PDF.js 官方查看器:
    • 访问 https://github.com/mozilla/pdf.js/releases[1]
    • 下载最新版本的预构建包(例如:pdfjs-5.4.449-dist.zip
    • 解压后找到 web 目录
  2. 复制到项目:
# 将 web 目录复制到 public/pdfjs-dist/web/# 最终结构应该是:public/  └── pdfjs-dist/      ├── build/      │   └── pdf.worker.min.mjs      └── web/          ├── viewer.html          ├── viewer.js          ├── viewer.css          └── ... (其他文件)
  1. 验证配置:
    • 启动开发服务器后,访问 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(04))if (header !== '%PDF') {thrownewError('无效的 PDF 文件格式')}

3. 错误处理

提供详细的错误信息:

try {awaitloadPdf(data)catch (errorany) {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((itemany) => {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

解决方案

  1. 确保 worker 文件在 public 目录
  2. 使用本地 worker 文件而不是 CDN
  3. 检查路径是否正确

Q2: PDF 文件无法加载

错误Invalid PDF structure

解决方案

  1. 验证文件是否为有效的 PDF 格式
  2. 检查文件是否损坏
  3. 添加文件头验证

Q3: 打包后无法运行

错误:CORS 错误

解决方案

  1. 不要直接用 file:// 打开打包后的文件
  2. 使用 npm run preview 预览
  3. 或使用本地服务器(http-server、Python 等)

Q4: PDFViewer 初始化失败

错误Invalid container and/or viewer option

解决方案

  1. 确保 container 是绝对定位
  2. 提供 container 和 viewer 两个参数
  3. 等待 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 中访问

解决方案

  1. 必须使用本地查看器(推荐)
    • 下载 PDF.js 官方查看器
    • 将 web 目录复制到 public/pdfjs-dist/web/
    • 代码会自动检测本地查看器是否存在
  2. 路径配置
// ✅ 正确:使用本地查看器const viewerPath = '/pdfjs-dist/web/viewer.html'// ❌ 错误:CDN 版本无法访问 Blob URLconst viewerPath = 'https://mozilla.github.io/pdf.js/web/viewer.html'
  1. 验证配置
    • 访问 http://localhost:3016/pdfjs-dist/web/viewer.html 应该能看到查看器界面
    • 如果不存在,代码会提示配置步骤

总结

选择建议

场景
推荐方式
生产环境
方式三(官方组件化)
需要查看所有页面
方式二(多页查看器)
简单预览
方式一(单页查看器)
快速原型
方式四(iframe)

核心要点

  1. ✅ Worker 配置:使用本地 worker 文件
  2. ✅ 文件验证:始终验证 PDF 文件格式
  3. ✅ 错误处理:提供详细的错误信息
  4. ✅ 性能优化:使用懒加载和分批渲染
  5. ✅ 内存管理:及时清理资源
  6. ✅ 生产环境:推荐使用官方组件化方式

参考资源

  • 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/

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » PDF.js 在 Vue 中的使用指南

评论 抢沙发

3 + 7 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮