React + PDF.js 預覽 PDF 文件:從基礎實現到高級優化的完整指南

關鍵點

  • PDF.js:Mozilla 開發的開源 JavaScript 庫,用于在瀏覽器中渲染 PDF 文件。
  • React 集成:結合 React 組件化特性,實現高效、交互式的 PDF 預覽功能。
  • 功能實現:支持 PDF 文件加載、頁面導航、縮放、搜索、書簽和注釋。
  • 優化策略:包括性能優化(懶加載、緩存)、可訪問性和手機端適配。
  • 常見問題:處理大文件加載、跨瀏覽器兼容性和內存管理。
  • 實踐場景:通過一個文檔管理應用,展示 PDF.js 在 React 中的完整實現。

引言

PDF 文件是現代 Web 應用中常見的文檔格式,廣泛用于展示報告、合同、書籍等內容。在前端開發中,預覽 PDF 文件需要高效的渲染能力和良好的用戶體驗。PDF.js 是 Mozilla 開發的強大開源庫,能夠在瀏覽器中直接渲染 PDF 文件,無需依賴原生插件。結合 React 的組件化特性,開發者可以構建交互式、響應式的 PDF 預覽功能,滿足多樣化的業務需求。

然而,PDF.js 的集成和優化并非易事。大文件加載可能導致性能瓶頸,跨瀏覽器兼容性問題可能影響渲染效果,復雜的交互功能(如搜索、書簽)需要額外的開發工作。本文通過構建一個基于 React 和 PDF.js 的文檔管理應用,深入探討 PDF 預覽的實現流程,從基礎渲染到高級功能(如縮放、搜索、注釋),并提供性能優化、可訪問性和手機端適配的實踐方案。通過詳細的代碼示例和場景分析,開發者將掌握如何在 React 中高效使用 PDF.js。


在現代 Web 應用中,PDF 文件預覽是一項常見需求,涵蓋文檔管理、在線閱讀和電子合同等場景。PDF.js 是一個功能強大的 JavaScript 庫,能夠在瀏覽器中直接解析和渲染 PDF 文件,無需依賴原生插件或服務器端處理。結合 React 的組件化開發模式,開發者可以構建高效、交互式的 PDF 預覽功能,支持頁面導航、縮放、搜索、書簽和注釋等特性。

盡管 PDF.js 提供了強大的渲染能力,其在 React 項目中的集成仍面臨諸多挑戰。例如,大型 PDF 文件可能導致加載緩慢,復雜的交互功能需要精細的狀態管理,跨瀏覽器兼容性和可訪問性問題也需特別關注。本文通過一個基于 React 的文檔管理應用,全面探討 PDF.js 的集成、功能實現和優化實踐。我們將從基礎渲染開始,逐步實現高級功能(如動態縮放、文本搜索、書簽導航),并提供性能優化、可訪問性和手機端適配的解決方案。

通過本項目,您將學習到:

  • PDF.js 基礎:加載和渲染 PDF 文件,處理多頁文檔。
  • React 集成:使用組件化方式管理 PDF 渲染和交互。
  • 高級功能:實現頁面導航、縮放、搜索、書簽和注釋。
  • 性能優化:通過懶加載、緩存和分片渲染提升效率。
  • 可訪問性:為 PDF 內容添加 ARIA 屬性,支持屏幕閱讀器。
  • 手機端適配:優化響應式布局和觸控交互。
  • 部署:將應用部署到 Vercel,支持高可用性。

本文面向有經驗的開發者,假設您熟悉 HTML、CSS、JavaScript、React 和 TypeScript 基礎知識。內容詳實且實用,適合深入學習 PDF.js 和 React 的集成。


需求分析

在動手編碼之前,我們需要明確文檔管理應用的功能需求。一個清晰的需求清單能指導開發過程并幫助我們優化 PDF 預覽功能。以下是項目的核心需求:

  1. PDF 文件加載與渲染
    • 支持上傳或通過 URL 加載 PDF 文件。
    • 渲染單頁或多頁 PDF,支持動態分頁。
  2. 頁面導航
    • 提供上一頁、下一頁和跳轉到指定頁的功能。
    • 顯示當前頁碼和總頁數。
  3. 縮放功能
    • 支持放大、縮小和自適應縮放。
    • 確保縮放后圖像和文本清晰。
  4. 文本搜索
    • 支持在 PDF 中搜索關鍵詞,高亮匹配結果。
    • 提供搜索結果導航(上一個、下一個)。
  5. 書簽與大綱
    • 解析 PDF 的書簽(大綱)并展示導航菜單。
    • 支持點擊書簽跳轉到對應頁面。
  6. 注釋功能
    • 支持添加文本注釋和高亮標記。
    • 保存和加載注釋數據。
  7. 性能優化
    • 實現懶加載,僅渲染可見頁面。
    • 使用緩存減少重復渲染。
    • 優化大型 PDF 文件的加載速度。
  8. 可訪問性(a11y)
    • 為 PDF 內容添加 ARIA 屬性。
    • 支持鍵盤導航和屏幕閱讀器。
  9. 手機端適配
    • 響應式布局,適配不同屏幕尺寸。
    • 優化觸控交互(如縮放、滑動)。
  10. 部署
    • 集成到 Vite 項目,部署到 Vercel。
    • 支持 CDN 加速靜態資源加載。

需求背后的意義

這些需求覆蓋了 PDF 預覽的核心場景,同時為學習 PDF.js 和 React 的集成提供了實踐機會:

  • PDF 渲染:實現基礎文檔預覽功能。
  • 交互功能:提升用戶體驗,滿足復雜業務需求。
  • 性能優化:確保大型 PDF 文件的快速加載和渲染。
  • 可訪問性:滿足無障礙標準,擴大用戶覆蓋。
  • 手機端適配:適配移動設備,提升用戶體驗。

技術棧選擇

在實現文檔管理應用之前,我們需要選擇合適的技術棧。以下是本項目使用的工具和技術,以及選擇它們的理由:

  • React 18
    核心前端框架,支持組件化開發和并發渲染,適合動態應用。
  • PDF.js
    Mozilla 的開源 PDF 渲染庫,支持瀏覽器內渲染和交互功能。
  • TypeScript
    提供類型安全,增強代碼可維護性和 IDE 補全,適合復雜項目。
  • Vite
    構建工具,提供快速的開發服務器和高效的打包能力。
  • React Query
    數據獲取和狀態管理庫,簡化異步 PDF 文件加載。
  • Framer Motion
    用于實現動畫效果(如頁面切換、縮放動畫)。
  • Tailwind CSS
    提供靈活的樣式解決方案,支持響應式設計。
  • Vercel
    用于部署應用,提供高可用性和全球 CDN 支持。

技術棧優勢

  • React 18:支持并發渲染,優化復雜應用性能。
  • PDF.js:輕量高效,支持豐富的 PDF 功能。
  • TypeScript:提升代碼質量,減少運行時錯誤。
  • Vite:啟動速度快,熱更新體驗優越。
  • React Query:簡化異步數據管理,優化 PDF 加載。
  • Framer Motion:實現流暢的動畫效果。
  • Tailwind CSS:簡化樣式開發,支持響應式設計。
  • Vercel:與 React 生態深度集成,部署簡單。

這些工具組合不僅易于上手,還能幫助開發者掌握 PDF.js 和 React 的最佳實踐。


項目實現

現在進入核心部分——代碼實現。我們將從項目搭建開始,逐步實現 PDF 文件加載、渲染、交互功能、性能優化、可訪問性和部署。

1. 項目搭建

使用 Vite 創建一個 React + TypeScript 項目:

npm create vite@latest pdf-viewer -- --template react-ts
cd pdf-viewer
npm install
npm run dev

安裝必要的依賴:

npm install pdfjs-dist @tanstack/react-query framer-motion tailwindcss postcss autoprefixer

初始化 Tailwind CSS:

npx tailwindcss init -p

編輯 tailwind.config.js

/** @type {import('tailwindcss').Config} */
export default {content: ["./index.html","./src/**/*.{js,ts,jsx,tsx}",],theme: {extend: {},},plugins: [],
}

src/index.css 中引入 Tailwind:

@tailwind base;
@tailwind components;
@tailwind utilities;

配置 PDF.js Worker:

src/pdf.worker.ts

import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';

2. 組件拆分

我們將應用拆分為以下組件:

  • App:根組件,負責整體布局。
  • PDFViewer:核心 PDF 渲染組件,管理頁面渲染和導航。
  • PDFControls:處理導航、縮放和搜索功能。
  • PDFOutline:展示書簽和大綱,支持跳轉。
  • PDFAnnotations:管理注釋功能。
  • AccessibilityPanel:管理可訪問性設置。
文件結構
src/
├── components/
│   ├── PDFViewer.tsx
│   ├── PDFControls.tsx
│   ├── PDFOutline.tsx
│   ├── PDFAnnotations.tsx
│   └── AccessibilityPanel.tsx
├── hooks/
│   └── usePDF.ts
├── types/
│   └── index.ts
├── assets/
│   └── sample.pdf
├── pdf.worker.ts
├── App.tsx
├── main.tsx
└── index.css

3. PDF 文件加載與渲染

3.1 基礎渲染

src/hooks/usePDF.ts

import { useState, useCallback } from 'react';
import * as pdfjsLib from 'pdfjs-dist';
import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';interface PDFState {document: PDFDocumentProxy | null;currentPage: number;totalPages: number;scale: number;
}export function usePDF(url: string) {const [state, setState] = useState<PDFState>({document: null,currentPage: 1,totalPages: 0,scale: 1,});const loadPDF = useCallback(async () => {try {const pdf = await pdfjsLib.getDocument(url).promise;setState(prev => ({...prev,document: pdf,totalPages: pdf.numPages,}));} catch (error) {console.error('PDF 加載失敗:', error);}}, [url]);const renderPage = useCallback(async (pageNum: number, canvas: HTMLCanvasElement) => {if (!state.document) return;const page = await state.document.getPage(pageNum);const viewport = page.getViewport({ scale: state.scale });const context = canvas.getContext('2d');if (!context) return;canvas.height = viewport.height;canvas.width = viewport.width;await page.render({canvasContext: context,viewport,}).promise;},[state.document, state.scale]);return { state, loadPDF, renderPage, setState };
}

src/components/PDFViewer.tsx

import { useEffect, useRef } from 'react';
import { usePDF } from '../hooks/usePDF';
import PDFControls from './PDFControls';function PDFViewer({ url }: { url: string }) {const canvasRef = useRef<HTMLCanvasElement>(null);const { state, loadPDF, renderPage } = usePDF(url);useEffect(() => {loadPDF();}, [loadPDF]);useEffect(() => {if (canvasRef.current && state.document) {renderPage(state.currentPage, canvasRef.current);}}, [state.currentPage, state.scale, state.document, renderPage]);return (<div className="p-4 bg-white rounded-lg shadow"><h2 className="text-xl font-bold mb-4">PDF 預覽</h2><PDFControlscurrentPage={state.currentPage}totalPages={state.totalPages}scale={state.scale}setState={state.setState}/><canvas ref={canvasRef} className="w-full" aria-label={`PDF 第 ${state.currentPage}`} /></div>);
}export default PDFViewer;

實現過程

  • 使用 pdfjsLib.getDocument 加載 PDF 文件。
  • 渲染指定頁面到 <canvas> 元素。
  • 支持動態頁面切換和縮放。

避坑

  • 確保 workerSrc 配置正確,防止 Worker 加載失敗。
  • 處理 PDF 加載錯誤,提供用戶反饋。
3.2 頁面導航

src/components/PDFControls.tsx

import { useCallback } from 'react';
import type { PDFState } from '../hooks/usePDF';interface PDFControlsProps {currentPage: number;totalPages: number;scale: number;setState: React.Dispatch<React.SetStateAction<PDFState>>;
}function PDFControls({ currentPage, totalPages, scale, setState }: PDFControlsProps) {const prevPage = useCallback(() => {setState(prev => ({...prev,currentPage: Math.max(1, prev.currentPage - 1),}));}, [setState]);const nextPage = useCallback(() => {setState(prev => ({...prev,currentPage: Math.min(prev.totalPages, prev.currentPage + 1),}));}, [setState]);const setScale = useCallback((newScale: number) => {setState(prev => ({ ...prev, scale: newScale }));},[setState]);return (<div className="flex items-center space-x-4 mb-4"><buttononClick={prevPage}disabled={currentPage === 1}className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"aria-label="上一頁">上一頁</button><span>{currentPage}/{totalPages}</span><buttononClick={nextPage}disabled={currentPage === totalPages}className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"aria-label="下一頁">下一頁</button><selectvalue={scale}onChange={e => setScale(Number(e.target.value))}className="p-2 border rounded-lg"aria-label="縮放比例"><option value={0.5}>50%</option><option value={1}>100%</option><option value={1.5}>150%</option><option value={2}>200%</option></select></div>);
}export default PDFControls;

避坑

  • 確保頁面范圍檢查,防止越界。
  • 添加 disabled 屬性,優化用戶體驗。

4. 高級功能

4.1 文本搜索

src/hooks/usePDF.ts(更新):

import { useState, useCallback } from 'react';
import * as pdfjsLib from 'pdfjs-dist';
import type { PDFDocumentProxy, PDFPageProxy, TextItem } from 'pdfjs-dist';interface SearchResult {page: number;index: number;text: string;
}interface PDFState {document: PDFDocumentProxy | null;currentPage: number;totalPages: number;scale: number;searchResults: SearchResult[];currentSearchIndex: number;
}export function usePDF(url: string) {const [state, setState] = useState<PDFState>({document: null,currentPage: 1,totalPages: 0,scale: 1,searchResults: [],currentSearchIndex: -1,});const searchText = useCallback(async (query: string) => {if (!state.document || !query) return;const results: SearchResult[] = [];for (let pageNum = 1; pageNum <= state.totalPages; pageNum++) {const page = await state.document.getPage(pageNum);const textContent = await page.getTextContent();textContent.items.forEach((item: TextItem, index) => {if ('str' in item && item.str.toLowerCase().includes(query.toLowerCase())) {results.push({ page: pageNum, index, text: item.str });}});}setState(prev => ({...prev,searchResults: results,currentSearchIndex: results.length > 0 ? 0 : -1,currentPage: results.length > 0 ? results[0].page : prev.currentPage,}));},[state.document, state.totalPages]);const navigateSearch = useCallback((direction: 'next' | 'prev') => {setState(prev => {if (prev.searchResults.length === 0) return prev;const newIndex =direction === 'next'? (prev.currentSearchIndex + 1) % prev.searchResults.length: (prev.currentSearchIndex - 1 + prev.searchResults.length) % prev.searchResults.length;return {...prev,currentSearchIndex: newIndex,currentPage: prev.searchResults[newIndex].page,};});},[]);return { state, loadPDF, renderPage, searchText, navigateSearch, setState };
}

src/components/PDFControls.tsx(更新):

import { useState } from 'react';interface PDFControlsProps {currentPage: number;totalPages: number;scale: number;searchResults: SearchResult[];currentSearchIndex: number;setState: React.Dispatch<React.SetStateAction<PDFState>>;searchText: (query: string) => void;navigateSearch: (direction: 'next' | 'prev') => void;
}function PDFControls({currentPage,totalPages,scale,searchResults,currentSearchIndex,setState,searchText,navigateSearch,
}: PDFControlsProps) {const [query, setQuery] = useState('');return (<div className="flex flex-col space-y-4 mb-4"><div className="flex items-center space-x-4"><buttononClick={prevPage}disabled={currentPage === 1}className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"aria-label="上一頁">上一頁</button><span>{currentPage}/{totalPages}</span><buttononClick={nextPage}disabled={currentPage === totalPages}className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"aria-label="下一頁">下一頁</button><selectvalue={scale}onChange={e => setScale(Number(e.target.value))}className="p-2 border rounded-lg"aria-label="縮放比例"><option value={0.5}>50%</option><option value={1}>100%</option><option value={1.5}>150%</option><option value={2}>200%</option></select></div><div className="flex items-center space-x-4"><inputtype="text"value={query}onChange={e => setQuery(e.target.value)}className="p-2 border rounded-lg"placeholder="搜索文本"aria-label="搜索 PDF 內容"/><buttononClick={() => searchText(query)}className="px-4 py-2 bg-blue-500 text-white rounded-lg"aria-label="搜索">搜索</button><buttononClick={() => navigateSearch('prev')}disabled={searchResults.length === 0}className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"aria-label="上一個搜索結果">上一個</button><buttononClick={() => navigateSearch('next')}disabled={searchResults.length === 0}className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"aria-label="下一個搜索結果">下一個</button><span>{searchResults.length > 0 ? `結果 ${currentSearchIndex + 1}/${searchResults.length}` : '無結果'}</span></div></div>);
}

避坑

  • 確保搜索查詢大小寫不敏感。
  • 高亮搜索結果需額外 Canvas 渲染(可擴展)。
4.2 書簽與大綱

src/components/PDFOutline.tsx

import { useEffect, useState } from 'react';
import type { PDFDocumentProxy, OutlineNode } from 'pdfjs-dist';
import { usePDF } from '../hooks/usePDF';function PDFOutline({ url }: { url: string }) {const { state } = usePDF(url);const [outline, setOutline] = useState<OutlineNode[]>([]);useEffect(() => {if (state.document) {state.document.getOutline().then(setOutline);}}, [state.document]);const navigateTo = async (dest: string | any[]) => {if (!state.document) return;const ref = Array.isArray(dest) ? dest[0] : await state.document.getDestination(dest);const pageIndex = await state.document.getPageIndex(ref);state.setState(prev => ({ ...prev, currentPage: pageIndex + 1 }));};return (<div className="p-4 bg-white rounded-lg shadow"><h2 className="text-xl font-bold mb-4">書簽</h2><ul>{outline.map((item, index) => (<li key={index} className="p-2"><buttononClick={() => navigateTo(item.dest)}className="text-blue-500 hover:underline"aria-label={`跳轉到書簽 ${item.title}`}>{item.title}</button></li>))}</ul></div>);
}export default PDFOutline;

避坑

  • 檢查 PDF 是否包含書簽(getOutline 可能返回 null)。
  • 處理異步跳轉,確保頁面存在。
4.3 注釋功能

src/components/PDFAnnotations.tsx

import { useState, useCallback } from 'react';
import type { PDFState } from '../hooks/usePDF';interface Annotation {page: number;text: string;x: number;y: number;
}function PDFAnnotations({ state, canvasRef }: { state: PDFState; canvasRef: React.RefObject<HTMLCanvasElement> }) {const [annotations, setAnnotations] = useState<Annotation[]>([]);const addAnnotation = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {if (!canvasRef.current) return;const rect = canvasRef.current.getBoundingClientRect();const x = e.clientX - rect.left;const y = e.clientY - rect.top;const text = prompt('輸入注釋內容:');if (text) {setAnnotations(prev => [...prev, { page: state.currentPage, text, x, y }]);}},[state.currentPage, canvasRef]);return (<div className="relative"><canvas ref={canvasRef} className="w-full" onClick={addAnnotation} aria-label="PDF 頁面" />{annotations.filter(anno => anno.page === state.currentPage).map((anno, index) => (<divkey={index}className="absolute bg-yellow-200 p-2 rounded-lg"style={{ left: anno.x, top: anno.y }}role="tooltip">{anno.text}</div>))}</div>);
}

避坑

  • 保存注釋數據到本地存儲或服務器。
  • 確保注釋位置隨縮放調整。

5. 性能優化

5.1 懶加載頁面

src/components/PDFViewer.tsx(更新):

import { useEffect, useRef } from 'react';
import { useInView } from 'framer-motion';
import { usePDF } from '../hooks/usePDF';
import PDFControls from './PDFControls';function PDFViewer({ url }: { url: string }) {const canvasRef = useRef<HTMLCanvasElement>(null);const containerRef = useRef<HTMLDivElement>(null);const isInView = useInView(containerRef, { once: false });const { state, loadPDF, renderPage } = usePDF(url);useEffect(() => {if (isInView) loadPDF();}, [isInView, loadPDF]);useEffect(() => {if (canvasRef.current && state.document && isInView) {renderPage(state.currentPage, canvasRef.current);}}, [state.currentPage, state.scale, state.document, renderPage, isInView]);return (<div ref={containerRef} className="p-4 bg-white rounded-lg shadow"><h2 className="text-xl font-bold mb-4">PDF 預覽</h2><PDFControlscurrentPage={state.currentPage}totalPages={state.totalPages}scale={state.scale}searchResults={state.searchResults}currentSearchIndex={state.currentSearchIndex}setState={state.setState}searchText={state.searchText}navigateSearch={state.navigateSearch}/><PDFAnnotations state={state} canvasRef={canvasRef} /></div>);
}

優點

  • 使用 useInView 延遲加載非可見頁面。
  • 減少初次渲染的性能開銷。

避坑

  • 確保 IntersectionObserver 準確觸發。
  • 提供加載占位符,改善用戶體驗。
5.2 緩存頁面

src/hooks/usePDF.ts(更新):

import { useMemo } from 'react';export function usePDF(url: string) {const [state, setState] = useState<PDFState>({ ... });const pageCache = useMemo(() => new Map<number, HTMLCanvasElement>(), []);const renderPage = useCallback(async (pageNum: number, canvas: HTMLCanvasElement) => {if (pageCache.has(pageNum)) {const cachedCanvas = pageCache.get(pageNum)!;canvas.getContext('2d')?.drawImage(cachedCanvas, 0, 0);return;}if (!state.document) return;const page = await state.document.getPage(pageNum);const viewport = page.getViewport({ scale: state.scale });const context = canvas.getContext('2d');if (!context) return;canvas.height = viewport.height;canvas.width = viewport.width;await page.render({canvasContext: context,viewport,}).promise;pageCache.set(pageNum, canvas);},[state.document, state.scale, pageCache]);return { state, loadPDF, renderPage, searchText, navigateSearch, setState };
}

避坑

  • 限制緩存大小,防止內存溢出。
  • 清理過期緩存(如頁面關閉時)。

6. 可訪問性(a11y)

src/components/AccessibilityPanel.tsx

import { useState } from 'react';function AccessibilityPanel() {const [highContrast, setHighContrast] = useState(false);return (<div className="p-4 bg-white rounded-lg shadow"><h2 className="text-xl font-bold mb-4">可訪問性設置</h2><label className="flex items-center space-x-2"><inputtype="checkbox"checked={highContrast}onChange={() => setHighContrast(!highContrast)}className="p-2"aria-label="啟用高對比度模式"/><span>高對比度模式</span></label><div className={highContrast ? 'bg-black text-white' : ''}><p aria-live="polite">測試文本:{highContrast ? '高對比度' : '正常'}</p></div></div>);
}export default AccessibilityPanel;

避坑

  • 為 Canvas 添加 aria-label 描述頁面內容。
  • 測試屏幕閱讀器對動態內容的兼容性。

7. 手機端適配

src/components/PDFViewer.tsx(更新):

function PDFViewer({ url }: { url: string }) {return (<div ref={containerRef} className="p-2 md:p-4 bg-white rounded-lg shadow"><h2 className="text-lg md:text-xl font-bold mb-4">PDF 預覽</h2><PDFControlscurrentPage={state.currentPage}totalPages={state.totalPages}scale={state.scale}searchResults={state.searchResults}currentSearchIndex={state.currentSearchIndex}setState={state.setState}searchText={state.searchText}navigateSearch={state.navigateSearch}/><div className="overflow-x-auto"><PDFAnnotations state={state} canvasRef={canvasRef} /></div></div>);
}

避坑

  • 使用 overflow-x-auto 支持橫向滾動。
  • 確保觸控縮放與 PDF.js 縮放兼容。

8. 集成所有功能

src/App.tsx

import PDFViewer from './components/PDFViewer';
import PDFOutline from './components/PDFOutline';
import AccessibilityPanel from './components/AccessibilityPanel';function App() {return (<div className="min-h-screen bg-gray-100 p-2 md:p-4"><h1 className="text-2xl md:text-3xl font-bold text-center p-4">文檔管理器</h1><div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-5xl mx-auto"><PDFViewer url="/sample.pdf" /><PDFOutline url="/sample.pdf" /><AccessibilityPanel /></div></div>);
}export default App;

9. 部署

9.1 構建項目
npm run build
9.2 部署到 Vercel
  1. 注冊 Vercel:訪問 Vercel 官網并創建賬號。
  2. 新建項目:選擇“New Project”。
  3. 導入倉庫:將項目推送至 GitHub 并導入。
  4. 配置構建
    • 構建命令:npm run build
    • 輸出目錄:dist
  5. 部署:點擊“Deploy”.

避坑

  • 確保 PDF 文件路徑正確(使用相對路徑)。
  • 使用 CDN 加速 PDF.js Worker 和靜態資源。

常見問題與解決方案

10.1 大文件加載緩慢

問題:大型 PDF 文件導致加載時間長。

解決方案

  • 使用懶加載,僅渲染可見頁面。
  • 啟用 PDF.js 的范圍請求(Range Requests):
    pdfjsLib.getDocument({ url, rangeChunkSize: 65536 });
    

10.2 跨瀏覽器兼容性

問題:Safari 或 Edge 渲染異常。

解決方案

  • 確保 PDF.js 版本一致(推薦最新穩定版)。
  • 測試 Canvas 渲染兼容性。

10.3 內存管理

問題:多頁 PDF 導致內存占用高。

解決方案

  • 清理頁面緩存:
    pageCache.clear();
    
  • 銷毀 PDF 文檔:
    state.document?.destroy();
    

練習:添加 PDF 下載功能

為鞏固所學,設計一個練習:為 PDF 預覽器添加下載功能。

需求

  • 支持將當前 PDF 文件下載為 Blob。
  • 提供下載進度提示。
  • 集成到 PDFControls 組件。

實現步驟

src/components/PDFControls.tsx(更新):

import { useState } from 'react';function PDFControls({ ...props }: PDFControlsProps) {const [downloading, setDownloading] = useState(false);const downloadPDF = async () => {if (!props.state.document) return;setDownloading(true);const data = await props.state.document.getData();const blob = new Blob([data], { type: 'application/pdf' });const url = URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;link.download = 'document.pdf';link.click();URL.revokeObjectURL(url);setDownloading(false);};return (<div className="flex flex-col space-y-4 mb-4">{/* 其他控件 */}<buttononClick={downloadPDF}disabled={downloading}className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"aria-label="下載 PDF">{downloading ? '下載中...' : '下載 PDF'}</button></div>);
}

目標

  • 學會使用 PDF.js 的 getData 獲取 PDF 數據。
  • 優化下載體驗,提供進度反饋。

注意事項

  • PDF.js 配置:確保 Worker 和主庫版本匹配。
  • 性能優化:使用懶加載和緩存減少開銷。
  • 可訪問性:為 Canvas 和交互元素添加 ARIA 屬性。
  • 學習建議:參考 PDF.js 文檔、React 文檔 和 Vite 文檔.

結語

通過這個文檔管理應用,您深入了解了 React 和 PDF.js 的集成流程,掌握了 PDF 文件加載、渲染、交互功能和優化的關鍵技術。這些技能將幫助您構建高效、交互式的 PDF 預覽應用,滿足復雜業務需求。希望您繼續探索 PDF.js 的高級功能,如表單交互和數字簽名,打造卓越的用戶體驗!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/88057.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/88057.shtml
英文地址,請注明出處:http://en.pswp.cn/web/88057.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

新能源汽車BMS電感產品應用及選型推薦

在新能源電動汽車中&#xff0c;BMS&#xff08;電池管理系統&#xff09;如同一個守護者&#xff0c;默默守護電池的安全與性能。它精準監控電壓、電流、溫度&#xff0c;防止過充過放&#xff0c;并通過智能均衡技術提升續航能力。電感在BMS系統的電源轉換、濾波和隔離通信等…

【機器學習筆記 Ⅱ】12隨機森林

隨機森林&#xff08;Random Forest&#xff09;詳解 隨機森林是一種基于集成學習&#xff08;Ensemble Learning&#xff09;的高性能分類/回歸算法&#xff0c;通過構建多棵決策樹并綜合其預測結果&#xff0c;顯著提升模型的準確性和魯棒性。其核心思想是“集體智慧優于個體…

問題 1:MyBatis-plus-3.5.9 的分頁功能修復

問題 1&#xff1a;MyBatis-plus-3.5.9 的分頁功能修復 使用 Sw?agger 接口文檔?依次對上述接口進行測 試&#xff0c;發現 listU?serVOByPage 接口有一些問題&#xff01; 分頁好像沒有生效&#xff0c;還是查出了全部數據&#xff1a; 由于我們用的是 MyBatis Plus 來操…

Qt 如何提供在線幫助

Qt 如何提供在線幫助一、概述二、工具提示、狀態提示和"Whats This?"幫助1、工具提示(Tool Tips)添加工具提示到控件富文本工具提示全局工具提示設置延遲顯示控制自定義工具提示窗口禁用工具提示工具提示與狀態欄聯動特點&#xff1a;2、狀態提示(Status Tips)3、&q…

Typecho站點關閉插件開發全指南:從原理到實現

文章目錄 開發Typecho站點關閉插件:從原理到實現一、背景與需求分析二、插件設計思路2.1 技術選型2.2 功能模塊設計三、插件開發實現3.1 插件基礎結構3.2 插件主文件實現3.3 核心功能實現3.4 后臺管理界面3.5 關閉頁面模板四、插件配置完善4.1 配置表單實現4.2 定時任務處理五…

詳細解析 .NET 依賴注入的三種生命周期模式

文章目錄一、Transient&#xff08;瞬時生命周期&#xff09;原理使用方式核心特性適用場景優勢劣勢二、Scoped&#xff08;作用域生命周期&#xff09;原理使用方式核心特性適用場景優勢劣勢三、Singleton&#xff08;單例生命周期&#xff09;原理使用方式核心特性適用場景優…

軟件工程經濟與倫理

前言 各位帥哥美女&#xff0c;能看到這篇博客的都有口福了&#xff0c;學習這門課程就像遨游在大份的海洋&#xff0c;一不小心就吃上一口。能看到這篇博客說明我們是有緣人可以點贊收藏一下&#xff0c;這篇博客可以在你無比饑餓的時候給你送上一坨&#xff01;&#xff08;香…

AI 體驗走查 - 火山引擎存儲的 AI UX 探索之路

01 概述 火山引擎存儲技術團隊驅動 AI 自主完成用戶體驗走查 / 可用性測試的執行與評價&#xff0c;幫助業務改善交互體驗。 立項“故事走查”的背景訴求和 AI 機遇 如何搭建“AI 評價”能力&#xff0c;精準識別交互問題 讓交互體驗故事走查變為技術產品&#xff0c;講解系…

【世紀龍科技】汽車零部件檢驗虛擬實訓室-助力汽車職教實訓

在汽車產業加速向電動化、智能化轉型的背景下&#xff0c;職業院校汽車專業教學面臨新的挑戰&#xff1a;傳統實訓受限于設備數量不足、操作風險高、標準化程度低等問題&#xff0c;導致學生實踐機會有限&#xff0c;技能掌握不扎實。如何讓學生在有限資源下高效掌握零部件檢驗…

MySQL常用操作 查看表描述以及表結構、連接數及緩存和性能指標

查看表描述以及表結構查看數據庫名SHOW DATABASES; SELECT DATABASE(); SELECT DATABASE() AS current_database;查看數據庫中表的列表SHOW TABLES; SELECT TABLE_NAME, TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA your_database_name; SELECT TABLE_NA…

音視頻學習(三十六):websocket協議總結

概述項目描述標準RFC 6455使用端口默認 80&#xff08;ws&#xff09;&#xff0c;443&#xff08;wss&#xff09;基于協議TCP特性全雙工、低開銷、持久連接、可穿透代理特點 全雙工通信&#xff1a; WebSocket 允許客戶端和服務器之間建立一個持久的連接&#xff0c;并且數據…

docker版本nacos的搭建

1.下載鏡像2.拷貝出容器中對應的配置文件&#xff0c;logs&#xff0c;data&#xff0c;conf3.編寫yaml配置文件version: 3.8 services:nacos-server:image: nacos/nacos-server:v2.4.0container_name: nacos-serverrestart: unless-stoppedports:- "8848:8848" # …

【機器學習深度學習】 如何解決“宏平均偏低 / 小類識別差”的問題?

目錄 &#x1f9e9; 場景 一、先問清楚&#xff1a;小類差&#xff0c;到底差在哪&#xff1f; 二、對癥下藥&#xff1a;六大優化策略&#xff08;分類任務專用&#xff09; ? 1. 處理類別不平衡&#xff08;最常見&#xff09; ? 2. 優化數據質量 ? 3. 更強的模型結…

數據結構 --- 棧

棧 --- stack前言一、棧結構二、相關方法1.初始化2.入棧3.出棧4.判空5.獲取棧頂元素6.獲取棧大小7.銷毀前言 棧是一個特殊的線性表&#xff0c;遵循一個先進后出的特性&#xff0c;即操作數據&#xff08;入棧&#xff0c;出棧&#xff09;只能從棧頂操作&#xff0c;棧底是一…

【uniapp】---- 在 HBuilderX 中使用 tailwindcss

1. 前言 接手了一個uniapp的微信小程序項目,因為在上一個 taro 的項目中使用的 tailwindcss,感覺比較方便,又不想動項目中原來的代碼,因此就配置 tailwindcss,在新創建的子包中使用。 2. 分析 vue2 版本的 uni-app 內置的 webpack 版本為 4 , postcss 版本為 7, 所以還是…

Spring Boot + Easy Excel 自定義復雜樣式導入導出

tips&#xff1a;能用模板就用模板&#xff0c;當模板不適用的情況下&#xff0c;再選擇自定義生成 Excel。官網&#xff1a;https://easyexcel.opensource.alibaba.com安裝<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</arti…

Spark從入門到實戰:安裝與使用全攻略

目錄一、Spark 簡介1.1 Spark 的概念1.2 Spark 的優勢1.3 Spark 的應用場景二、安裝前準備2.1 硬件要求2.2 軟件要求2.3 下載 Spark三、Spark 安裝步驟3.1 解壓安裝包3.2 配置環境變量3.3 配置 spark-env.sh3.4 配置 slaves 文件&#xff08;分布式模式&#xff09;3.5 啟動 Sp…

Python 進程間的通信:原理剖析與項目實戰

在 Python 編程中,當涉及多進程編程時,進程間的通信(Inter-Process Communication,簡稱 IPC)是一個重要的課題。多個進程在運行過程中,常常需要交換數據、傳遞狀態或協同工作,這就離不開進程間通信機制。本文將深入講解 Python 進程間通信的原理,并結合實際項目案例,展…

神經網絡之BP算法

一、正向傳播正向傳播&#xff08;Forward Propagation&#xff09;是神經網絡中數據從輸入層流向輸出層的過程。輸入數據通過各層的權重和激活函數逐層計算&#xff0c;最終得到預測輸出。數學表示&#xff1a; 對于第 ( l ) 層的神經元&#xff0c;其輸出計算如下&#xff1a…

Ubuntu 版本號與別名對照表(部分精選)

Ubuntu 的別名遵循 形容詞 動物名 的命名規則&#xff0c;且兩個單詞首字母相同&#xff0c;按字母表順序循環使用&#xff08;從 Ubuntu 6.06 開始&#xff09;。 &#x1f4c5; Ubuntu 版本號與別名對照表&#xff08;部分精選&#xff09; 版本號別名 (開發代號)發布時間…