likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 1
  • 作者简介:大家好,我是文艺理科生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页。
// 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请求,即后端封装的接口。
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,说明服务端启动完成。

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

前端项目搭建

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

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

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

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

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

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

<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.createObjectURLblob文件流格式转为url,绑定到vue-pdf-embed组件上的source属性。 pdf预览:如何优雅地击败内存泄漏

  1. App.vue文件中引用组件,代码如下:
<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. 其实运行到这里就算完成了。

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

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

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

查阅了一些资料,有用的只有这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实例的代码。

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

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

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

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

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

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

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

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

参考文献:

demo: github

日拱一卒,功不唐捐。