🌟前端分页加载/懒加载预览PDF🌟

简介: 🌟前端分页加载/懒加载预览PDF🌟

前言

在开发过程中,我们经常会遇到预览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预览出来。也是平时我们用的比较多的一种方式。 44.png

但我这是一个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();

45.png

拿到了每一页的图片之后,我们就可以拿这些图片去做预览功能了。这里有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的功能 47.png

按页分割

上面介绍的是把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);

切分后的结果如下: 56.png

然后预览的时候就十分简单,我是直接用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文件如何更快的预览的方式,如果你有什么有趣的方式,欢迎在评论区一起脑暴讨论。感谢您的阅读,喜欢的话点个关注点个赞吧~

相关文章
|
1月前
|
前端开发
PDF文件上传转成base64编码并支持预览
PDF文件上传转成base64编码并支持预览
120 12
|
2月前
|
缓存 前端开发 JavaScript
探索前端性能优化:从加载速度到用户体验的全面升级
探索前端性能优化:从加载速度到用户体验的全面升级
56 0
|
18天前
|
缓存 前端开发 JavaScript
前端性能优化:提升网页加载速度的10个技巧
【10月更文挑战第25天】在互联网时代,网页加载速度直接影响用户体验和搜索引擎排名。本文介绍了10个提升网页加载速度的技巧,包括减少HTTP请求、启用压缩、使用CDN、延迟加载非关键资源、优化图片、减少重定向、使用浏览器缓存、优化CSS和JavaScript、异步加载JavaScript以及代码分割。通过这些方法,可以显著提高网页性能,改善用户体验。
59 5
|
21天前
|
前端开发 JavaScript
除了 jsPDF,还有哪些前端库可以用于生成 PDF?
【10月更文挑战第21天】这些前端库都有各自的特点和优势,你可以根据具体的项目需求、技术栈以及对功能的要求来选择合适的库。不同的库在使用方法、性能表现以及功能支持上可能会有所差异,需要根据实际情况进行评估和选择。
|
21天前
|
前端开发 API
前端界面生成PDF并导出下载
【10月更文挑战第21天】利用合适的第三方库,你可以在前端轻松实现界面生成 PDF 并导出下载的功能,为用户提供更方便的文档分享和保存方式。你还可以根据具体的需求进一步优化和定制生成的 PDF 文件,以满足不同的业务场景要求。
|
3月前
|
Web App开发 iOS开发 容器
Vue3PDF预览(vue3-pdf-app)
`vue3-pdf-app` 插件提供了一个简单而强大的 PDF 预览解决方案。通过 `&lt;a&gt;` 标签即可快速预览 PDF 文件。为满足更复杂的定制需求,提供了 `PDFViewer.vue` 组件,基于 `vue3-pdf-app@1.0.3` 封装,支持多种功能如缩放、旋转、全屏预览、打印等,并可自定义主题颜色与语言。组件属性包括文件地址 (`src`)、预览容器尺寸 (`width`, `height`)、默认缩放规则 (`pageScale`) 和主题 (`theme`) 等。适用于多种浏览器,方便集成到项目中。
553 2
Vue3PDF预览(vue3-pdf-app)
|
3月前
|
缓存 前端开发 JavaScript
微前端集成优化:让所有子应用体积更小,加载更快!
【8月更文挑战第17天】微前端集成优化:让所有子应用体积更小,加载更快!
68 1
微前端集成优化:让所有子应用体积更小,加载更快!
|
2月前
|
缓存 边缘计算 前端开发
关于前端性能优化问题,认识网页加载过程和防抖节流
该文章详细探讨了前端性能优化的方法,包括理解网页加载过程、实施防抖和节流技术来提升用户体验和性能。
|
2月前
|
移动开发 前端开发 JavaScript
使用html-to-image代替html2canvas,结合jspdf实现下载pdf(下载截图下载前端dom元素)
本文介绍了在前端项目中,当使用`html2canvas`遇到问题时,如何使用`html-to-image`库作为替代方案,结合`jspdf`实现将DOM元素生成为PDF文件并提供下载。文章首先讨论了`html2canvas`可能遇到的问题,并提供了该库的使用示例代码。随后,详细介绍了`html-to-image`库的安装和使用方法,展示了如何将DOM元素转换为Canvas,再利用`jspdf`生成PDF文件。最后,文章通过示例代码说明了整个转换和下载的过程,并展示了效果截图。
89 0
|
3月前
|
移动开发 资源调度 JavaScript
Vue移动端网页(H5)预览pdf文件(pdfh5和vue-pdf)
这篇文章介绍了在Vue移动端网页中使用`pdfh5`和`vue-pdf`两个插件来实现PDF文件的预览,包括滚动查看、缩放、添加水印、分页加载、跳转指定页数等功能。
2665 0
Vue移动端网页(H5)预览pdf文件(pdfh5和vue-pdf)