pdfjs-dist 在vite4.x 下的使用问题

本文记录了我在基于 vite 的vue3项目中使用 pdfjs-dist 的过程中遇到的一个疑难问题。当我将 vite 从2.7.x 升级到 4.3.x 后出现 pdfjs-dist 不能使用的问题。经过排查,最后发现是 vite 4.x 在将 pdfjs-dist 转化为 ESM 模块时存在某些问题,导致 import 的库文件无法正常使用。

背景

项目技术栈:vue3 + vite + ts

pdfjs-dist版本:2.9.359

最近将项目从js升级到ts了,主要过程虽然踩坑不少,但是结果还是好的,顺利升级完成。

因为某些版本方面的原因,vitest 的vite.config.ts配置一直提示ts异常,解决方案是:升级vitest到3.x, 升级 vite (从vite 2.7 升级到 4.x)。 升级后 pdf浏览组件 出现一些问题。

先看升级前的写法:

html 复制代码
<template>
  <div class="pdf-container">
    <canvas v-for="pageIndex in pdfPages" :id="`pdf-canvas-${pageIndex}`"  :key="pageIndex" />
  </div>
</template>
<script setup lang="ts">
import * as PDFJS from 'pdfjs-dist/legacy/build/pdf.js'
// 这里导入的是 pdf.worker.entry.js
import * as PdfWorker from 'pdfjs-dist/legacy/build/pdf.worker.entry.js'
import { nextTick, ref, Ref, watch } from 'vue'
import { Log } from '@/utils'
import { isEmpty, debounce } from 'lodash-es'
import { ElNotification } from 'element-plus'


const props:any = defineProps({
  pdf: {
    required: true
  }
})

let pdfDoc:any = null
const pdfPages:Ref = ref(0)
const pdfScale:Ref = ref(1.3)
const loadFile = async (url:any) => {
  // 设定pdfjs的 workerSrc 参数
  PDFJS.GlobalWorkerOptions.workerSrc = PdfWorker
  const loadingTask = PDFJS.getDocument(url)
  loadingTask.promise.then(async (pdf:any) => {
    pdfDoc = pdf // 保存加载的pdf文件流
    pdfPages.value = pdfDoc.numPages // 获取pdf文件的总页数
    await nextTick(() => {
      renderPage(1) // 将pdf文件内容渲染到canvas
    })
  }).catch((error:any) => {
    Log.Warn(`[eCloudFilm] pdfReader loadFile error: ${error}`)
    ElNotification.error({
      message: '获取pdf报告失败'
    })
  })
}

const renderPage = (num:any) => {
  pdfDoc.getPage(num).then((page:any) => {
    page.cleanup()
    const canvas:any = document.getElementById(`pdf-canvas-${num}`)
    if (canvas) {
      const ctx = canvas.getContext('2d')
      const dpr = window.devicePixelRatio || 1
      const bsr = ctx.webkitBackingStorePixelRatio ||
                  ctx.mozBackingStorePixelRatio ||
                  ctx.msBackingStorePixelRatio ||
                  ctx.oBackingStorePixelRatio ||
                  ctx.backingStorePixelRatio ||
                  1
      const ratio = dpr / bsr
      const viewport = page.getViewport({ scale: pdfScale.value })
      canvas.width = viewport.width * ratio
      canvas.height = viewport.height * ratio
      canvas.style.width = viewport.width + 'px'
      canvas.style.height = viewport.height + 'px'
      ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
      const renderContext = {
        canvasContext: ctx,
        viewport: viewport
      }
      page.render(renderContext)
      if (num < pdfPages.value) {
        renderPage(num + 1)
      }
    }
  })
}

const debouncedLoadFile = debounce((pdf:any) => loadFile(pdf), 1000)
watch(() => props.pdf, (newValue:any) => {
  !isEmpty(newValue) && debouncedLoadFile(newValue)
}, {
  immediate: true
})
</script>

pdfjs-dist 导入部分为:

ts 复制代码
import * as PDFJS from 'pdfjs-dist/legacy/build/pdf.js'
// 这里导入的是 pdf.worker.entry.js
import * as PdfWorker from 'pdfjs-dist/legacy/build/pdf.worker.entry.js'

升级到vite 4.x 后,因为 vite 的某些原因,

ts 复制代码
import * as PdfWorker from 'pdfjs-dist/legacy/build/pdf.worker.entry.js'

这样导入程序编译会直接报错,pdf.worker.entry.js 内容如下:

ts 复制代码
(typeof window !== "undefined"
  ? window
  : {}
).pdfjsWorker = require("./pdf.worker.js");

已知报错跟这个文件的模块化方式有关,具体原因不详述。 (vite会将各类其他标准模块化的文件转化为 es 标准模式, 这个文件在转化的过程中出现问题,不过在vite2.7中正常,可能跟vite的某些变更有关,此处不深究)。

通过查看 .vite 下的编译缓存文件,对 vite2.7 下编译文件的对比,

  • vite2.7 打包的 pdf.worker.entry.js
  • vite 4.43 打包的 pdf.worker.entry.js
  • vite4.43 打包的 pdf.worker.js

通过对比,发现 vite2.7 打包的 pdf.worker.entry.js 和 vite4.43 打包的 pdf.worker.js 文件基本一致,都是 esmodule 形式的模块,因此改引入 pdf.worker.entry.jspdf.worker.js, 修改后果然能正常编译。

但是,pdf 不能正常显示,报如下错误:

js 复制代码
Error: Setting up fake worker failed: 
"Cannot read properties of undefined (reading 'WorkerMessageHandler')".

原因分析

vite2.7 中 pdf.worker.entry.js 除了将pdf.worker.js 转成esmodule后,还将 pdfjsWorker 手动挂载到 window 对象上,而vite4.3中,没有此操作。 但是,在pdfjs的渲染过程中,代码中用到了 window.pdfjsWorker 对象,因此,在 vite 4.3 下通过直接导入 pdf.worker.js 使用时,程序会抛出异常。

解决方案

vite 4.3 下,在程序中手动挂载 pdfjsWorker 对象到 window上。

js 复制代码
// In vite4, it is not support to import pdfWorker from pdf.worker.entry.js // So we should to set window.pdfjsWorker ourself window.pdfjsWorker = PdfWorker
window.pdfjsWorker = PdfWorker

以下是修改后的实现:

html 复制代码
<template>
  <div class="pdf-container">
    <canvas v-for="pageIndex in pdfPages" :id="`pdf-canvas-${pageIndex}`"  :key="pageIndex" />
  </div>
</template>
<script setup lang="ts">
import * as PDFJS from 'pdfjs-dist/legacy/build/pdf.js'
import * as PdfWorker from 'pdfjs-dist/build/pdf.worker.js'
import { nextTick, ref, Ref, watch } from 'vue'
import { Log } from '@/utils'
import { isEmpty, debounce } from 'lodash-es'
import { ElNotification } from 'element-plus'

const props:any = defineProps({
  pdf: {
    required: true
  }
})

// In vite4, it is not support to import pdfWorker from pdf.worker.entry.js
// So we should to set window.pdfjsWorker ourself
window.pdfjsWorker = PdfWorker
let pdfDoc:any = null
const pdfPages:Ref = ref(0)
const pdfScale:Ref = ref(1.3)
const loadFile = async (url:any) => {
  // 设定pdfjs的 workerSrc 参数
  PDFJS.GlobalWorkerOptions.workerSrc = PdfWorker
  const loadingTask = PDFJS.getDocument(url)
  loadingTask.promise.then(async (pdf:any) => {
    pdfDoc = pdf // 保存加载的pdf文件流
    pdfPages.value = pdfDoc.numPages // 获取pdf文件的总页数
    await nextTick(() => {
      renderPage(1) // 将pdf文件内容渲染到canvas
    })
  }).catch((error:any) => {
    Log.Warn(`[eCloudFilm] pdfReader loadFile error: ${error}`)
    ElNotification.error({
      message: '获取pdf报告失败'
    })
  })
}

const renderPage = (num:any) => {
  pdfDoc.getPage(num).then((page:any) => {
    page.cleanup()
    const canvas:any = document.getElementById(`pdf-canvas-${num}`)
    if (canvas) {
      const ctx = canvas.getContext('2d')
      const dpr = window.devicePixelRatio || 1
      const bsr = ctx.webkitBackingStorePixelRatio ||
                  ctx.mozBackingStorePixelRatio ||
                  ctx.msBackingStorePixelRatio ||
                  ctx.oBackingStorePixelRatio ||
                  ctx.backingStorePixelRatio ||
                  1
      const ratio = dpr / bsr
      const viewport = page.getViewport({ scale: pdfScale.value })
      canvas.width = viewport.width * ratio
      canvas.height = viewport.height * ratio
      canvas.style.width = viewport.width + 'px'
      canvas.style.height = viewport.height + 'px'
      ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
      const renderContext = {
        canvasContext: ctx,
        viewport: viewport
      }
      page.render(renderContext)
      if (num < pdfPages.value) {
        renderPage(num + 1)
      }
    }
  })
}

const debouncedLoadFile = debounce((pdf:any) => loadFile(pdf), 1000)
watch(() => props.pdf, (newValue:any) => {
  !isEmpty(newValue) && debouncedLoadFile(newValue)
}, {
  immediate: true
})
</script>

TODO

后面有时间给 vite 提个issue

相关推荐
谁呛我名字1 小时前
大数据应用开发——数据可视化
javascript·vue.js·echarts
神夜大侠6 小时前
VUE 实现公告无缝循环滚动
前端·javascript·vue.js
明辉光焱6 小时前
【Electron】Electron Forge如何支持Element plus?
前端·javascript·vue.js·electron·node.js
杨荧9 小时前
【JAVA毕业设计】基于Vue和SpringBoot的宠物咖啡馆平台
java·开发语言·jvm·vue.js·spring boot·spring cloud·开源
NoloveisGod11 小时前
Vue的基础使用
前端·javascript·vue.js
GISer_Jing11 小时前
前端系统设计面试题(二)Javascript\Vue
前端·javascript·vue.js
理想不理想v11 小时前
使用JS实现文件流转换excel?
java·前端·javascript·css·vue.js·spring·面试
EasyNTS12 小时前
无插件H5播放器EasyPlayer.js网页web无插件播放器vue和react详细介绍
前端·javascript·vue.js
guokanglun12 小时前
Vue.js动态组件使用
前端·javascript·vue.js
糊涂涂是个小盆友14 小时前
前端 - 使用uniapp+vue搭建前端项目(app端)
前端·vue.js·uni-app