Files
2025-12-05 00:09:32 +08:00

22 KiB
Raw Permalink Blame History

React-PDF 功能总结文档

库版本: react-pdf v9.2.1 基于: Mozilla PDF.js 测试Demo: /pdf-demo 创建时间: 2025-11-22


📋 目录


1. 库概述

1.1 什么是 react-pdf

react-pdf 是一个用于在 React 应用中显示和预览 PDF 文件的库,基于 Mozilla 的 PDF.js 引擎构建。

1.2 主要特性

  • 查看 PDF - 在浏览器中渲染和显示 PDF 文档
  • 多页支持 - 支持渲染单页或所有页面
  • 文本选择 - 启用文本层后可选择和复制文本
  • 缩放控制 - 内置缩放和旋转功能
  • 多种渲染模式 - Canvas 或 SVG 渲染
  • 注释支持 - 显示 PDF 内置注释
  • 响应式 - 适应不同屏幕尺寸
  • 高性能 - 基于 WebWorker 的异步渲染

1.3 与 @react-pdf/renderer 的区别

特性 react-pdf @react-pdf/renderer
用途 查看已有PDF 生成新PDF
输入 PDF文件 React组件
输出 浏览器渲染 PDF文件
文本选择 支持 不适用
交互性

2. 核心功能

2.1 基础文档渲染

2.1.1 Document 组件

⚠️ 第一步:导入必需的 CSS 文件

import { Document, Page, pdfjs } from 'react-pdf';

// 🔑 必须导入CSS,否则文本层无法工作!
import 'react-pdf/dist/Page/TextLayer.css';
import 'react-pdf/dist/Page/AnnotationLayer.css';

// 配置Worker
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';

基本使用:

<Document
  file={pdfUrl}                          // PDF文件URL或ArrayBuffer
  onLoadSuccess={onDocumentLoadSuccess}  // 加载成功回调
  onLoadError={onDocumentLoadError}      // 加载失败回调
  onLoadProgress={onLoadProgress}        // 加载进度回调
  loading={<div>加载中...</div>}         // 自定义加载组件
  error={<div>加载失败</div>}            // 自定义错误组件
  noData={<div>无数据</div>}             // 无数据组件
>
  {/* Page 组件 */}
</Document>

支持的文件格式:

  • URL 字符串: "http://example.com/file.pdf"
  • File 对象: new File([blob], "file.pdf")
  • ArrayBuffer: 二进制数据
  • Base64: "data:application/pdf;base64,JVBERi0x..."

2.1.2 Page 组件

import { Page } from 'react-pdf';

<Page
  pageNumber={1}                    // 页码(从1开始)
  scale={1.0}                       // 缩放比例
  rotate={0}                        // 旋转角度(0, 90, 180, 270
  renderTextLayer={true}            // 启用文本层
  renderAnnotationLayer={true}      // 启用注释层
  renderMode="canvas"               // 渲染模式: "canvas" | "svg"
  devicePixelRatio={window.devicePixelRatio}  // 设备像素比
  onLoadSuccess={onPageLoadSuccess} // 页面加载成功回调
  onLoadError={onPageLoadError}     // 页面加载失败回调
  className="pdf-page"              // 自定义样式类
/>

2.2 事件回调

2.2.1 Document 事件

// 加载成功
function onDocumentLoadSuccess({ numPages }: { numPages: number }) {
  console.log('PDF总页数:', numPages);
}

// 加载失败
function onDocumentLoadError(error: Error) {
  console.error('PDF加载失败:', error.message);
}

// 加载进度
function onDocumentLoadProgress({ loaded, total }: { loaded: number; total: number }) {
  const progress = Math.round((loaded / total) * 100);
  console.log('加载进度:', progress + '%');
}

2.2.2 Page 事件

// 页面加载成功
function onPageLoadSuccess(page: any) {
  console.log('页面渲染成功:', page.pageNumber);
  console.log('页面尺寸:', page.width, page.height);
}

// 页面加载失败
function onPageLoadError(error: Error) {
  console.error('页面渲染失败:', error);
}

3. 内置功能详解

3.1 缩放功能 (Scale)

3.1.1 基本使用

const [scale, setScale] = useState(1.0);

// 放大
const handleZoomIn = () => {
  setScale(prev => Math.min(prev + 0.25, 3.0)); // 最大300%
};

// 缩小
const handleZoomOut = () => {
  setScale(prev => Math.max(prev - 0.25, 0.5)); // 最小50%
};

// 应用缩放
<Page pageNumber={1} scale={scale} />

3.1.2 缩放建议值

缩放级别 scale值 使用场景
极小 0.5 预览缩略图
0.75 查看概览
正常 1.0 默认阅读
1.5 放大查看细节
极大 2.0-3.0 精细查看文本

3.1.3 注意事项

  • ⚠️ 缩放会影响性能,建议范围 0.5-3.0
  • ⚠️ 缩放不会改变文本层坐标,需要手动调整高亮位置
  • 使用 transform: scale() CSS 可以实现视觉缩放但不重新渲染

3.2 旋转功能 (Rotate)

3.2.1 基本使用

const [rotation, setRotation] = useState(0);

// 左转90度
const handleRotateLeft = () => {
  setRotation(prev => (prev - 90) % 360);
};

// 右转90度
const handleRotateRight = () => {
  setRotation(prev => (prev + 90) % 360);
};

// 应用旋转
<Page pageNumber={1} rotate={rotation} />

3.2.2 支持的旋转角度

  • - 正常方向
  • 90° - 顺时针旋转90度
  • 180° - 倒置
  • 270° - 顺时针旋转270度(或逆时针90度)

注意: 仅支持 90 度的倍数,其他角度无效。

3.3 文本层 (Text Layer)

3.3.1 启用文本层

⚠️ 重要:必须导入 CSS 文件!

// 导入react-pdf的CSS样式(文本层必需)
import 'react-pdf/dist/Page/TextLayer.css';
import 'react-pdf/dist/Page/AnnotationLayer.css';

<Page
  pageNumber={1}
  renderTextLayer={true}  // ✅ 启用文本层
/>

常见错误:如果不导入CSS,文本层虽然会渲染,但文本无法被选择

3.3.2 文本层功能

启用后可实现:

  1. 文本选择 - 用户可以用鼠标选中文本
  2. 文本复制 - 选中后可以复制文本
  3. 文本搜索 - 浏览器原生搜索功能 (Ctrl+F)
  4. 屏幕阅读器 - 可访问性支持

3.3.3 文本选择监听

const handleTextSelection = () => {
  const selection = window.getSelection();
  if (!selection || selection.isCollapsed) return;

  const selectedText = selection.toString();
  console.log('选中文本:', selectedText);
};

<div onMouseUp={handleTextSelection}>
  <Page pageNumber={1} renderTextLayer={true} />
</div>

3.3.4 文本层样式

默认文本层是透明的,覆盖在 Canvas 渲染的 PDF 上方。可以通过 CSS 自定义:

/* 文本层容器 */
.react-pdf__Page__textContent {
  /* 默认透明,文本不可见但可选择 */
  opacity: 0.2; /* 调试时可以显示文本层 */
}

/* 文本层中的文本 */
.react-pdf__Page__textContent span {
  color: transparent; /* 文本透明 */
  position: absolute;
  white-space: pre;
}

3.4 注释层 (Annotation Layer)

3.4.1 启用注释层

<Page
  pageNumber={1}
  renderAnnotationLayer={true}  // ✅ 启用注释层
/>

3.4.2 支持的注释类型

  • 📝 文本注释 (Text Annotations)
  • 🔗 链接注释 (Link Annotations)
  • 📎 附件注释 (File Attachment Annotations)
  • 🎨 高亮/下划线 (Highlight/Underline)
  • 💬 弹出注释 (Popup Annotations)

3.4.3 注释交互

  • 链接可点击(内部链接、外部URL
  • 注释可以悬停显示提示
  • 表单字段可交互(如果PDF包含表单)

3.5 渲染模式 (Render Mode)

3.5.1 Canvas 模式(默认)

<Page pageNumber={1} renderMode="canvas" />

特点:

  • 性能最佳
  • 内存占用低
  • 适合大多数场景
  • 缩放时可能模糊
  • 打印质量一般

3.5.2 SVG 模式

<Page pageNumber={1} renderMode="svg" />

特点:

  • 矢量渲染,无限缩放不失真
  • 打印质量极佳
  • 支持复杂图形
  • 性能较低
  • 内存占用高
  • 不适合大文档

3.5.3 选择建议

场景 推荐模式 原因
普通阅读 Canvas 性能好
高清打印 SVG 质量高
移动端 Canvas 省内存
频繁缩放 SVG 不失真
大文档(>50页) Canvas 性能

3.6 设备像素比 (Device Pixel Ratio)

3.6.1 基本使用

<Page
  pageNumber={1}
  devicePixelRatio={window.devicePixelRatio || 1}
/>

3.6.2 作用

  • 🖥️ 在高分辨率屏幕(Retina屏)上提高清晰度
  • 📱 适配不同设备的像素密度
  • 值越大,渲染质量越高,但性能消耗也越大

常见值:

  • 普通屏幕: 1
  • Retina屏: 2
  • 4K屏: 3 或更高

建议: 使用 window.devicePixelRatio 自动适配。

3.7 Worker 配置

3.7.1 设置 Worker 路径

import { pdfjs } from 'react-pdf';

// 使用本地worker文件
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';

// 或使用CDN
pdfjs.GlobalWorkerOptions.workerSrc =
  `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;

3.7.2 Worker 作用

  • 异步渲染 - 在后台线程处理PDF解析
  • 🚫 不阻塞UI - 主线程不会被卡住
  • 📈 性能提升 - 大文件加载更流畅

4. API 参考

4.1 Document Props

属性 类型 默认值 说明
file string | File | ArrayBuffer | object 必填 PDF文件源
loading ReactNode "Loading PDF..." 加载中显示的内容
error ReactNode "Failed to load PDF" 加载失败显示的内容
noData ReactNode "No PDF file specified" 无数据时显示的内容
onLoadSuccess (pdf) => void - 加载成功回调
onLoadError (error) => void - 加载失败回调
onLoadProgress (progress) => void - 加载进度回调
onSourceSuccess () => void - 文件源加载成功回调
onSourceError (error) => void - 文件源加载失败回调
rotate 0 | 90 | 180 | 270 0 旋转角度
className string - CSS类名
inputRef RefObject - 引用

4.2 Page Props

属性 类型 默认值 说明
pageNumber number 必填 页码(从1开始)
scale number 1.0 缩放比例
rotate 0 | 90 | 180 | 270 0 旋转角度
width number - 页面宽度(px
height number - 页面高度(px
renderTextLayer boolean true 是否渲染文本层
renderAnnotationLayer boolean true 是否渲染注释层
renderMode "canvas" | "svg" "canvas" 渲染模式
devicePixelRatio number 1 设备像素比
onLoadSuccess (page) => void - 页面加载成功回调
onLoadError (error) => void - 页面加载失败回调
onRenderSuccess () => void - 渲染成功回调
onRenderError (error) => void - 渲染失败回调
className string - CSS类名
inputRef RefObject - 引用

4.3 回调函数参数

onDocumentLoadSuccess

interface PDFDocumentProxy {
  numPages: number;           // 总页数
  fingerprints: string[];     // 文档指纹
  getPage(pageNumber: number): Promise<PDFPageProxy>;
  getMetadata(): Promise<Metadata>;
  // ... 更多方法
}

onPageLoadSuccess

interface PDFPageProxy {
  pageNumber: number;         // 页码
  rotate: number;             // 旋转角度
  ref: object;                // 页面引用
  userUnit: number;           // 用户单位
  view: number[];             // 视图矩形 [x1, y1, x2, y2]
  width: number;              // 原始宽度
  height: number;             // 原始高度
  // ... 更多属性和方法
}

5. 实践示例

5.1 完整的PDF查看器

import { useState } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';

pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';

function PdfViewer({ url }: { url: string }) {
  const [numPages, setNumPages] = useState<number | null>(null);
  const [pageNumber, setPageNumber] = useState(1);
  const [scale, setScale] = useState(1.0);

  return (
    <div>
      {/* 工具栏 */}
      <div className="toolbar">
        <button onClick={() => setScale(s => s + 0.25)}>放大</button>
        <button onClick={() => setScale(s => s - 0.25)}>缩小</button>
        <span>缩放: {Math.round(scale * 100)}%</span>

        <button
          onClick={() => setPageNumber(p => Math.max(1, p - 1))}
          disabled={pageNumber <= 1}
        >
          上一页
        </button>
        <span>页码: {pageNumber} / {numPages}</span>
        <button
          onClick={() => setPageNumber(p => Math.min(numPages || 1, p + 1))}
          disabled={!numPages || pageNumber >= numPages}
        >
          下一页
        </button>
      </div>

      {/* PDF渲染 */}
      <Document
        file={url}
        onLoadSuccess={({ numPages }) => setNumPages(numPages)}
      >
        <Page
          pageNumber={pageNumber}
          scale={scale}
          renderTextLayer={true}
        />
      </Document>
    </div>
  );
}

5.2 渲染所有页面

function PdfAllPages({ url }: { url: string }) {
  const [numPages, setNumPages] = useState<number | null>(null);

  return (
    <Document
      file={url}
      onLoadSuccess={({ numPages }) => setNumPages(numPages)}
    >
      {numPages && Array.from({ length: numPages }, (_, i) => (
        <div key={i + 1} className="page-container">
          <p> {i + 1} </p>
          <Page pageNumber={i + 1} />
        </div>
      ))}
    </Document>
  );
}

5.3 自定义高亮功能

function PdfWithHighlight({ url }: { url: string }) {
  const [highlights, setHighlights] = useState<any[]>([]);

  const handleTextSelection = () => {
    const selection = window.getSelection();
    if (!selection || selection.isCollapsed) return;

    const range = selection.getRangeAt(0);
    const rects = Array.from(range.getClientRects());

    // 计算高亮区域相对位置
    const pageElement = range.startContainer.parentElement
      ?.closest('[data-page-number]');

    if (!pageElement) return;

    const pageRect = pageElement.getBoundingClientRect();
    const pageNumber = parseInt(
      pageElement.getAttribute('data-page-number') || '1'
    );

    const newHighlight = {
      pageNumber,
      rects: rects.map(rect => ({
        left: rect.left - pageRect.left,
        top: rect.top - pageRect.top,
        width: rect.width,
        height: rect.height
      })),
      text: selection.toString()
    };

    setHighlights(prev => [...prev, newHighlight]);
  };

  return (
    <div onMouseUp={handleTextSelection}>
      <Document file={url}>
        <div style={{ position: 'relative' }}>
          <Page pageNumber={1} renderTextLayer={true} />

          {/* 渲染高亮层 */}
          {highlights.map((hl, idx) => (
            <div key={idx}>
              {hl.rects.map((rect: any, i: number) => (
                <div
                  key={i}
                  style={{
                    position: 'absolute',
                    left: rect.left,
                    top: rect.top,
                    width: rect.width,
                    height: rect.height,
                    backgroundColor: 'yellow',
                    opacity: 0.3,
                    pointerEvents: 'none'
                  }}
                />
              ))}
            </div>
          ))}
        </div>
      </Document>
    </div>
  );
}

5.4 响应式PDF查看器

import { useRef, useEffect, useState } from 'react';

function ResponsivePdfViewer({ url }: { url: string }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [width, setWidth] = useState<number>(0);

  useEffect(() => {
    const updateWidth = () => {
      if (containerRef.current) {
        setWidth(containerRef.current.offsetWidth);
      }
    };

    updateWidth();
    window.addEventListener('resize', updateWidth);
    return () => window.removeEventListener('resize', updateWidth);
  }, []);

  return (
    <div ref={containerRef}>
      <Document file={url}>
        <Page pageNumber={1} width={width} />
      </Document>
    </div>
  );
}

6. 性能优化

6.1 延迟加载 (Lazy Loading)

import { lazy, Suspense } from 'react';

const Document = lazy(() => import('react-pdf').then(m => ({ default: m.Document })));
const Page = lazy(() => import('react-pdf').then(m => ({ default: m.Page })));

function LazyPdfViewer() {
  return (
    <Suspense fallback={<div>加载PDF组件...</div>}>
      <Document file="document.pdf">
        <Page pageNumber={1} />
      </Document>
    </Suspense>
  );
}

6.2 虚拟滚动

对于大文档(>100页),使用虚拟滚动只渲染可见页面:

import { FixedSizeList } from 'react-window';

function VirtualPdfViewer({ url, numPages }: any) {
  const Row = ({ index, style }: any) => (
    <div style={style}>
      <Page pageNumber={index + 1} width={800} />
    </div>
  );

  return (
    <FixedSizeList
      height={800}
      itemCount={numPages}
      itemSize={1100}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

6.3 缓存策略

// 使用缓存避免重复加载
const pdfCache = new Map();

async function loadPdfWithCache(url: string) {
  if (pdfCache.has(url)) {
    return pdfCache.get(url);
  }

  const response = await fetch(url);
  const blob = await response.blob();
  pdfCache.set(url, blob);

  return blob;
}

6.4 优化建议

优化项 建议 影响
缩放范围 限制在 0.5-3.0 性能 +++
按需渲染 只渲染可见页面 性能 +++++
Worker 必须启用 性能 +++++
文本层 按需启用 性能 +++
SVG模式 仅在需要时使用 性能 ++
devicePixelRatio 限制最大值为2 性能 +++

7. 常见问题

7.1 Worker 相关

Q: 为什么出现 "Cannot read property 'getDocument' of undefined" 错误?

A: 没有正确配置 Worker 路径。

// ❌ 错误
import { Document, Page } from 'react-pdf';

// ✅ 正确
import { Document, Page, pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';

Q: Worker 文件从哪里获取?

A:

  1. node_modules/pdfjs-dist/build/pdf.worker.js 复制到 public/ 目录
  2. 或使用 CDN: //cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js

7.2 渲染问题

Q: PDF 显示模糊怎么办?

A:

  1. 提高 scale
  2. 设置 devicePixelRatio={window.devicePixelRatio}
  3. 使用 SVG 渲染模式

Q: PDF 加载很慢怎么办?

A:

  1. 压缩 PDF 文件
  2. 使用虚拟滚动
  3. 按需加载页面
  4. 启用 Worker(默认启用)

7.3 文本选择问题

Q: 文本层启用了但无法选择文本?

A: 最常见原因是没有导入 CSS 文件!

// ✅ 正确:导入CSS
import 'react-pdf/dist/Page/TextLayer.css';
import 'react-pdf/dist/Page/AnnotationLayer.css';

<Page pageNumber={1} renderTextLayer={true} />

其他可能的原因:

  1. 确认 renderTextLayer={true}
  2. 检查 CSS 是否覆盖了文本层 (pointer-events: none)
  3. 检查文本层是否被其他元素遮挡(z-index
  4. 确保容器允许文本选择(user-select: text

Q: 文本层位置不准确?

A: 文本层坐标受缩放影响,需要在高亮时调整:

const adjustedRect = {
  left: rect.left / scale,
  top: rect.top / scale,
  width: rect.width / scale,
  height: rect.height / scale
};

7.4 样式问题

Q: 如何自定义 PDF 页面样式?

A:

/* 自定义页面边框 */
.react-pdf__Page {
  border: 1px solid #ccc;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

/* 自定义Canvas样式 */
.react-pdf__Page__canvas {
  max-width: 100%;
  height: auto !important;
}

/* 自定义文本层样式 */
.react-pdf__Page__textContent {
  /* 调试时显示文本层 */
  opacity: 0.2;
}

7.5 CORS 问题

Q: 加载外部 PDF 出现 CORS 错误?

A:

  1. 使用代理服务器
  2. 配置服务器 CORS 头
  3. 使用 fetch 先下载再传递 Blob
async function loadPdfWithCors(url: string) {
  const response = await fetch(url, { mode: 'cors' });
  const blob = await response.blob();
  return blob;
}

<Document file={blob} />

7.6 TypeScript 类型问题

Q: TypeScript 类型定义缺失?

A:

npm install --save-dev @types/react-pdf

或手动声明:

declare module 'react-pdf' {
  export const Document: any;
  export const Page: any;
  export const pdfjs: any;
}

8. 总结

8.1 核心优势

  1. 功能完善 - 支持缩放、旋转、文本选择等核心功能
  2. 性能优异 - Worker 异步渲染,不阻塞UI
  3. 灵活配置 - Canvas/SVG 双渲染模式
  4. 易于集成 - React组件化,API简洁
  5. 活跃维护 - 基于 PDF.js,持续更新

8.2 适用场景

  • 文档预览系统
  • 在线阅读器
  • 文档审核系统
  • 电子签名应用
  • 在线表单填写

8.3 不适用场景

  • 生成PDF(使用 @react-pdf/renderer
  • 编辑PDF内容(使用 PDF.js API或其他库)
  • PDF合并/拆分(使用 pdf-lib

9. 参考资源


文档版本: v1.0 最后更新: 2025-11-22 维护者: Claude Code 🤖