React 实现 PDF 文件在线预览 - 手把手教你写 React PDF 预览功能

完整教程:《React 实现 PDF 文件在线预览 - 手把手教你写 React PDF 预览功能》
React 实现 PDF 文件在线预览
- 快速搭建应用
- 开发预览组件用于展示 PDF 的第一页内容
- 开发预览组件用于完整展示 PDF 并实现翻页功能
- 选择 PDF 文本块
- 在线查看 PDF 源代码块
- 构建基于 React 的 PDFjs 框架并实现云服务功能
对于开发中的 React 项目而言,在多种应用场景下都不可避免地需要 PDF 文件的在线查看功能。这些应用场景包括合同 ERP、销售 CRM 和内部文档 CMS 管理系统等。本篇文章将详细指导读者如何将 PDF 预览组件集成到 React 应用中,并实现一系列常见的操作流程。
完成本教程的学习后,你会搭建出以下 PDF 在线查看效果的 React PDF 预览组件 https://kalacloud.com/blog/how-build-react-pdf-viewer-pdfjs/

当你需要构建后端管理系统而不必涉及前端开发时,请考虑使用卡拉云这款新兴的低代码平台
本次教程中使用的技术栈
- Vite
- React
- Typescript
- pdf.js
快速搭建项目
> yarn create vite pdf-preview --template react-ts
现在我们安装下 pdf.js
官网并未提供关于 npm 下载方式的具体指导。
因此,在这种情况下,默认安装/umd版本是常见的做法。
实际上,在使用一个库时,默认会查看文档和官方示例并不是唯一的最佳实践。
从源代码中的 examples/webpack/main.js 文件中可以看到 pdfjs-dist 这个 npm 包。
我们可以进行下载。
然后按照自己的习惯组织下文件目录
.
├── components
│ └── PDFRender
│ └── index.tsx
├── main.tsx
├── App.tsx
└── vite-env.d.ts
了解五款开源React移动端UI组件库的测评与推荐 https://kalacloud.com/blog/best-react-mobile-ui-component-libraries/
渲染第一页 - React 开发预览组件
为了简化流程和提高效率,在开发过程中我打算创建一个新的PDFRender组件,并负责将PDF文件的第一页内容进行展示。
import * as pdf from 'pdfjs-dist'
import pdfWorker from 'pdfjs-dist/build/pdf.worker.js?url'
import React, { useLayoutEffect, useRef } from "react";
pdf.GlobalWorkerOptions.workerSrc = pdfWorker;
export const PDFRender: React.FC<{ src: string }> = (props) => {
const canvasRef = useRef<HTMLCanvasElement | null>(null)
useLayoutEffect(() => {
pdf
.getDocument(props.src)
.promise
.then(pdfDocument => {
return pdfDocument.getPage(1);
})
.then((pdfPage) => {
const viewport = pdfPage.getViewport({ scale: 1.0 });
const canvas = canvasRef.current;
if (!canvas) {
return Promise.reject()
}
canvas.width = viewport.width
canvas.height = viewport.height;
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D
const renderTask = pdfPage.render({
canvasContext: ctx,
viewport,
});
return renderTask.promise;
})
.catch(err => {
console.log(err)
})
}, [])
return (
<canvas ref={canvasRef}/>
)
}

细心的同学可能发现了这两行代码
import pdfWorker from 'pdfjs-dist/build/pdf.worker.js?url'
pdf.GlobalWorkerOptions.workerSrc = pdfWorker;
由于PDF的交互可能会造成JavaScript阻塞问题,因此PDF.js采用了Web Worker技术来提升性能
最后我们使用下这个组件,看下效果
import { PDFRender } from "./components/PDFRender";
const pdfFilePath = '/kalacloud-demo.pdf'
export const App = () => {
return (
<PDFRender src={pdfFilePath} />
)
}
效果如下

代码简单讲解下
- 调用 getDocument 函数获取 pdf 文档内容
- 调用 getPage 方法获取当前页面内容
- 利用 canvas 绘制当前页面
深入学习推荐:《优质主流 React UI 组件库评测指南》
渲染整个 PDF 并翻页 - React 开发预览组件
采用该方案实现页面渲染不复杂。参考之前的方法确定页数后,通过循环结构进行页面渲染。
import * as pdf from 'pdfjs-dist'
import pdfWorker from 'pdfjs-dist/build/pdf.worker.js?url'
import { useEffect, useRef, useState } from "react";
pdf.GlobalWorkerOptions.workerSrc = pdfWorker;
export const usePDFData = (options: { src: string, scale?: number }) => {
const previewUrls = useRef<string[]>([])
const urls = useRef<string[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
urls.current = []
setLoading(true)
;(async () => {
// 这里千万别解构,会导致 this 指向错误
const pdfDocument = await pdf.getDocument(options.src).promise
const task = new Array(pdfDocument.numPages).fill(null)
await Promise.all(task.map(async (_, i) => {
const page = await pdfDocument.getPage(i + 1)
const viewport = page.getViewport({ scale: options.scale || 2 })
const canvas = document.createElement('canvas')
canvas.width = viewport.width
canvas.height = viewport.height
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D
const renderTask = page.render({
canvasContext: ctx,
viewport,
});
await renderTask.promise;
// 分别获取不同尺寸的图片,一个用来预览一个用来展示
urls.current[i] = canvas.toDataURL('image/jpeg', 1)
previewUrls.current[i] = canvas.toDataURL('image/jpeg', 0.5)
}))
setLoading(false)
})()
}, [options.src])
return {
loading,
urls: urls.current,
previewUrls: previewUrls.current,
}
}

接下来我们实现滚动翻页功能
- 点击对应页滚动到指定的位置
- 滚动到对应位置,高亮当前页
先看下最终的效果

为了实现点击滚动到对应的位置这一功能, 这一过程非常简便, 可以通过调用scrollIntoView方法快速定位至所需位置
const goPage = (i: number) => {
setCurrentPage(i)
document.querySelectorAll('.page')[i]!.scrollIntoView({ behavior: 'smooth' })
}
再来实现下滚动位置自动高亮页数
主要通过 IntersectionObserver api 实现功能,并对其结果进行判断以确定当前页面。具体来说,在检测到某页面的显示状态时(即该页面在视口中可被看到),若其可见性达到或超过50%,则认为当前页面有一半的内容处于显示范围内。
const io = useRef(new IntersectionObserver((entries) => {
entries.forEach(item => {
item.intersectionRatio >= 0.5 && setCurrentPage(Number(item.target.getAttribute('index')))
})
}, {
threshold: [0.5]
}))
作为专业的技术平台,KalaCloud公司推出了其旗舰级产品——《最佳React后台管理系统架构》,该软件旨在为开发者打造高效、安全的行政管理解决方案,并提供了丰富的功能模块以满足企业需求
PDF 文本选择
在某些特定情况下(即特殊场景),可能会有必要支持用户复制PDF上的文字内容。显而易见的是,在图片中无法直接选择文字进行操作。然而,强大的 pdf.js 工具能够提供相同的文字位置来绘制内容,并且我们计划实现这一功能。下一步我们将实现这一目标并完成相关功能开发工作
import * as pdf from 'pdfjs-dist'
import pdfWorker from 'pdfjs-dist/build/pdf.worker.js?url'
+ import { TextLayerBuilder } from 'pdfjs-dist/web/pdf_viewer';
+ import 'pdfjs-dist/web/pdf_viewer.css';
import { useEffect, useRef, useState } from "react";
pdf.GlobalWorkerOptions.workerSrc = pdfWorker;
export const usePDFData = (options: { src: string, scale?: number }) => {
const previewUrls = useRef<string[]>([])
const pages = useRef<{ canvas: HTMLCanvasElement, text: HTMLDivElement }[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
pages.current = []
setLoading(true)
;(async () => {
const pdfDocument = await pdf.getDocument(options.src).promise
const task = new Array(pdfDocument.numPages).fill(null)
await Promise.all(task.map(async (_, i) => {
const page = await pdfDocument.getPage(i + 1)
const viewport = page.getViewport({ scale: options.scale || 2 })
const canvas = document.createElement('canvas')
canvas.width = viewport.width
canvas.height = viewport.height
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D
const renderTask = page.render({
canvasContext: ctx,
viewport,
});
await renderTask.promise;
previewUrls.current[i] = canvas.toDataURL('image/jpeg', 0.5)
+ const textContent = await page.getTextContent()
+ const textLayerDiv = document.createElement('div');
+ textLayerDiv.setAttribute('class', 'textLayer');
+ const textLayer = new TextLayerBuilder({
+ textLayerDiv,
+ pageIndex: i + 1,
+ viewport,
+ eventBus: undefined
+ });
+
+ textLayer.setTextContent(textContent);
+ textLayer.render();
pages.current[i] = {
canvas,
text: textLayerDiv
}
}))
setLoading(false)
})()
}, [options.src])
return {
loading,
pages: pages.current,
previewUrls: previewUrls.current,
}
}

进一步了解React Echarts 使用教程 - 如何在 React 加入图表
React PDF 在线预览源代码
请访问 github 以查看本次教程的源代码
假如你只需要预览 PDF 并且不关心浏览器兼容,那么使用 embed 只需要一行代码就能实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<style>
* {
padding: 0;
margin: 0;
}
body {
height: 100%;
width: 100%;
overflow: hidden;
background-color: rgb(82, 86, 89);
}
embed {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<embed src="/kalacloud-demo.pdf" type="application/pdf">
</body>
</html>

拓展阅读:《优质开源 React Table 表格组件评测指南》
React PDFjs 搭建总结及卡拉云
本文阐述了在React框架中实现PDF预览功能的方法。若不关注前端开发,则可借助现成工具来简化工作流程,减少开发负担。具体而言,我们强烈推荐使用卡拉云,该平台集成了丰富多样的组件,用户无需掌握任何前端知识,只需通过拖拽即可快速搭建完善的解决方案。
卡拉云能够帮助您迅速构建企业内部工具体系。这一图展示了使用卡拉云搭建的内部广告投放监测系统,请注意:无需前端开发知识即可操作;完成整个搭建过程仅需十分钟的时间。另外一种选择是快速构建专属于您的后端管理系统。(https://kalacloud.com/)

扩展阅读:
- 完整解析React Router 6(React路由):最详尽学习指南
- 全栈开发实践:基于React与Node.js构建上传图片/预览功能的管理后台系统
- 深入解析React Draggable组件:最全面拖拽功能实现技巧
- 推荐盘点:当前最值得选用的8款 React Datepicker时间日期选择器组件测评与对比分析
- Mastering React Form表单验证技术:从入门到精通的详细指南
