JS分別利用分頁加載和懶加載預(yù)覽PDF
前言
在開發(fā)過程中,我們經(jīng)常會遇到預(yù)覽PDF的需求。這個時候我們一般是會有一個PDF的地址,然后通過一些手段將PDF文件預(yù)覽在頁面上。比如說以下的方式或者第三方庫:
embed標簽iframe標簽pdf.jsreact-pdf
但是當PDF比較大的時候,加載速度緩慢以及占用內(nèi)存過大會是一個比較嚴重的問題。PDF體積較大時,網(wǎng)絡(luò)傳輸?shù)臅r間必然會較長,這樣用戶的體驗會比較差;當打開較大的PDF時,會占用較多的內(nèi)存,可能會導(dǎo)致標簽頁/瀏覽器卡死或者崩潰。
常規(guī)預(yù)覽方式
先來看看常規(guī)的PDF預(yù)覽方式,這里簡單提兩種預(yù)覽方式,都是十分簡單的,代碼如下:
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標簽去預(yù)覽,另一種是用iframe標簽去預(yù)覽,這里都是借助于瀏覽器自帶的PDF渲染引擎去把PDF預(yù)覽出來。也是平時我們用的比較多的一種方式。

但我這是一個15M左右,100頁+的PDF,在我4M帶寬的服務(wù)器下,傳輸時間需要大概30秒左右,時間還是相當長的。
所以下面要介紹的是PDF的分頁加載和渲染。旨在解決大PDF網(wǎng)絡(luò)傳輸過慢以及占用內(nèi)存過大的問題。
按圖片分割
既然一次性加載整個PDF文件太過緩慢,那我們可以考慮把它拆分成一個個更細的單元去進行預(yù)覽。這里我可以先把一整個PDF文件按頁碼轉(zhuǎn)成一張張圖片,1頁PDF對應(yīng)1張圖片。那么經(jīng)過這樣的預(yù)處理之后,我們就很容易能夠做到按需加載、或者分片加載,而不是一次性加載一整個PDF文件。這樣用戶的首屏等待時間會大大降低,可以提升用戶體驗。
PDF轉(zhuǎn)圖片
這里需要先預(yù)處理一下數(shù)據(jù),將PDF文件轉(zhuǎn)換為圖片。我這里使用的是 pdf-image 這個庫,具體的安裝教程可以點擊鏈接查看。
安裝完之后可以用node寫一個這樣的預(yù)處理腳本,將所需要處理的PDF文件的每一頁都轉(zhuǎn)換成一張圖片:
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, // 提高分辨率,根據(jù)需要調(diào)整
"-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();

拿到了每一頁的圖片之后,我們就可以拿這些圖片去做預(yù)覽功能了。這里有100多張圖片,渲染的時候不需要一次性將它們渲染出來,可以做一下懶加載。這里我是使用IntersectionObserver去實現(xiàn)了圖片的懶加載。先實現(xiàn)一個懶加載的hook如下:
- 這里可以先給每一個元素一個默認的高度,然后利用
IntersectionObserver監(jiān)聽元素是否出現(xiàn)在視口范圍內(nèi) - 如果出現(xiàn)在視口返回內(nèi),就加載這張圖片
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屬性并且取消監(jiān)視
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", // 監(jiān)視區(qū)向下拓展200px
});
addObserve();
}, []);
useEffect(() => {
initObserver();
return () => {
observer.current.disconnect();
};
}, []);
};
export default useObserve;
然后實現(xiàn)預(yù)覽組件如下:
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`}>
{/* 設(shè)置一個默認的缺省圖,避免在加載過程中出現(xiàn)白屏的現(xiàn)象 */}
<img
class={styles["image-item"]}
src={defaultImage}
data-src={item.originSrc}
key={index}
width="100%"
/>
</div>
);
})}
</div>
</div>
);
};
export default Preview;
這樣就實現(xiàn)了按需加載預(yù)覽PDF的功能

按頁分割
上面介紹的是把PDF轉(zhuǎn)成圖片之后,利用加載多圖的方式來做PDF的懶加載。下面這種方式是,把一個大的PDF切分成若干個小的PDF,再使用常規(guī)的預(yù)覽方式去預(yù)覽。首先還是寫一個腳本來預(yù)處理數(shù)據(jù),切分PDF文件。
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);
切分后的結(jié)果如下:

然后預(yù)覽的時候就十分簡單,我是直接用iframe去一份份的預(yù)覽,點擊加載更多再去加載下一個片段。一個簡單的預(yù)覽組件如下,一個個地加載我們分割的PDF文件:
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;
以上就是JS分別利用分頁加載和懶加載預(yù)覽PDF的詳細內(nèi)容,更多關(guān)于JS預(yù)覽PDF的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
BootStrap表單驗證 FormValidation 調(diào)整反饋圖標位置的實例代碼
這篇文章主要介紹了BootStrap表單驗證 FormValidation 調(diào)整反饋圖標位置的實例代碼,需要的朋友可以參考下2017-05-05
使用濾鏡設(shè)置透明導(dǎo)致 IE 6/7/8/9 解析異常的解決方法
使用濾鏡設(shè)置透明導(dǎo)致 IE 6/7/8/9 解析異常的解決方法,需要的朋友可以參考下。2011-04-04
JavaScript Html實現(xiàn)移動端紅包雨功能頁面
這篇文章主要為大家詳細介紹了JavaScript Html實現(xiàn)移動端紅包雨功能頁面,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-01-01

