前端渲染大体积 多页面pdf

示例

介绍

首先使用的是 vue-pdf-embed渲染pdf,前段时间测试测出一个bug,当pdf文档页码过多(500页,7M)时,会出现页面卡死现象。首先尝试使用loading解决,css position解决,worker但是无济于事。然后开始研究vue-pdf-embed,发现内部是基于的pdfjs封装了一层,vue-pdf-embed内容是一次性渲染全部pdf。其实也是可以使用翻页的方式,那么就是一页一页的渲染,不会同时渲染很多页pdf。

技术栈

vue3 pdfjs

解决方案

有一个重要的点是:pdfjs 可以获取到pdf的总页数,以及每一页的pdf的高度。那么先使用一个div撑起父容器,然后滚动父容器,根据scrollTop/pdfHeight 获取到当前视口该展示哪一页 currentPage。

要解决的问题:向下滚动(相对简单),向上滚动,快速下拉,快速上拉

以下有5种方案,为什么会有这么多种方案呢,都是做着做着 发现更好的方案,以及一些pdfjs的限制问题。最终方案是最后2种。

  1. 1个canvas渲染,分别渲染前一张pdf的上半部分和后一张pdf的下半部分(放弃,pdfjs不允许使用同一个canvas重复渲染)
  2. 2个canvas渲染,例如先绘制第1页和第1页,然后currentPage到2的时候 删除2个canvas,第1个canvas绘制第2页,第2个canvas绘制第3页。以此类推。(放弃每次切换页数的时候,页面都会一闪一闪的)
  3. 多个canvas 弊端 向上滚动一页 全部要重新绘制
  4. 每滚动一页 将最上面的canvas删除 在最后面添加新的canvas,其中还有边界值需要处理,比如pdf本来就只有1页,pdf 到最后一页和首页。
  5. 维护一个数组 数组长度为3(或者5),数组中的每个值对应pageindex,currentPage变化后,更新数组,然后和dom对比,做diff操作。

方案5的好处,无须考虑那么多其他的,第4种方案还要解决快速拉动的问题。每次切换pageIndex,前面的canvas被删除掉了(其实不做删除操作也是可以的,因为每个canvas所在的位置是固定的,且不会重叠),后面的canvas新增了,不会让用户看见一闪的现象。

全部代码

js 复制代码
<template>
  <!-- Canvas 显示区域 -->
  <div
    v-loading="loading"
    class="canvas-container"
    ref="canvasContainer"
    @scroll.passive="handleScroll"
    :style="{ overflow: 'auto' }"
  >
    <div class="mark" :style="{ height: totalHeight + 'px' }"></div>
  </div>
</template>

<script setup>

import * as pdfjsLib from "pdfjs-dist/build/pdf";
import PdfWorker from "pdfjs-dist/build/pdf.worker.mjs?url";

import { onMounted, watch } from "vue";
pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker; // 使用woker

const props = defineProps({
  url: {
    type: String,
    required: true,
    default: "",
  },

});
const totalHeight = ref(0); // 总高度
const totalPages = ref(1); // 总页码
const pageHeight = ref(1); // 一页高度
let currentPage = 1; // 当前page
const loading = ref(false);
let pdfDoc = null; // 不能使用ref  会报错
let renderIndex = 0;
onMounted(() => {
  console.log(props.url, "props.url");
  if (props.url) {
    loadPdf(props.url, renderIndex);
  }
});

watch(
  () => props.url,
  (newVal, oldVal) => {
    if (canvasContainer.value) {
      canvasContainer.value.scrollTop = 0;
      let canvases = canvasContainer.value.querySelectorAll("canvas");
      canvases.forEach((item) => {
        canvasContainer.value.removeChild(item);
      });
    }
    if (newVal) {
      // console.log("newVal", newVal);
      renderIndex++;
      currentPage = 1;
      allCanvasIndex = [];
      loadPdf(newVal, renderIndex);
    }
  }
);
const emit = defineEmits(["error"]);
const canvasContainer = ref(null);

let allCanvas = [];
let allCanvasIndex = [];
// 一次性渲染多少张pdf
let canvasMaxNum = 2 + 1;
let t = 0;

let lastScrollTop = 0;
let renderPartialIndex = 0;
function getCurrentIndexs(nindex) {
  let currentIndexs = [];
  for (let i = 0; i < canvasMaxNum; i++) {
    if (nindex + i <= totalPages.value) {
      currentIndexs.push(nindex + i);
    }
  }
  return currentIndexs;
}
// 新增单个canvas
function addCanvas(npageIndex) {
  let ncanvas = document.createElement("canvas");
  ncanvas.classList = ["canvasshow"];
  canvasContainer.value.appendChild(ncanvas);

  ncanvas.id = "topCanvas" + npageIndex;
  ncanvas.style.top = `${pageHeight.value * (npageIndex - 1)}px`;
  renderPartialA(pdfDoc, npageIndex, ncanvas);
}
//监听滚动
function handleScroll(e) {
  // console.log(e,'eee');
  
  const canvasContainer0 = e.target;
  const scrollTop = canvasContainer0.scrollTop;
  emit("scroll", scrollTop);
  clearTimeout(t);
  // 防抖
  t = setTimeout(() => {
    if (totalPages.value <= canvasMaxNum) {
      return;
    }
    let page = scrollTop / pageHeight.value;
    let npage = Math.ceil(Math.max(1, page)); // 当前视图

    if (npage != currentPage) {
    // 获取视图应该展示的pageIndex
      let currentIndexs = getCurrentIndexs(npage);
      currentIndexs.forEach((item) => {
        if (!allCanvasIndex.includes(item)) {
          if (item > 0) {
            addCanvas(item);
          }
        }
      });
      //移除操作
      allCanvasIndex.forEach((item) => {
        if (!currentIndexs.includes(item)) {
          let canvas = canvasContainer.value.querySelector("#topCanvas" + item);
          if (canvas) {
            canvasContainer.value.removeChild(canvas);
          }
        }
      });
      allCanvasIndex = currentIndexs;
    }
    currentPage = npage;
  },50);
}

async function loadPdf(url, renderIndex1) {
  //   if (!url) return;
  try {
    loading.value = true;

    pdfDoc = await pdfjsLib.getDocument({ url: url }).promise;
    if (renderIndex1 !== renderIndex) {
      return;
    }

    // this.pdfDoc=pdfDoc
    totalPages.value = pdfDoc.numPages;
    // 先渲染全部或者canvasMaxNum
    allCanvas = new Array(Math.min(totalPages.value, canvasMaxNum))
      .fill(null)
      .map((item, index) => {
      // 维护allCanvasIndex
        allCanvasIndex.push(index + 1);
        let ncanvas = document.createElement("canvas");
        ncanvas.id = "topCanvas" + (index + 1);
        ncanvas.classList = ["canvasshow"];
        return ncanvas;
      });
    renderPartialIndex++;
    await renderPartial(pdfDoc, true, renderPartialIndex);
  } catch (error) {
    console.log(error);
    console.error("PDF 加载失败:", error);
    emit("error", error);
  } finally {
    loading.value = false;
  }
}
let scale0=1
// 绘制单个pdf页
async function renderPartialA(pdfDoc, pageIndex, canvas) {
  if (!pdfDoc) return;

  try {
    const page = await pdfDoc.getPage(pageIndex);

    const viewport = page.getViewport({ scale: scale0 });

    const fullWidth = viewport.width;
    const fullHeight = viewport.height;


           canvas.style.width = fullWidth
        canvas.style.height = fullHeight
           canvas.width = fullWidth
        canvas.height = fullHeight
    renderPartialArea(page, clipParamsTop, canvas);
  } catch (error) {
    //  Cannot read private member #pagePromises from an object whose class did not declare it
    console.error("渲染失败:", error);
  }
}

const getPageDimensions = (ratio) => {
  let width
  let height

  if (props.height && !props.width) {
    height = props.height
    width = height / ratio
  } else {
    width = props.width ?? canvasContainer.value.clientWidth
    height = width * ratio
  }

  return [width, height]
}
async function renderPartial(pdfDoc, init, renderPartialIndex1) {
  if (!pdfDoc) return;

  try {
    const ps = allCanvas.map((item, index) => {
      return pdfDoc.getPage(1 + index);
    });
    Promise.all(ps).then((pages) => {
      if (renderPartialIndex1 !== renderPartialIndex) {
        return; 
      }
      let page = pages[0];
      let w = canvasContainer.value.clientWidth;
          const viewWidth = page.view[2] - page.view[0]
           const viewHeight = page.view[3] - page.view[1]
          let isTransposed=false  
           const pageWidth = isTransposed ? viewHeight : viewWidth
            const [actualWidth, actualHeight] = getPageDimensions(
          isTransposed ? viewWidth / viewHeight : viewHeight / viewWidth
        )
    let scale=actualWidth  * window.devicePixelRatio/ pageWidth
    console.log(scale,'scale');
      scale0=scale
      const viewport = page.getViewport({ scale: scale});
      const fullWidth = viewport.width;
      const fullHeight = viewport.height;
      pageHeight.value = (fullHeight * (w - 10)) / fullWidth;
      totalHeight.value = totalPages.value * pageHeight.value;
      if (init) {
        allCanvas.forEach((item, index) => {
          if (index == 0) {
            item.style.top = `${0}px`;
          } else {
            item.style.top = `${pageHeight.value * index}px`;
          }
          canvasContainer.value.appendChild(item);
        });
      }

      // 根据预设计算裁剪参数
 

      pages.forEach((page, index) => {
          allCanvas[index].style.width = fullWidth
        allCanvas[index].style.height = fullHeight
          allCanvas[index].width = fullWidth
        allCanvas[index].height = fullHeight
        renderPartialArea(page, clipParamsTop, allCanvas[index]);
      });
    });
  } catch (error) {
    //  Cannot read private member #pagePromises from an object whose class did not declare it
    console.error("渲染失败:", error);
  }
}

const scale = ref(1);
async function renderPartialArea(page, clipParams, canvas) {
  const ctx = canvas.getContext("2d");

  const { clipX, clipY, clipWidth, clipHeight } = clipParams;
  
  const partialViewport = page.getViewport({ scale: scale0 }).clone({
    scale: scale0,
  });

  // 应用裁剪和渲染
  ctx.save();
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  const renderContext = {
    canvasContext: ctx,
    viewport: partialViewport,
  };

  await page.render(renderContext).promise;
  ctx.restore();
}
</script>
<style>
.canvasshow {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  pointer-events: none;
  z-index: 10;
}
</style>
<style scoped>
.partial-pdf-viewer {
  border: 1px solid #ddd;
  /* border-radius: 8px; */
  /* padding: 16px; */
}

.control-panel {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-bottom: 16px;
  padding: 12px;
  background: #f5f5f5;
  border-radius: 4px;
}

.control-group {
  display: flex;
  align-items: center;
  gap: 8px;
}

.clip-controls {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.clip-controls > div {
  display: flex;
  align-items: center;
  gap: 8px;
}

input[type="number"] {
  width: 60px;
  padding: 4px;
}

.canvas-container {
  width: 100%;
  height: 100%;
  position: relative;
  display: inline-block;
  border: 1px solid #eee;
  background: white;
}
.canvas-container::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
.canvas-container::-webkit-scrollbar-thumb {
  background: #c1c1c1;
  border-radius: 4px;
}
.mark {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  z-index: -10;
  pointer-events: none;
}
canvas {
  display: block;
}

.loading {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: rgba(0, 0, 0, 0.7);
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
}

.info-panel {
  margin-top: 12px;
  padding: 8px;
  background: #f0f0f0;
  border-radius: 4px;
  font-size: 12px;
  color: #666;
}
</style>
相关推荐
c0detrend2 小时前
读诗的时候我却使用了自己研发的Chrome元素截图插件
前端·chrome
希冀1232 小时前
【Vue】第五篇
前端·javascript·vue.js
Moonbit2 小时前
你行你上!MoonBit LOGO 重构有奖征集令
前端·后端·设计
littleplayer3 小时前
Root-> A ->B -> C page, 当前C page, 如何返回B,又如何直接返回A page呢
前端
姝然_95273 小时前
Android Activity启动流程详细分析
前端
littleplayer3 小时前
SwiftUI 导航
前端
用户92426257007313 小时前
Vue 组件入门学习笔记:局部注册、全局注册与 Props 传值详解
前端
云枫晖3 小时前
Webpack系列-构建性能优化实战:从开发到生产
前端·webpack·性能优化
Patrick_Wilson3 小时前
AI会如何评价一名前端工程师的技术人格
前端·typescript·ai编程