邵阳市文章资讯

Vue3+TypeScript项目中安装PDF.js详细的步骤

2026-03-29 13:13:01 浏览次数:0
详细信息

一、安装依赖

1. 安装 PDF.js 核心库

npm install pdfjs-dist
# 或者使用 yarn
yarn add pdfjs-dist
# 或者使用 pnpm
pnpm add pdfjs-dist

2. 安装类型声明文件(TypeScript 需要)

npm install --save-dev @types/pdfjs-dist
# 或者
npm install @types/pdfjs-dist

二、基础配置

1. 创建 PDF 查看器组件

<!-- src/components/PdfViewer.vue -->
<template>
  <div class="pdf-viewer">
    <div class="controls">
      <button @click="prevPage" :disabled="currentPage <= 1">上一页</button>
      <span>第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</span>
      <button @click="nextPage" :disabled="currentPage >= totalPages">下一页</button>
      <input 
        type="number" 
        v-model.number="pageInput" 
        @change="goToPage"
        min="1"
        :max="totalPages"
      >
      <button @click="zoomIn">放大</button>
      <button @click="zoomOut">缩小</button>
      <span>缩放: {{ scale.toFixed(2) }}</span>
    </div>

    <div class="canvas-container" ref="containerRef">
      <canvas ref="canvasRef"></canvas>
    </div>

    <div v-if="loading" class="loading">加载中...</div>
    <div v-if="error" class="error">{{ error }}</div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'
import type { PDFDocumentProxy } from 'pdfjs-dist/types/src/pdf'

// 设置 worker
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
  'pdfjs-dist/build/pdf.worker.min.mjs',
  import.meta.url
).toString()

interface Props {
  url: string
}

const props = defineProps<Props>()

// Refs
const canvasRef = ref<HTMLCanvasElement | null>(null)
const containerRef = ref<HTMLElement | null>(null)
const pdfDoc = ref<PDFDocumentProxy | null>(null)
const currentPage = ref(1)
const totalPages = ref(0)
const scale = ref(1.5)
const loading = ref(false)
const error = ref<string | null>(null)
const pageInput = ref(1)

// 渲染页面
const renderPage = async (pageNum: number) => {
  if (!pdfDoc.value || !canvasRef.value) return

  try {
    loading.value = true

    const page = await pdfDoc.value.getPage(pageNum)
    const viewport = page.getViewport({ scale: scale.value })

    const canvas = canvasRef.value
    const context = canvas.getContext('2d')

    if (!context) return

    // 设置 Canvas 尺寸
    canvas.height = viewport.height
    canvas.width = viewport.width

    // 渲染 PDF 页面
    const renderContext = {
      canvasContext: context,
      viewport: viewport
    }

    await page.render(renderContext).promise

    currentPage.value = pageNum
    pageInput.value = pageNum
    loading.value = false
  } catch (err) {
    error.value = `渲染页面失败: ${err}`
    loading.value = false
  }
}

// 加载 PDF 文档
const loadPDF = async () => {
  try {
    loading.value = true
    error.value = null

    const loadingTask = pdfjsLib.getDocument(props.url)
    pdfDoc.value = await loadingTask.promise
    totalPages.value = pdfDoc.value.numPages

    await renderPage(1)
  } catch (err) {
    error.value = `加载PDF失败: ${err}`
    loading.value = false
  }
}

// 页面导航
const prevPage = () => {
  if (currentPage.value > 1) {
    renderPage(currentPage.value - 1)
  }
}

const nextPage = () => {
  if (pdfDoc.value && currentPage.value < pdfDoc.value.numPages) {
    renderPage(currentPage.value + 1)
  }
}

const goToPage = () => {
  const pageNum = Math.max(1, Math.min(pageInput.value, totalPages.value))
  renderPage(pageNum)
}

// 缩放功能
const zoomIn = () => {
  scale.value += 0.25
  renderPage(currentPage.value)
}

const zoomOut = () => {
  if (scale.value > 0.5) {
    scale.value -= 0.25
    renderPage(currentPage.value)
  }
}

// 监听 URL 变化
watch(() => props.url, () => {
  loadPDF()
})

// 生命周期
onMounted(() => {
  loadPDF()
})

onUnmounted(() => {
  if (pdfDoc.value) {
    pdfDoc.value.destroy()
  }
})
</script>

<style scoped>
.pdf-viewer {
  max-width: 100%;
  margin: 0 auto;
}

.controls {
  display: flex;
  gap: 10px;
  align-items: center;
  margin-bottom: 20px;
  flex-wrap: wrap;
  padding: 10px;
  background: #f5f5f5;
  border-radius: 4px;
}

.canvas-container {
  overflow: auto;
  border: 1px solid #ddd;
  background: #fafafa;
  max-height: 80vh;
}

canvas {
  display: block;
  margin: 0 auto;
}

.loading, .error {
  padding: 20px;
  text-align: center;
  font-size: 16px;
}

.loading {
  color: #666;
}

.error {
  color: #f44336;
}

input[type="number"] {
  width: 60px;
  padding: 5px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

button {
  padding: 8px 16px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

button:hover:not(:disabled) {
  background: #0056b3;
}
</style>

2. 在父组件中使用

<!-- src/App.vue 或父组件 -->
<template>
  <div class="app">
    <h1>PDF 查看器</h1>

    <!-- 使用本地 PDF 文件 -->
    <PdfViewer :url="localPdfUrl" />

    <!-- 或者使用远程 URL -->
    <!-- <PdfViewer :url="'https://example.com/document.pdf'" /> -->
  </div>
</template>

<script setup lang="ts">
import PdfViewer from './components/PdfViewer.vue'
import { ref } from 'vue'

// 本地文件需要放在 public 目录或通过 import 引入
const localPdfUrl = ref('/sample.pdf') // public/sample.pdf
// 或者使用 import
// const localPdfUrl = ref(new URL('./assets/sample.pdf', import.meta.url).href)
</script>

三、高级功能扩展

1. 创建增强版 PDF 查看器(支持缩略图)

<!-- src/components/AdvancedPdfViewer.vue -->
<template>
  <div class="advanced-pdf-viewer">
    <div class="sidebar" v-if="showThumbnails">
      <div 
        v-for="page in thumbnails" 
        :key="page.pageNum"
        :class="['thumbnail', { active: page.pageNum === currentPage }]"
        @click="renderPage(page.pageNum)"
      >
        <canvas :ref="el => setThumbnailRef(el, page.pageNum)"></canvas>
        <div class="page-number">{{ page.pageNum }}</div>
      </div>
    </div>

    <div class="main-content">
      <PdfViewer 
        ref="pdfViewerRef"
        :url="url"
        @page-change="onPageChange"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import PdfViewer from './PdfViewer.vue'
import * as pdfjsLib from 'pdfjs-dist'

interface Props {
  url: string
  showThumbnails?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  showThumbnails: true
})

const pdfViewerRef = ref()
const thumbnails = ref<Array<{ pageNum: number }>>([])
const currentPage = ref(1)
const thumbnailCanvases = ref<Record<number, HTMLCanvasElement>>({})

const setThumbnailRef = (el: HTMLCanvasElement | null, pageNum: number) => {
  if (el) {
    thumbnailCanvases.value[pageNum] = el
  }
}

const onPageChange = (page: number) => {
  currentPage.value = page
}

// 生成缩略图
const generateThumbnails = async () => {
  try {
    const loadingTask = pdfjsLib.getDocument(props.url)
    const pdfDoc = await loadingTask.promise

    thumbnails.value = Array.from(
      { length: pdfDoc.numPages },
      (_, i) => ({ pageNum: i + 1 })
    )

    // 批量渲染缩略图
    await Promise.all(
      thumbnails.value.map(async (thumbnail) => {
        const page = await pdfDoc.getPage(thumbnail.pageNum)
        const viewport = page.getViewport({ scale: 0.2 })
        const canvas = thumbnailCanvases.value[thumbnail.pageNum]

        if (canvas) {
          const context = canvas.getContext('2d')
          if (context) {
            canvas.height = viewport.height
            canvas.width = viewport.width

            await page.render({
              canvasContext: context,
              viewport: viewport
            }).promise
          }
        }
      })
    )
  } catch (error) {
    console.error('生成缩略图失败:', error)
  }
}

onMounted(() => {
  if (props.showThumbnails) {
    generateThumbnails()
  }
})
</script>

<style scoped>
.advanced-pdf-viewer {
  display: flex;
  height: 100vh;
}

.sidebar {
  width: 200px;
  overflow-y: auto;
  border-right: 1px solid #ddd;
  padding: 10px;
  background: #f8f9fa;
}

.main-content {
  flex: 1;
  overflow: auto;
}

.thumbnail {
  margin-bottom: 10px;
  cursor: pointer;
  border: 2px solid transparent;
  border-radius: 4px;
  overflow: hidden;
  transition: all 0.3s;
}

.thumbnail:hover {
  border-color: #007bff;
}

.thumbnail.active {
  border-color: #007bff;
  box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
}

.thumbnail canvas {
  width: 100%;
  height: auto;
  display: block;
}

.page-number {
  text-align: center;
  padding: 5px;
  background: rgba(0, 0, 0, 0.1);
  font-size: 12px;
}
</style>

2. TypeScript 类型定义扩展

// src/types/pdf.d.ts
declare module 'pdfjs-dist' {
  export interface PDFPageProxy {
    getViewport(params: { scale: number; rotation?: number }): any
    render(params: any): any
  }

  export interface PDFDocumentProxy {
    numPages: number
    getPage(pageNumber: number): Promise<PDFPageProxy>
    destroy(): void
  }

  export const GlobalWorkerOptions: {
    workerSrc: string
  }

  export function getDocument(src: string): {
    promise: Promise<PDFDocumentProxy>
  }
}

四、Vue 组合式 API 封装

// src/composables/usePdfViewer.ts
import { ref, computed } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'

export function usePdfViewer() {
  const pdfDocument = ref<pdfjsLib.PDFDocumentProxy | null>(null)
  const currentPage = ref(1)
  const totalPages = ref(0)
  const scale = ref(1.5)
  const isLoading = ref(false)
  const error = ref<string | null>(null)

  // 加载 PDF
  const loadPdf = async (url: string) => {
    try {
      isLoading.value = true
      error.value = null

      const loadingTask = pdfjsLib.getDocument(url)
      pdfDocument.value = await loadingTask.promise
      totalPages.value = pdfDocument.value.numPages

      return pdfDocument.value
    } catch (err) {
      error.value = `加载 PDF 失败: ${err}`
      throw err
    } finally {
      isLoading.value = false
    }
  }

  // 渲染页面到 Canvas
  const renderPageToCanvas = async (
    pageNum: number,
    canvas: HTMLCanvasElement,
    options?: {
      scale?: number
      rotation?: number
    }
  ) => {
    if (!pdfDocument.value) {
      throw new Error('PDF 文档未加载')
    }

    try {
      isLoading.value = true

      const page = await pdfDocument.value.getPage(pageNum)
      const viewport = page.getViewport({
        scale: options?.scale || scale.value,
        rotation: options?.rotation || 0
      })

      const context = canvas.getContext('2d')
      if (!context) {
        throw new Error('无法获取 Canvas 上下文')
      }

      // 设置 Canvas 尺寸
      canvas.height = viewport.height
      canvas.width = viewport.width

      // 渲染
      await page.render({
        canvasContext: context,
        viewport
      }).promise

      currentPage.value = pageNum
    } catch (err) {
      error.value = `渲染页面失败: ${err}`
      throw err
    } finally {
      isLoading.value = false
    }
  }

  // 页面导航
  const goToPage = (pageNum: number) => {
    const page = Math.max(1, Math.min(pageNum, totalPages.value))
    currentPage.value = page
    return page
  }

  const nextPage = () => goToPage(currentPage.value + 1)
  const prevPage = () => goToPage(currentPage.value - 1)

  // 缩放
  const zoom = (factor: number) => {
    scale.value = Math.max(0.25, Math.min(5, scale.value + factor))
    return scale.value
  }

  const zoomIn = () => zoom(0.25)
  const zoomOut = () => zoom(-0.25)

  // 清理
  const destroy = () => {
    if (pdfDocument.value) {
      pdfDocument.value.destroy()
      pdfDocument.value = null
    }
  }

  return {
    // 状态
    pdfDocument,
    currentPage,
    totalPages,
    scale,
    isLoading,
    error,

    // 计算属性
    hasNextPage: computed(() => currentPage.value < totalPages.value),
    hasPrevPage: computed(() => currentPage.value > 1),

    // 方法
    loadPdf,
    renderPageToCanvas,
    goToPage,
    nextPage,
    prevPage,
    zoomIn,
    zoomOut,
    destroy
  }
}

五、配置 PDF.js Worker(重要)

1. 推荐配置(Vite 项目)

// vite.config.ts 或 vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  optimizeDeps: {
    include: ['pdfjs-dist']
  }
})

2. 如果是 Webpack 项目

// vue.config.js
module.exports = {
  configureWebpack: {
    resolve: {
      alias: {
        'pdfjs-dist/build/pdf.worker': 'pdfjs-dist/build/pdf.worker.min.js'
      }
    }
  }
}

六、使用示例

<!-- src/views/PdfDemo.vue -->
<template>
  <div class="pdf-demo">
    <div class="toolbar">
      <input type="file" @change="handleFileUpload" accept=".pdf" />
      <button @click="loadSamplePdf">加载示例PDF</button>
    </div>

    <div v-if="pdfUrl">
      <AdvancedPdfViewer :url="pdfUrl" />
    </div>

    <div v-else class="placeholder">
      请选择或上传 PDF 文件
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import AdvancedPdfViewer from '@/components/AdvancedPdfViewer.vue'

const pdfUrl = ref<string>('')

// 处理文件上传
const handleFileUpload = (event: Event) => {
  const input = event.target as HTMLInputElement
  if (input.files && input.files[0]) {
    const file = input.files[0]
    pdfUrl.value = URL.createObjectURL(file)
  }
}

// 加载示例 PDF
const loadSamplePdf = () => {
  // 可以从 public 目录或远程 URL 加载
  pdfUrl.value = '/sample.pdf' // public/sample.pdf
  // 或者
  // pdfUrl.value = 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf'
}
</script>

七、常见问题解决

1. Worker 加载问题

// 在 main.ts 或组件中设置
import { GlobalWorkerOptions } from 'pdfjs-dist'

// Vite 项目
GlobalWorkerOptions.workerSrc = new URL(
  'pdfjs-dist/build/pdf.worker.min.mjs',
  import.meta.url
).toString()

// Webpack 项目
GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`
// 或
GlobalWorkerOptions.workerSrc = require('pdfjs-dist/build/pdf.worker.min.js')

2. TypeScript 类型错误

确保安装了正确的类型声明:

// package.json
{
  "devDependencies": {
    "@types/pdfjs-dist": "^2.x.x"
  }
}

3. 性能优化

这样就完成了完整的 PDF.js 在 Vue3 + TypeScript 项目中的集成。你可以根据具体需求调整和扩展功能。

相关推荐