在前端开发中,PDF 预览是个常见需求。简单粗暴的方案是用 <embed> 标签直接嵌入,但你有没有遇到过这样的问题:样式不好调、功能太单一、用户体验不够友好?今天,我要带你认识一个基于 react-pdf 的自定义 PDF 预览组件 PDFView,它不仅支持翻页、缩放、全屏,还能无缝集成到你的项目中。我们会拆解它的实现,对比 <embed> 的优劣,最后用一个 Demo 展示它的实力。准备好了吗?让我们一起把 PDF 预览玩出新花样吧!
为什么需要自定义 PDF 预览?
先说说需求场景。假设你有个文件管理系统,用户上传 PDF 后需要在线预览。你可能会直接写:
<embed src="file.pdf#toolbar=0" type="application/pdf" width="100%" height="700px" />
这行代码确实能用,但问题不少:
- 样式控制弱:背景、边框不好调整,工具栏难以隐藏。
- 交互性差:没有翻页按钮、缩放功能,用户体验一般。
- 功能单一:无法动态调整页面大小或全屏展示。
而我们的 PDFView 组件,基于 react-pdf,用 React 的方式解决问题,提供更灵活的控制和更优雅的体验。接下来,我们拆解它的代码,看看它是怎么“打败” <embed> 的!
核心代码拆解:从设计到实现
问题驱动开发
初版本的痛点:
- 性能瓶颈:大文件一次性加载全部页面,内存占用高,加载慢。
- 功能缺失:没有页面旋转,方向不对只能干瞪眼;没有多页预览,翻页全靠手动。
这些问题在实际场景中很常见。比如,用户上传一个 50 页的合同 PDF,如果加载卡顿,或者需要旋转查看签名页,原始版本就有点“力不从心”。优化后的 PDFView 将通过分页加载提升性能,新增旋转和缩略图功能,让体验飞起来!
优化后的核心实现
1. 性能优化:分页加载
- 问题:原始版本用 <Document> 一次性加载所有页面,大文件时容易卡顿。
- 解决:引入 loadedPages 状态(Set 类型),只加载当前页和用户访问过的页面。
- 实现:
- 初始化仅加载第 1 页。
- 用户翻页或跳转时动态添加加载页面。
- 缩略图模式下未加载页面显示占位符,点击时加载。
useEffect(() => {
if (pageNumber && !loadedPages.has(pageNumber)) {
setLoadedPages(prev => new Set(prev).add(pageNumber));
}
}, [pageNumber]);
2. 功能增强:页面旋转
- 需求:支持用户调整页面方向(比如横向文档)。
- 实现:
- 新增 rotation 状态,默认 0°。
- 提供 rotateLeft(-90°)和 rotateRight(+90°)函数。
- 通过 Page 组件的 rotate 属性应用旋转。
const rotateLeft = () => setRotation((prev) => (prev - 90) % 360);
const rotateRight = () => setRotation((prev) => (prev + 90) % 360);
3. 功能增强:多页预览
- 需求:用户想快速浏览所有页面,像缩略图一样。
- 实现:
- 新增 showThumbnails 状态,切换单页和缩略图模式。
- 缩略图模式下渲染所有页面(小尺寸),点击跳转到对应页。
{showThumbnails ? (
<div className={styles.thumbnailContainer}>
{Array.from({ length: numPages }, (_, i) => i + 1).map((page) => (
<div key={page} className={styles.thumbnail} onClick={() => { setPageNumber(page); setShowThumbnails(false); }}>
{loadedPages.has(page) ? (
<Page pageNumber={page} width={150} rotate={rotation} loading={<Spin />} />
) : (
<div className={styles.thumbnailPlaceholder}>加载中...</div>
)}
<span>第 {page} 页</span>
</div>
))}
</div>
) : (
<Page pageNumber={pageNumber} width={pageWidth} rotate={rotation} loading={<Spin size="large" />} />
)}
4. 按需加载:只渲染当前页
- 思路:用 visiblePages 控制渲染页面,初始只加载元信息,动态加载当前页。
- 实现:
- 移除 loadedPages,用 visiblePages 精确控制。
- 单页模式只渲染 pageNumber,缩略图模式限制前后几页。
useEffect(() => {
if (!showThumbnails) {
setVisiblePages([pageNumber]);
} else {
const start = Math.max(1, pageNumber - 2);
const end = Math.min(numPages, pageNumber + 2);
setVisiblePages(Array.from({ length: end - start + 1 }, (_, i) => start + i));
}
}, [pageNumber, showThumbnails, numPages]);