pdf预览:如何优雅地击败内存泄漏

  • 作者简介:大家好,我是文艺理科生Owen,某车企前端开发,负责AIGC+RAG项目
  • 目前在卷的技术方向:工程化系列,主要偏向最佳实践
  • 希望可以在评论区交流互动,感谢支持~~~

最近项目中有个内嵌预览pdf文件的需求,考虑到vue3项目,所以决定使用 vue-pdf-embed 快速实现预览功能。

说干就干,先来个demo~

为了复现项目真实环境,demo分为前端和服务端两部分。

前端负责请求获取对应页的pdf文件流 和渲染 。

服务端负责原文件按页拆分 ,并接受请求返回对应页的pdf文件流。

服务端项目搭建

服务端依赖选型如下:

依赖名称 作用
express 快速、简洁且灵活的Web应用开发框架
pdf-lib 创建和修改PDF文档
  1. 新建文件夹node-pdf-demo,安装 express 和 pdf-lib。运行 pnpm i express pdf-lib。
  2. 在根目录下准备一个pdf文件,示例中为 book.pdf。
  3. 分别新建两个文件:split.js用来拆分原文件,server.js用来封装接口。
  4. split.js代码如下。
    简单来说,就是将原文档通过pdf-lib读入,拷贝每一页到新的子文档实例中,并写入磁盘中。根目录下运行node split.js。然后根目录下会有14个pdf文件了,分别对应原文档的1-14页。
js 复制代码
// split.js
const fs = require('fs')
const PDFDocument = require('pdf-lib').PDFDocument;

(async function() {
  // 原文件路径
  const pdfPath = 'book.pdf'
  // 生成每一页的pdf文件
  const docmentAsBytes = fs.readFileSync(pdfPath);
  // 加载pdf文件流
  const pdfDoc = await PDFDocument.load(docmentAsBytes)
  const numberOfPages = pdfDoc.getPages().length;
  for (let i = 0; i < numberOfPages; i++) {
    // 创建一个子文档实例对象
    const subDocument = await PDFDocument.create();
    // 拷贝对应页的文件流
    const [copiedPage] = await subDocument.copyPages(pdfDoc, [i])
    // 将拷贝的结果保存在刚创建的子文档实例对象中
    subDocument.addPage(copiedPage);
    // 保存子文档实例对象
    const pdfBytes = await subDocument.save()
    // 将子文档实例对象写入磁盘中,以文件形式存放在根目录下
    await writePdfBytesToFile(`file-${i + 1}.pdf`, pdfBytes);
  }
})()

// 将字节流写入磁盘中
async function writePdfBytesToFile(fileName, pdfBytes) {
  return fs.promises.writeFile(fileName, pdfBytes);
}
  1. server.js代码如下。
    需要重点关注的是 app,是express的实例对象。通过它,可以处理http请求,即后端封装的接口。
js 复制代码
const fs = require('fs')
const express = require('express')
const app = express()

// 获取某一页的pdf文件流
app.get('/pdf/:page', async (req, res) => {
  // 读入请求参数对应页码的文件流
  const pdf = fs.readFileSync(`file-${+req.params.page}.pdf`)
  // 将文件页码总数写入到header中,返回给前端
  res.setHeader('totalPage', 14)
  // 返回文件流 
  res.send(pdf)
})

// 启动服务器
app.listen(3000, () => {
  console.log(`Server running on port 3000`);
});
  1. 运行node server.js,命令行输出 Server running on port 3000,说明服务端启动完成。

前端项目搭建

  1. 快速生成项目:pnpm create vite vue3-pdf-demo --template vue

  2. 运行:
    cd vue3-pdf-demo
    pnpm install
    pnpm run dev

  3. 项目启动完成(文案稍微修改了下~):

  1. 安装依赖 pnpm i vue-pdf-embed, 启动项目 pnpm dev。

  2. 在 components下新建一个vue文件,命名为PdfPreview.vue,我们在这个文件中编写pdf预览组件。代码如下:

js 复制代码
<template>
  <div class="container">
    <div class="pdf-box">
      <VuePdfEmbed annotationLayer textLayer
        @rendering-failed="handleRenderFailed" :source="doc" 
        class="vue-pdf-embed" />
    </div>
    <div class="operation">
      <button @click="handleClick('prev')">&lt;</button>
      <span>{{ pageNum }}/{{ pageTotalNum }}</span>
      <button @click="handleClick('next')">&gt;</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, watch, nextTick } from 'vue';
import VuePdfEmbed, { useVuePdfEmbed } from 'vue-pdf-embed';

// essential styles
import 'vue-pdf-embed/dist/style/index.css'

// optional styles
import 'vue-pdf-embed/dist/style/annotationLayer.css'
import 'vue-pdf-embed/dist/style/textLayer.css'
import axios from 'axios'

onMounted(() => {
  request()
})

const pageTotalNum = ref(0); // 总页数
const pageNum = ref(1)
const pdfSource = ref('')

// 用来排查展示失败错误
const handleRenderFailed = (err) => {
  console.log(err, 'err');
}

// 请求获取pdf对应页码的文件流,并转为url
const request = async () => {
  pdfSource.value = ''
  axios.get(`/pdf/${pageNum.value}`, {
    responseType: 'blob'
  }).then(res => {
    pageTotalNum.value = +res.headers.totalpage
    const blob = window.URL.createObjectURL(new Blob([res.data], { type: 'application/pdf' }))
    pdfSource.value = blob
  })
}

const handleClick = (type: string) => {
  if (type === 'prev') {
    pageNum.value--
  } else if (type === 'next') {
    pageNum.value++
  }
  if (pageNum.value <= 0) {
    pageNum.value = pageTotalNum.value
  } else if (pageNum.value > pageTotalNum.value) {
    pageNum.value = 1
  }
  window.URL.revokeObjectURL(pdfSource.value)
  nextTick(() => {
    request()
  })


}
const { doc } = useVuePdfEmbed({
  source: pdfSource
})

// watch(() => pageNum.value, () => {
//   doc.value?.destroy()
// }, {
//   immediate: true
// })
</script>

<style scoped lang="scss">
.container {
  width: 500px;
  background-color: #999;

  .pdf-box {
    width: 100%;
    height: 100%;
  }
}

.operation {
  display: flex;
  justify-content: center;
  align-items: center;
  span {
    width: 50px;
    text-align: center;
  }
}

button {
  width: 50px;
  height: 50px;
}
</style>

在组件中,通过axios请求获取对应页码的文件流,返回类型是blob二进制流格式。然后通过 window.URL.createObjectURL将blob文件流格式转为url,绑定到vue-pdf-embed组件上的source属性。

  1. 在App.vue文件中引用组件,代码如下:
js 复制代码
<script setup>
import PdfPreview from './components/PdfPreview.vue'
</script>

<template>
  <div class="app-container">
    <div class="title">
      <h1>Owen Pdf Preview</h1>
      <img src="/src/assets/vue.svg" alt="">
    </div>
    <PdfPreview msg="Owen Pdf Preview" />
  </div>
</template>

<style scoped>
.app-container {
  display: flex;
  justify-content: space-around;
  align-items: center;
  width: 70%;
  margin: 0 auto;
}

.title {
  display: flex;
  flex-direction: column;
  align-items: center;
}

img {
  width: 200px;
  height: 200px;
}
</style>
  1. 其实运行到这里就算完成了。

但实际自测中,发现当进行页码切换时,会不断地将url映射的blob文件对象添加到缓存中,浏览器内存会越占越大,导致网页崩溃。

查阅了一些资料,有用的只有这5个。

总结下有两种方案:

  • 在onload事件中window.URL.revoke
  • this.src=''

按照建议尝试后,仍然无效。帖子中的方案都是在原生代码里使用,而在vue3中响应式变量缓存导致无法解决。

相关链接:

  1. 关于 URL.createObjectURL 可能导致的内存泄露的问题 #367
  2. Understanding Object URLS for client-side files and how to free the memory
  3. createobjecturl-memory-leak-in-chrome
  4. How to properly release memory allocated by a blob in Javascript?
  5. window.URL.revokeObjectURL doesn't release memory immediately

其实目的是当预览下一页的blob时,将上次的blob清空 。顺着这个思路去源码里寻找答案。 经过无数次坚持与放弃的挣扎 终于看到了 doc.value?.destroy() 销毁pdf实例的代码。

同时在文档中也看到了组合式api用法。明白了~

  1. 解开被注释的这段代码。通过监听当前页码的变化,当发生变化时,销毁pdf的实例。

再次测试,正常。(已翻到第3页,只有一个blob地址)

总结:本文通过一个demo实现了pdf预览功能,服务端采用express封装接口,返回对应页码的文件流。前端采用vue-pdf-embed组件,实现了pdf预览功能。最后通过前后端完整的交互流程,复现并解决了内存泄漏的问题。

参考文献:

  1. vue3中使用 vue-pdf-embed 实现pdf文件预览、翻页、下载等功能
  2. vue3 vue-pdf-embed 实现pdf预览、缩放、拖拽、旋转和左侧菜单选择
  3. Vue3 实现 PDF 文件在线预览功能

demo: github

日拱一卒,功不唐捐。

相关推荐
橙光成长2 小时前
PDF离线工具箱深度解析:4MB纯前端单文件,40+功能全本地运行,文件绝不上传
前端·pdf·1024程序员节
小poop9 小时前
CSS 基础与实战:从入门到精通网页美化
前端
Csvn10 小时前
运行时性能
前端
Never_every9911 小时前
位图转矢量工具哪个好?实测抠抠图AI位图转矢量工具
大数据·前端
paopaokaka_luck11 小时前
宠物门店经营管理系统(销售与库存联动、进货自动记账、 退换与退款关联、员工个人业绩、ECharts 经营分析、滞销识别与客户分层、宠物档案年龄计算)
前端·javascript·spring boot·mysql·echarts·mybatis·1024程序员节
空中湖12 小时前
用 React 写数据大屏:为什么我不建议用图表库
前端·react.js·前端框架
欣欣之王来了13 小时前
原始HTML插值:v-html指令的使用与安全风险
前端·学习·架构·项目·vue3教程
IT_陈寒13 小时前
Redis的LRU淘汰策略坑了我一整天
前端·人工智能·后端
计算机魔术师13 小时前
砍半价格、参数再涨,国产旗舰追到 Opus 鼻尖了
前端
paopaokaka_luck13 小时前
心理咨询管理系统(WebSocket实时沟通、基于用户的协同过滤、 ECharts多维分析、 预约业务流程、预约校验与定时处理)
java·vue.js·spring boot·mysql·spring·echarts·1024程序员节