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

902 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# React-PDF 功能总结文档
> **库版本**: react-pdf v9.2.1
> **基于**: Mozilla PDF.js
> **测试Demo**: `/pdf-demo`
> **创建时间**: 2025-11-22
---
## 📋 目录
- [1. 库概述](#1-库概述)
- [2. 核心功能](#2-核心功能)
- [3. 内置功能详解](#3-内置功能详解)
- [4. API参考](#4-api参考)
- [5. 实践示例](#5-实践示例)
- [6. 性能优化](#6-性能优化)
- [7. 常见问题](#7-常见问题)
---
## 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 文件**
```typescript
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';
```
**基本使用:**
```typescript
<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 组件
```typescript
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 事件
```typescript
// 加载成功
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 事件
```typescript
// 页面加载成功
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 基本使用
```typescript
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 基本使用
```typescript
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 支持的旋转角度
- `0°` - 正常方向
- `90°` - 顺时针旋转90度
- `180°` - 倒置
- `270°` - 顺时针旋转270度(或逆时针90度)
**注意**: 仅支持 90 度的倍数,其他角度无效。
### 3.3 文本层 (Text Layer)
#### 3.3.1 启用文本层
**⚠️ 重要:必须导入 CSS 文件!**
```typescript
// 导入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 文本选择监听
```typescript
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 自定义:
```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 启用注释层
```typescript
<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 模式(默认)
```typescript
<Page pageNumber={1} renderMode="canvas" />
```
**特点**:
- ✅ 性能最佳
- ✅ 内存占用低
- ✅ 适合大多数场景
- ❌ 缩放时可能模糊
- ❌ 打印质量一般
#### 3.5.2 SVG 模式
```typescript
<Page pageNumber={1} renderMode="svg" />
```
**特点**:
- ✅ 矢量渲染,无限缩放不失真
- ✅ 打印质量极佳
- ✅ 支持复杂图形
- ❌ 性能较低
- ❌ 内存占用高
- ❌ 不适合大文档
#### 3.5.3 选择建议
| 场景 | 推荐模式 | 原因 |
|------|---------|------|
| 普通阅读 | Canvas | 性能好 |
| 高清打印 | SVG | 质量高 |
| 移动端 | Canvas | 省内存 |
| 频繁缩放 | SVG | 不失真 |
| 大文档(>50页) | Canvas | 性能 |
### 3.6 设备像素比 (Device Pixel Ratio)
#### 3.6.1 基本使用
```typescript
<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 路径
```typescript
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
```typescript
interface PDFDocumentProxy {
numPages: number; // 总页数
fingerprints: string[]; // 文档指纹
getPage(pageNumber: number): Promise<PDFPageProxy>;
getMetadata(): Promise<Metadata>;
// ... 更多方法
}
```
#### onPageLoadSuccess
```typescript
interface PDFPageProxy {
pageNumber: number; // 页码
rotate: number; // 旋转角度
ref: object; // 页面引用
userUnit: number; // 用户单位
view: number[]; // 视图矩形 [x1, y1, x2, y2]
width: number; // 原始宽度
height: number; // 原始高度
// ... 更多属性和方法
}
```
---
## 5. 实践示例
### 5.1 完整的PDF查看器
```typescript
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 渲染所有页面
```typescript
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 自定义高亮功能
```typescript
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查看器
```typescript
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)
```typescript
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页),使用虚拟滚动只渲染可见页面:
```typescript
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 缓存策略
```typescript
// 使用缓存避免重复加载
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 路径。
```typescript
// ❌ 错误
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 文件!**
```typescript
// ✅ 正确:导入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: 文本层坐标受缩放影响,需要在高亮时调整:
```typescript
const adjustedRect = {
left: rect.left / scale,
top: rect.top / scale,
width: rect.width / scale,
height: rect.height / scale
};
```
### 7.4 样式问题
**Q: 如何自定义 PDF 页面样式?**
A:
```css
/* 自定义页面边框 */
.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
```typescript
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:
```bash
npm install --save-dev @types/react-pdf
```
或手动声明:
```typescript
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. 参考资源
- 📖 [官方文档](https://github.com/wojtekmaj/react-pdf)
- 📖 [PDF.js 文档](https://mozilla.github.io/pdf.js/)
- 📦 [NPM Package](https://www.npmjs.com/package/react-pdf)
- 🎮 [在线Demo](http://projects.wojtekmaj.pl/react-pdf/)
- 💬 [GitHub Issues](https://github.com/wojtekmaj/react-pdf/issues)
---
**文档版本**: v1.0
**最后更新**: 2025-11-22
**维护者**: Claude Code 🤖