@react-pdf/renderer vs react-pdf
在 React 应用中生成与展示 PDF 文档的技术选型
@react-pdf/rendererreact-pdf类似的npm包:

在 React 应用中生成与展示 PDF 文档的技术选型

@react-pdf/renderer 和 react-pdf 都是 React 生态中处理 PDF 的核心库,但它们的职责截然不同。@react-pdf/renderer 是一个渲染引擎,允许开发者使用 React 组件和样式来从头构建 PDF 文件,它不依赖浏览器原生能力,可直接在 Node.js 或服务端生成二进制 PDF 流。而 react-pdf 是一个基于 Mozilla pdf.js 的查看器组件,专为在浏览器中渲染和展示现有的 PDF 文件而设计,提供缩放、翻页等交互功能。简而言之,前者用于“写”PDF,后者用于“读”PDF。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
@react-pdf/renderer016,840320 kB3552 个月前MIT
react-pdf011,193337 kB171 个月前MIT

@react-pdf/renderer vs react-pdf: 生成与查看的本质区别

在 React 开发生态中,处理 PDF 通常涉及两个完全不同的阶段:创建文档和展示文档。@react-pdf/renderer 和 react-pdf 分别占据了这两个生态位。很多开发者容易混淆二者,试图用查看器去生成文件,或用生成器去展示复杂交互,这会导致架构上的严重失误。让我们深入技术细节,看看它们如何工作以及如何正确选型。

🛠️ 核心定位:创造者 vs 观察者

@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>
  );
}

📐 样式与布局:CSS 子集 vs 原生渲染

@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>

🌐 运行环境:服务端友好 vs 浏览器依赖

@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" />;
}

🔤 字体处理:嵌入 vs 解析

@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;

// 之后正常渲染即可,库会自动处理字符映射

⚡ 性能考量:生成开销 vs 渲染开销

@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 在模态框中预览该文件,确认无误后再下载或发送。

场景:动态报告生成与预览

  1. 生成阶段:利用 @react-pdf/renderer 根据用户数据动态创建 PDF Blob。
  2. 预览阶段:将生成的 Blob URL 传递给 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/rendererreact-pdf
核心用途创建 PDF 文件查看 PDF 文件
底层依赖自研渲染引擎 (无依赖)Mozilla pdf.js
运行环境Node.js, 浏览器, Serverless主要是浏览器 (需 Canvas)
样式控制使用 CSS 子集定义样式无法修改 PDF 内部样式
字体支持需手动注册 TTF/OTF自动解析嵌入字体
输出产物Blob, Buffer, StreamReact 组件 (Canvas DOM)
典型场景发票、报表导出、证书生成文档阅读器、合同预览

💡 架构师建议

不要试图用一个库解决所有问题。

  • 如果你的需求是**“让用户下载一个文件”,或者“在服务器上自动化生成文档”**,请坚定选择 @react-pdf/renderer。它是目前 React 生态中生成 PDF 的事实标准,避免了引入沉重的无头浏览器方案。

  • 如果你的需求是**“让用户在网页上看文件”**,请坚定选择 react-pdf。不要尝试用 HTML/CSS 去模拟 PDF 布局,也不要用 @react-pdf/renderer 生成后再转图片展示,那样会丢失文本可选择性和缩放清晰度。

  • 高级用法:在复杂的 SaaS 应用中,通常两者并存。后端或边缘函数使用 @react-pdf/renderer 生成归档文件,前端使用 react-pdf 提供流畅的预览体验。理解它们的边界,能让你的文档处理架构更加清晰、高效。

如何选择: @react-pdf/renderer vs react-pdf

  • @react-pdf/renderer:

    选择 @react-pdf/renderer 如果你的需求是动态生成 PDF 文件,例如发票、报告、简历或导出功能。它适合需要在服务端(Node.js)无头生成 PDF,或者需要在客户端将 React 界面直接转换为 PDF 下载的场景。如果你需要完全控制文档的布局、字体嵌入和内容结构,且不希望依赖浏览器的打印对话框,这是唯一的选择。

  • react-pdf:

    选择 react-pdf 如果你的目标是让用户在网页应用中预览、查看或交互已有的 PDF 文件(如用户上传的合同、产品手册)。它适合构建文档阅读器、在线预览功能或需要自定义 PDF 查看器 UI(如工具栏、缩略图)的场景。注意,它不能用来创建新的 PDF 文件,仅用于渲染现有文件。

@react-pdf/renderer的README

React renderer for creating PDF files on the browser and server

How to install

yarn add @react-pdf/renderer

How it works

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 DOM

import 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 file

import React from 'react';
import ReactPDF from '@react-pdf/renderer';

ReactPDF.render(<MyDocument />, `${__dirname}/example.pdf`);

Contributors

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.

Sponsors

Thank you to all our sponsors! [Become a sponsors]

Backers

Thank you to all our backers! [Become a backer]

License

MIT © Diego Muracciole