前言
公众号:【可乐前端】,期待关注交换,分享一些有意思的前端常识
在开发过程中,咱们常常会遇到预览PDF
的需要。这个时候咱们个别是会有一个PDF
的地址,而后通过一些伎俩将PDF文件预览在页面上。比如说以下的形式或者第三方库:
embed
标签iframe
标签pdf.js
react-pdf
然而当PDF
比拟大的时候,加载速度迟缓以及占用内存过大会是一个比较严重的问题。PDF
体积较大时,网络传输的工夫必然会较长,这样用户的体验会比拟差;当关上较大的PDF
时,会占用较多的内存,可能会导致标签页/浏览器卡死或者解体。
惯例预览形式
先来看看惯例的PDF
预览形式,这里简略提两种预览形式,都是非常简略的,代码如下:
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
文件的每一页都转换成一张图片:
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
监听元素是否呈现在视口范畴内 - 如果呈现在视口返回内,就加载这张图片
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;
而后实现预览组件如下:
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
文件。
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文件:
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
文件如何更快的预览的形式,如果你有什么乏味的形式,欢送在评论区一起脑暴探讨。感谢您的浏览,喜爱的话点个关注点个赞吧~