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

相关推荐
接着奏乐接着舞。30 分钟前
Electron + Vue 项目如何实现软件在线更新
javascript·vue.js·electron
银迢迢4 小时前
如何创建一个Vue项目
前端·javascript·vue.js
几何心凉7 小时前
如何解决Vue组件间传递数据的问题?
前端·javascript·vue.js
鱼樱前端9 小时前
Vue3 + TypeScript 整合 MeScroll.js 组件
前端·vue.js
拉不动的猪10 小时前
刷刷题29
前端·vue.js·面试
武昌库里写JAVA10 小时前
原生iOS集成react-native (react-native 0.65+)
vue.js·spring boot·毕业设计·layui·课程设计
野生的程序媛10 小时前
重生之我在学Vue--第5天 Vue 3 路由管理(Vue Router)
前端·javascript·vue.js
鱼樱前端10 小时前
Vue 2 与 Vue 3 响应式原理详细对比
javascript·vue.js
阿丽塔~10 小时前
面试题之vue和react的异同
前端·vue.js·react.js·面试
鱼樱前端12 小时前
Vue 2 与 Vue 3 语法区别完整对比
前端·javascript·vue.js