vue2老项目如何使用pdfjs-dist解析pdf

24 阅读6分钟

先说结论

使用pdfjs-dist的2.7.570版本

# 使用pdfjs-dist的2.7.570版本的es5产物,该版本的es5的build产物没有特殊写法,对vue.config.js、babel.config.js的兼容性最好,不需要额外再下载其他插件,比如可选链插件等
# 引用方式
import * as pdfjsLib from 'pdfjs-dist/es5/build/pdf'
import pdfWorker from 'pdfjs-dist/es5/build/pdf.worker.entry'

pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker
pdfjsLib.GlobalWorkerOptions.isEvalSupported = false // 关闭 eval 支持(防止漏洞)// 漏洞链接: https://www.venustech.com.cn/new_type/aqtg/20240514/27492.html

背景

某天,产品同事发现C端的资质展示页面(H5)的图片渲染有问题,经排查发现有些链接是.pdf结尾的pdf文件,最后导致某些机型无法正常渲染。如下图所示

<img src="https://xxx.xx.com/xxx.pdf" />

这里有个小点: ios机型会把pdf渲染出来1页,安卓机型无法渲染pdf

遇到问题时的思考

  • 这种情况在线上是否多,如果量级不多,个人感觉可以尝试后端进行解析PDF转成图片,在更新数据库。但这需要结合业务系统来看,因为这是“资质文件”,所以得保证业务系统在这方面的功能是怎么样的,是只能上传图片还是pdf,还是都能,不过这是后面分析才得到的结果
  • 后端是否好解决,这需要和后端沟通

需求

  • 能够根据pdf链接展示出他的所有页数内容,不失真,能在各个机型的web-view中运行,如安卓app、ios的app、支付小程序、微信小程序。
  • 只做pdf解析预览功能,不需要额外功能,把内容转成图片即可。
  • 尽量做到不修改vue.config.js、babel.config.js来解决这个问题,理想情况就是新增一个组件,然后对应页面判断是链接,最后使用下就好了。毕竟这是老项目,这些可不能乱动。

前期开发工作

  • 与后端沟通,结果是后端不好解决,只能交给前端来解决
  • 先问gpt,具体方案和关键依赖,可能的坑点
  • 搜索gpt,根据得到的关键依赖(pdfjs-dist),了解他的issue、坑点、demo等
  • 顺便搜下相关文章,但发现质量都挺一般的,数量有点少,于是就参考gpt的,其次就是pdfjs-dist的文档有点难看懂

具体实施

  • 下载 pdfjs-dist,下载时下的是最新版本,5.x.x
  • 新建pdf-viewr.vue组件
  • 复制gpt给我的代码
  • 运行项目
# gpt给的案例
import * as pdfjsLib from 'pdfjs-dist/build/pdf'
import pdfWorker from 'pdfjs-dist/build/pdf.worker.entry'
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker

// 解析过程省略,主要核心是引用方式的问题

踩坑之旅

依照上述实施行为,结果就是直接报错,运行不了,于是我就开始了我的漫长踩坑之旅

我的依赖&版本

  • node: 12.22.22
  • @vue/cli: ^3.12.0
  • babel-core: 7.0.0-bridge.0
  • vue: 2.6.x

版本问题

使用pdfjs-dist遇到了非常多的版本兼容性问题,由于用的是最新版本,有很多写法,和我的对不上,于是我的想法是降版本,但一开始不知道降到多少,毕竟有1549个版本,想法是问ai,但ai回答出来的版本还是不太行,于是我就打算换个方式,找找有没有现成的封装好的组件

这里找到的是vue-pdf,这个组件是用pdfjs-dist+vue2的,我大喜过望,马上下载使用,写起来很快呀,但新的问题已然埋下

vue-pdf问题

  • issue数量特别多,只能说慎用,一共235个
  • 这个组件在打包上线后,会无法正常加载出来,导致对应页面白屏。根据gpt的说法是可能丢失了pdfjs-dist,然后我就去找原因,这种情况先找issue,我在vue-pdfissue中看到了有人遇到了同样的问题,然后我在某个issue中,看到了这么一句话:使用2.7.570版本

于是我顺藤摸瓜,找到pdfjs-dist的版本记录,找到他的内容是什么,然后我就发现了一个让我很兴奋的点:

这个依赖的有build和es5的build产物!!!,这是最关键的,他的其他版本我看了几个,我发现没有es5的打包产物

image.png

用build的产物仍然存在特殊写法,比如可选链,因为我的项目没有可选链的babel插件,所以仍然不能用

但是es5的产物没有特殊写法,所以理论上用这个就能解决了,因为引用报错的问题是语法相关的兼容性问题,我又不想新增babel插件、loader依赖等

最后在我使用了这个版本后,修改了原先AI提供的案例中的引用方式,于是项目正常跑了起来,这里就不使用vue-pdf了,使用pdfjs-dist,用它来解析,然后转图片。

对比如下:

# 前
import * as pdfjsLib from 'pdfjs-dist/build/pdf'
import pdfWorker from 'pdfjs-dist/build/pdf.worker.entry'
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker

# 后
import * as pdfjsLib from 'pdfjs-dist/es5/build/pdf'
import pdfWorker from 'pdfjs-dist/es5/build/pdf.worker.entry'
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker

病毒问题

病毒问题也是看vue-pdf的issue才知道的

根据报告链接所提供的方案就是

pdfjsLib.GlobalWorkerOptions.isEvalSupported = false // 关闭 eval 支持(防止漏洞)

链接: 【漏洞通告】Mozilla PDF.js代码执行漏洞(CVE-2024-4367)

小结

为了解决pdfjs-dist在vue2的老项目的兼容性问题,最终方案就是使用pdfjs-dist@2.7.570,这样就不用影响到之前的vue.config.js、babel.config.js相关配置了。

这个过程解决兼容问题,费时费力,还以为无法解决了,不过皇天不负苦心人,这个问题还是被解决了,所以记录下。

后续计划

  • 自己打包高版本(5.x.x)的pdfjs-dist来适应内部项目
  • 研究vue-pdf打包上线白屏问题
  • 接入vue3项目
  • 分页加载优化,大文件优化

代码

pdf解析预览组件

<template>
  <div class="pdf-viewer">
    <van-loading v-if="loading" type="spinner" size="32px" vertical>加载中</van-loading>
    <div v-else>
      <div v-if="error" class="fallback">
        <p>当前信息无法加载,您可以<a :href="pdfUrl" target="_blank">点击查看</a></p>
      </div>
      <div v-else>
        <div v-for="(page, index) in pages" :key="index" class="pdf-page">
          <img :src="page" :alt="'pdf ' + (index + 1)" class="pdf-image">
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { Loading } from 'vant'

// ? 使用pdfjs-dist的2.7.570版本的es5产物,该版本的es5的build产物没有特殊写法,对本项目vue.config.js、babel.config.js的兼容性最好
import * as pdfjsLib from 'pdfjs-dist/es5/build/pdf'
import pdfWorker from 'pdfjs-dist/es5/build/pdf.worker.entry'

pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker
pdfjsLib.GlobalWorkerOptions.isEvalSupported = false // 关闭 eval 支持(防止漏洞)// 漏洞链接: https://www.venustech.com.cn/new_type/aqtg/20240514/27492.html

export default {
  name: 'pdf-viewer',
  props: {
    pdfUrl: {
      type: String,
      required: true,
    },
  },
  components: {
    [Loading.name]: Loading,
  },
  data() {
    return {
      pages: [],
      loading: true,
      error: false,
    }
  },
  mounted() {
    this.loadPdf()
  },
  methods: {
    async loadPdf() {
      try {
        const loadingTask = pdfjsLib.getDocument(this.pdfUrl)
        const pdf = await loadingTask.promise
        const pageImages = []

        for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
          const page = await pdf.getPage(pageNum)
          const viewport = page.getViewport({ scale: 2 }) // scale 调大可提高清晰度

          const canvas = document.createElement('canvas')
          const context = canvas.getContext('2d')
          canvas.width = viewport.width
          canvas.height = viewport.height

          await page.render({ canvasContext: context, viewport }).promise
          pageImages.push(canvas.toDataURL())
        }

        this.pages = pageImages
        this.loading = false
      } catch (e) {
        console.error('PDF 加载失败:', e)
        this.error = true
        this.loading = false
      }
    },
  },
}
</script>

<style lang="stylus" scoped>
.pdf-viewer {
  width: 100%;
  overflow-x: hidden;
}
.pdf-image {
  width: 100%;
  display: block;
  object-fit: contain;
}
.fallback {
  text-align: center;
  color: #999;
  font-size: 14px;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;

  a {
    color: #007aff;
    text-decoration: underline;
  }
}
</style>

链接类型判断

    // 判断文件类型(扩展名优先,fallback 为 HEAD)
    async detectFileType(url) {
      // 1. 特殊 scheme 优先判断
      if (url.startsWith('data:image/')) return 'image'
      if (url.startsWith('data:application/pdf')) return 'pdf'
      if (url.startsWith('blob:')) return 'unknown'

      // 2. 扩展名判断(宽松匹配)
      if (/\.(jpe?g|png|gif|bmp|webp|svg)([\?#].*)?$/i.test(url)) return 'image'
      if (/\.pdf([\?#].*)?$/i.test(url)) return 'pdf'

      // 3. 尝试发 HEAD 请求获取 content-type
      try {
        const res = await axios.head(url) // ? 支付宝小程序如果遇到404链接会导致页面白屏,微信、安卓不会
        const contentType = res.headers['content-type'] || ''
        if (contentType.includes('image/')) return 'image'
        if (contentType.includes('application/pdf')) return 'pdf'
      } catch (e) {
        console.warn('链接类型获取失败:', url, e.message)
      }

      return 'unknown'
    }