前言
在开发过程中,我们经常会遇到预览PDF
的需求。这个时候我们一般是会有一个PDF
的地址,然后通过一些手段将PDF文件预览在页面上。比如说以下的方式或者第三方库:
embed
标签iframe
标签pdf.js
react-pdf
但是当PDF
比较大的时候,加载速度缓慢以及占用内存过大会是一个比较严重的问题。PDF
体积较大时,网络传输的时间必然会较长,这样用户的体验会比较差;当打开较大的PDF
时,会占用较多的内存,可能会导致标签页/浏览器卡死或者崩溃。
常规预览方式
先来看看常规的PDF
预览方式,这里简单提两种预览方式,都是十分简单的,代码如下:
js
import { PDF_URL } from "./constant"
const Normal = () => {
return (
<div>
<embed src={PDF_URL} type="application/pdf" width="100%" height="600px"></embed>
<iframe width={'100%'} height={600} src={PDF_URL} />
</div>
)
}
export default Normal
一种使用embed
标签去预览,另一种是用iframe
标签去预览,这里都是借助于浏览器自带的PDF
渲染引擎去把PDF
预览出来。也是平时我们用的比较多的一种方式。
但我这是一个15M
左右,100页+
的PDF
,在我4M
带宽的服务器下,传输时间需要大概30秒
左右,时间还是相当长的。
所以下面要介绍的是PDF
的分页加载和渲染。旨在解决大PDF
网络传输过慢以及占用内存过大的问题。
按图片分割
既然一次性加载整个PDF
文件太过缓慢,那我们可以考虑把它拆分成一个个更细的单元去进行预览。这里我可以先把一整个PDF
文件按页码转成一张张图片,1页PDF
对应1张图片。那么经过这样的预处理之后,我们就很容易能够做到按需加载、或者分片加载,而不是一次性加载一整个PDF
文件。这样用户的首屏等待时间会大大降低,可以提升用户体验。
PDF
转图片
这里需要先预处理一下数据,将PDF文件转换为图片。我这里使用的是 pdf-image 这个库,具体的安装教程可以点击链接查看。
安装完之后可以用node
写一个这样的预处理脚本,将所需要处理的PDF
文件的每一页都转换成一张图片:
js
const PDFImage = require("pdf-image").PDFImage;
const path = require("path");
const fs = require("fs");
const FILE_PATH = path.join(__dirname, "../public/files/test.pdf");
const outputDirectory = path.join(__dirname, "../public/files/tmp");
const pdfImage = new PDFImage(FILE_PATH, {
convertOptions: {
"-density": 250, // 提高分辨率,根据需要调整
"-quality": 100,
"-trim": null,
},
outputDirectory,
});
const run = async () => {
const info = await pdfImage.getInfo();
for (let i = 0; i < Number(info.Pages); i++) {
try {
await pdfImage.convertPage(i);
} catch (error) {
console.log("error", error);
}
}
};
run();
拿到了每一页的图片之后,我们就可以拿这些图片去做预览功能了。这里有100多张图片,渲染的时候不需要一次性将它们渲染出来,可以做一下懒加载。这里我是使用IntersectionObserver
去实现了图片的懒加载。先实现一个懒加载的hook
如下:
- 这里可以先给每一个元素一个默认的高度,然后利用
IntersectionObserver
监听元素是否出现在视口范围内 - 如果出现在视口返回内,就加载这张图片
js
import { useEffect,useRef,useCallback } from "react";
const useObserve = ({ rootRef, itemSelector }) => {
const observer = useRef(null);
const handlerObserve = entries => {
entries.forEach(({ isIntersecting, target }) => {
if (isIntersecting) {
const targetImg = target.children[0];
targetImg.src = targetImg.dataset.src;
// 修改过src属性之后,即可移除data-src属性并且取消监视
targetImg.removeAttribute("data-src");
observer.current.unobserve(target);
}
});
};
const addObserve = () => {
const list = document.querySelectorAll(itemSelector);
list.forEach(item => {
observer.current.observe(item);
});
};
const initObserver = useCallback(() => {
observer.current = new IntersectionObserver(handlerObserve, {
root: rootRef.current,
rootMargin: "0px 0px 200px 0px", // 监视区向下拓展200px
});
addObserve();
}, []);
useEffect(() => {
initObserver();
return () => {
observer.current.disconnect();
};
}, []);
};
export default useObserve;
然后实现预览组件如下:
js
import { Carousel } from "antd";
import styles from "./index.module.less";
import { LeftOutlined, RightOutlined } from "@ant-design/icons";
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
import { API_PREFIX } from "../../../../env";
import useObserve from "./useObserve";
const defaultImage = "";
const Preview = () => {
const [list, setList] = useState(() => {
return new Array(105).fill(0).map((item, index) => {
return {
page: index,
originSrc: `${API_PREFIX}/files/tmp/test-${index}.png`,
};
});
});
const contentRef = useRef(null);
useObserve({
rootRef: contentRef,
itemSelector: ".image-item-observe",
});
return (
<div className={styles.preview}>
<div ref={contentRef} className={styles.content}>
{list.map((item, index) => {
return (
<div class={`${styles["image-item-wrapper"]} image-item-observe`}>
{/* 设置一个默认的缺省图,避免在加载过程中出现白屏的现象 */}
<img
class={styles["image-item"]}
src={defaultImage}
data-src={item.originSrc}
key={index}
width="100%"
/>
</div>
);
})}
</div>
</div>
);
};
export default Preview;
这样就实现了按需加载预览PDF的功能
按页分割
上面介绍的是把PDF
转成图片之后,利用加载多图的方式来做PDF
的懒加载。下面这种方式是,把一个大的PDF
切分成若干个小的PDF
,再使用常规的预览方式去预览。首先还是写一个脚本来预处理数据,切分PDF
文件。
js
const fs = require("fs");
const { PDFDocument } = require("pdf-lib");
const path = require("path");
async function splitPDF(inputPath, outputPrefix) {
const pdfBytes = await fs.promises.readFile(inputPath);
const pdfDoc = await PDFDocument.load(pdfBytes);
const totalPages = pdfDoc.getPageCount();
const pagesPerChunk = 10;
for (let startPage = 1; startPage <= totalPages; startPage += pagesPerChunk) {
const endPage = Math.min(startPage + pagesPerChunk - 1, totalPages);
const newPdfDoc = await PDFDocument.create();
for (let pageNum = startPage; pageNum <= endPage; pageNum++) {
const [copiedPage] = await newPdfDoc.copyPages(pdfDoc, [pageNum - 1]);
newPdfDoc.addPage(copiedPage);
}
const outputPath = `${outputPrefix}/${Math.floor(endPage / 10)}.pdf`;
const newPdfBytes = await newPdfDoc.save();
await fs.promises.writeFile(outputPath, newPdfBytes);
console.log(`PDF successfully split from page ${startPage} to ${endPage}.`);
}
}
const FILE_PATH = path.join(__dirname, "../public/files/test.pdf");
const outputDirectory = path.join(__dirname, "../public/files/tmp-file");
splitPDF(FILE_PATH, outputDirectory);
切分后的结果如下:
然后预览的时候就十分简单,我是直接用iframe
去一份份的预览,点击加载更多再去加载下一个片段。一个简单的预览组件如下,一个个地加载我们分割的PDF文件:
js
import React, { useState, useEffect, useRef } from "react";
import { API_PREFIX } from "../../../../env";
const pdfs = new Array(11)
.fill(0)
.map((_, index) => `${API_PREFIX}/files/tmp-file/${index + 1}.pdf`);
const Split = () => {
const [list, setList] = useState([pdfs[0]]);
return (
<div>
{list.map((url, index) => {
return (
<div>
<iframe
id={`iframe-${index}`}
key={index}
width={"100%"}
height={600}
src={url}
/>
</div>
);
})}
<button
disabled={list.length === pdfs.length}
onClick={() => {
const arr = [...list];
arr.push(pdfs[arr.length]);
setList(arr)
}}
>
加载更多
</button>
</div>
);
};
export default Split;
最后
上文介绍了两种大PDF
文件如何更快的预览的方式,如果你有什么有趣的方式,欢迎在评论区一起脑暴讨论。感谢您的阅读,喜欢的话点个关注点个赞吧~