@react-pdf/renderer 和 react-pdf 都是 React 生态中处理 PDF 的核心库,但它们的职责截然不同。@react-pdf/renderer 是一个渲染引擎,允许开发者使用 React 组件和样式来从头构建 PDF 文件,它不依赖浏览器原生能力,可直接在 Node.js 或服务端生成二进制 PDF 流。而 react-pdf 是一个基于 Mozilla pdf.js 的查看器组件,专为在浏览器中渲染和展示现有的 PDF 文件而设计,提供缩放、翻页等交互功能。简而言之,前者用于“写”PDF,后者用于“读”PDF。
在 React 开发生态中,处理 PDF 通常涉及两个完全不同的阶段:创建文档和展示文档。@react-pdf/renderer 和 react-pdf 分别占据了这两个生态位。很多开发者容易混淆二者,试图用查看器去生成文件,或用生成器去展示复杂交互,这会导致架构上的严重失误。让我们深入技术细节,看看它们如何工作以及如何正确选型。
@react-pdf/renderer 是一个纯粹的生成引擎。它实现了一套类似 React 的虚拟 DOM 系统,但目标不是浏览器的 HTML,而是 PDF 规范。它不依赖浏览器环境,可以在 Node.js 服务器、Serverless 函数或客户端直接运行,输出二进制的 PDF 文件流。
// @react-pdf/renderer: 定义文档结构并生成文件
import { Document, Page, Text, StyleSheet, pdf } from '@react-pdf/renderer';
const MyDocument = () => (
<Document>
<Page size="A4">
<Text>发票编号:#1024</Text>
<Text>金额:$500.00</Text>
</Page>
</Document>
);
// 在浏览器或 Node.js 中获取 blob 或 buffer
const blob = await pdf(<MyDocument />).toBlob();
// 触发下载或发送给后端
react-pdf 是一个渲染组件。它底层依赖 Mozilla 的 pdf.js 库,将现有的 PDF 二进制数据解析并在 HTML5 Canvas 上绘制出来。它的核心任务是“展示”,提供缩放、翻页、文本选择等查看器功能,但它无法修改或创建 PDF 内容。
// react-pdf: 加载并展示现有 PDF 文件
import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';
// 设置 worker 源(必须步骤)
pdfjs.GlobalWorkerOptions.workerSrc = '//cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.177/pdf.worker.min.js';
function Viewer({ fileUrl }) {
const [numPages, setNumPages] = useState(null);
function onDocumentLoadSuccess({ numPages }) {
setNumPages(numPages);
}
return (
<div>
<Document file={fileUrl} onLoadSuccess={onDocumentLoadSuccess}>
{Array.from(new Array(numPages), (el, index) => (
<Page key={`page_${index + 1}`} pageNumber={index + 1} />
))}
</Document>
</div>
);
}
@react-pdf/renderer 使用一套受限的 CSS 子集进行样式定义。它不支持所有 CSS 属性(例如没有 position: fixed 的复杂交互,也没有复杂的 flex 嵌套),因为它需要将样式转换为 PDF 规范指令。布局引擎是自定义的,专注于分页逻辑(例如内容超出页面自动换页)。
// @react-pdf/renderer: 样式定义类似 CSS-in-JS
const styles = StyleSheet.create({
page: {
flexDirection: 'row',
backgroundColor: '#E4E4E4',
padding: 30
},
section: {
margin: 10,
padding: 10,
flexGrow: 1,
fontSize: 12,
fontFamily: 'Helvetica' // 必须指定字体族
},
});
// 使用时
<View style={styles.page}>
<Text style={styles.section}>内容区域</Text>
</View>
react-pdf 不涉及样式定义(针对 PDF 内容),因为它只是忠实地渲染 PDF 文件内部已有的样式。你只能控制查看器容器的 CSS(如 canvas 的大小、边框、背景),而无法改变 PDF 内部的字体或布局。如果 PDF 本身字体缺失,它通常回退到默认字体,这取决于 pdf.js 的配置。
// react-pdf: 只能控制容器样式,无法干预 PDF 内部样式
<div style={{ border: '1px solid #ccc', maxWidth: '800px' }}>
<Page
pageNumber={1}
width={600} // 控制渲染宽度
renderTextLayer={true} // 开启文本层以支持复制
renderAnnotationLayer={true} // 开启注释层
/>
</div>
@react-pdf/renderer 的最大优势是同构能力。由于它不依赖 DOM 或 Canvas,它可以在 Node.js 服务端直接运行。这意味着你可以在 API 接口中接收请求,动态生成 PDF 并直接返回流,无需启动无头浏览器(如 Puppeteer)。
// Node.js 服务端示例 (Express)
import express from 'express';
import { pdf } from '@react-pdf/renderer';
import Invoice from './InvoiceTemplate';
const app = express();
app.get('/invoice/:id', async (req, res) => {
const doc = <Invoice id={req.params.id} />;
const buffer = await pdf(doc).toBuffer();
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename=invoice.pdf');
res.send(buffer);
});
react-pdf 强依赖浏览器 API(如 canvas, worker)。虽然它有 SSR 的支持策略(通常通过动态导入避免服务端报错),但其核心功能必须在客户端浏览器中才能生效。它不适合在服务端生成内容,仅适合在服务端渲染(SSR)时输出占位符,然后在客户端注水(Hydration)加载真实视图。
// React 客户端组件 (需处理 SSR)
import dynamic from 'next/dynamic';
// 动态导入以避免 SSR 错误
const PDFViewer = dynamic(() => import('react-pdf').then(mod => mod.Document), {
ssr: false,
loading: () => <p>正在加载文档...</p>
});
// 仅在客户端渲染
export default function Page() {
return <PDFViewer file="/manual.pdf" />;
}
@react-pdf/renderer 要求开发者显式注册字体文件(TTF/OTF),特别是使用中文字体时。默认字体通常只支持西欧字符。如果不注册中文字体,生成的 PDF 中的中文会显示为方框或乱码。这需要额外的构建步骤来加载字体文件。
// @react-pdf/renderer: 必须注册字体
import { Font, StyleSheet } from '@react-pdf/renderer';
// 注册本地或远程字体文件
Font.register({
family: 'Noto Sans SC',
src: 'https://fonts.gstatic.com/s/notosanssc/v25/.../NotoSansSC-Regular.ttf'
});
const styles = StyleSheet.create({
text: { fontFamily: 'Noto Sans SC' }
});
react-pdf 自动处理字体。因为 PDF 文件在生成时已经嵌入了字体子集(或者依赖系统字体),pdf.js 负责解析这些嵌入数据并在 Canvas 上绘制。开发者通常不需要关心字体文件,除非 PDF 使用了非常特殊的编码且未正确嵌入,这时可能需要配置 cMapUrl 来支持 CJK(中日韩)字符的正确渲染。
// react-pdf: 配置 CMap 以支持特殊字符集
import { pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = '...';
pdfjs.GlobalWorkerOptions.cMapUrl = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.177/cmaps/';
pdfjs.GlobalWorkerOptions.cMapPacked = true;
// 之后正常渲染即可,库会自动处理字符映射
@react-pdf/renderer 的性能瓶颈在于生成过程。对于几百页的复杂文档,构建虚拟 DOM 树并转换为 PDF 指令需要 CPU 计算时间。在客户端生成大文件可能会阻塞主线程,导致界面卡顿,建议在 Web Worker 中运行或使用服务端生成。
react-pdf 的性能瓶颈在于解析和绘制。大型 PDF 文件(如扫描版书籍)可能高达几十 MB,pdf.js 需要解析二进制流并逐页绘制到 Canvas。为了优化体验,通常采用“按需加载”策略,只渲染当前可视区域的页面,而不是一次性渲染所有页。
// react-pdf: 优化策略 - 仅渲染可见页
function OptimizedViewer({ file }) {
const [page, setPage] = useState(1);
return (
<div>
<button onClick={() => setPage(p => p - 1)}>上一页</button>
<Document file={file}>
{/* 只渲染当前页,而不是全部 */}
<Page pageNumber={page} />
</Document>
<button onClick={() => setPage(p => p + 1)}>下一页</button>
</div>
);
}
在实际企业级应用中,这两个库经常配合使用,而不是二选一。典型的场景是:用户使用 @react-pdf/renderer 在客户端或服务端生成一份报告,生成完成后,立即使用 react-pdf 在模态框中预览该文件,确认无误后再下载或发送。
@react-pdf/renderer 根据用户数据动态创建 PDF Blob。react-pdf 进行展示。import { pdf } from '@react-pdf/renderer';
import { Document, Page } from 'react-pdf';
import ReportTemplate from './ReportTemplate';
function ReportWorkflow() {
const [pdfUrl, setPdfUrl] = useState(null);
const generateAndPreview = async () => {
// 1. 生成 PDF
const blob = await pdf(<ReportTemplate data={...} />).toBlob();
// 2. 创建临时 URL
const url = URL.createObjectURL(blob);
setPdfUrl(url);
};
return (
<div>
<button onClick={generateAndPreview}>生成并预览报告</button>
{pdfUrl && (
<div className="preview-container">
<Document file={pdfUrl}>
<Page pageNumber={1} width={400} />
</Document>
</div>
)}
</div>
);
}
| 特性 | @react-pdf/renderer | react-pdf |
|---|---|---|
| 核心用途 | 创建 PDF 文件 | 查看 PDF 文件 |
| 底层依赖 | 自研渲染引擎 (无依赖) | Mozilla pdf.js |
| 运行环境 | Node.js, 浏览器, Serverless | 主要是浏览器 (需 Canvas) |
| 样式控制 | 使用 CSS 子集定义样式 | 无法修改 PDF 内部样式 |
| 字体支持 | 需手动注册 TTF/OTF | 自动解析嵌入字体 |
| 输出产物 | Blob, Buffer, Stream | React 组件 (Canvas DOM) |
| 典型场景 | 发票、报表导出、证书生成 | 文档阅读器、合同预览 |
不要试图用一个库解决所有问题。
如果你的需求是**“让用户下载一个文件”,或者“在服务器上自动化生成文档”**,请坚定选择 @react-pdf/renderer。它是目前 React 生态中生成 PDF 的事实标准,避免了引入沉重的无头浏览器方案。
如果你的需求是**“让用户在网页上看文件”**,请坚定选择 react-pdf。不要尝试用 HTML/CSS 去模拟 PDF 布局,也不要用 @react-pdf/renderer 生成后再转图片展示,那样会丢失文本可选择性和缩放清晰度。
高级用法:在复杂的 SaaS 应用中,通常两者并存。后端或边缘函数使用 @react-pdf/renderer 生成归档文件,前端使用 react-pdf 提供流畅的预览体验。理解它们的边界,能让你的文档处理架构更加清晰、高效。
选择 @react-pdf/renderer 如果你的需求是动态生成 PDF 文件,例如发票、报告、简历或导出功能。它适合需要在服务端(Node.js)无头生成 PDF,或者需要在客户端将 React 界面直接转换为 PDF 下载的场景。如果你需要完全控制文档的布局、字体嵌入和内容结构,且不希望依赖浏览器的打印对话框,这是唯一的选择。
选择 react-pdf 如果你的目标是让用户在网页应用中预览、查看或交互已有的 PDF 文件(如用户上传的合同、产品手册)。它适合构建文档阅读器、在线预览功能或需要自定义 PDF 查看器 UI(如工具栏、缩略图)的场景。注意,它不能用来创建新的 PDF 文件,仅用于渲染现有文件。
React renderer for creating PDF files on the browser and server
yarn add @react-pdf/renderer
import React from 'react';
import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer';
// Create styles
const styles = StyleSheet.create({
page: {
flexDirection: 'row',
backgroundColor: '#E4E4E4',
},
section: {
margin: 10,
padding: 10,
flexGrow: 1,
},
});
// Create Document Component
const MyDocument = () => (
<Document>
<Page size="A4" style={styles.page}>
<View style={styles.section}>
<Text>Section #1</Text>
</View>
<View style={styles.section}>
<Text>Section #2</Text>
</View>
</Page>
</Document>
);
Web. Render in DOMimport React from 'react';
import ReactDOM from 'react-dom';
import { PDFViewer } from '@react-pdf/renderer';
const App = () => (
<PDFViewer>
<MyDocument />
</PDFViewer>
);
ReactDOM.render(<App />, document.getElementById('root'));
Node. Save in a fileimport React from 'react';
import ReactPDF from '@react-pdf/renderer';
ReactPDF.render(<MyDocument />, `${__dirname}/example.pdf`);
This project exists thanks to all the people who contribute. Looking to contribute? Please check our [contribute] document for more details about how to setup a development environment and submitting code.
Thank you to all our sponsors! [Become a sponsors]
Thank you to all our backers! [Become a backer]
MIT © Diego Muracciole