乐趣区

关于前端:🌟前端分页加载懒加载预览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 预览进去。也是平时咱们用的比拟多的一种形式。

但我这是一个 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 文件如何更快的预览的形式,如果你有什么乏味的形式,欢送在评论区一起脑暴探讨。感谢您的浏览,喜爱的话点个关注点个赞吧~

退出移动版