22 KiB
22 KiB
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 支持的旋转角度
0°- 正常方向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 文本层功能
启用后可实现:
- 文本选择 - 用户可以用鼠标选中文本
- 文本复制 - 选中后可以复制文本
- 文本搜索 - 浏览器原生搜索功能 (Ctrl+F)
- 屏幕阅读器 - 可访问性支持
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:
- 从
node_modules/pdfjs-dist/build/pdf.worker.js复制到public/目录 - 或使用 CDN:
//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js
7.2 渲染问题
Q: PDF 显示模糊怎么办?
A:
- 提高
scale值 - 设置
devicePixelRatio={window.devicePixelRatio} - 使用 SVG 渲染模式
Q: PDF 加载很慢怎么办?
A:
- 压缩 PDF 文件
- 使用虚拟滚动
- 按需加载页面
- 启用 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} />
其他可能的原因:
- 确认
renderTextLayer={true} - 检查 CSS 是否覆盖了文本层 (
pointer-events: none) - 检查文本层是否被其他元素遮挡(
z-index) - 确保容器允许文本选择(
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:
- 使用代理服务器
- 配置服务器 CORS 头
- 使用
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 核心优势
- ✅ 功能完善 - 支持缩放、旋转、文本选择等核心功能
- ✅ 性能优异 - Worker 异步渲染,不阻塞UI
- ✅ 灵活配置 - Canvas/SVG 双渲染模式
- ✅ 易于集成 - React组件化,API简洁
- ✅ 活跃维护 - 基于 PDF.js,持续更新
8.2 适用场景
- ✅ 文档预览系统
- ✅ 在线阅读器
- ✅ 文档审核系统
- ✅ 电子签名应用
- ✅ 在线表单填写
8.3 不适用场景
- ❌ 生成PDF(使用 @react-pdf/renderer)
- ❌ 编辑PDF内容(使用 PDF.js API或其他库)
- ❌ PDF合并/拆分(使用 pdf-lib)
9. 参考资源
- 📖 官方文档
- 📖 PDF.js 文档
- 📦 NPM Package
- 🎮 在线Demo
- 💬 GitHub Issues
文档版本: v1.0 最后更新: 2025-11-22 维护者: Claude Code 🤖