Vue中能否存储使用pdf.js的PDFDocumentProxy/PDFPageProxy对象
问题核心原因
你遇到的Cannot read/write from private field错误和worker代理无关,本质是Vue3响应式系统和pdfjs-dist 2.x的实例实现不兼容:
- Vue3 默认会对所有挂载到
data、props、ref等响应式上下文的对象做深度Proxy包装,拦截对象的读写、方法调用操作 - pdfjs-dist 2.x 版本的
PDFDocumentProxy、PDFPageProxy、RenderTask等核心实例,内部大量使用ES标准的类私有字段(#开头的属性)存储内部状态 - 被Proxy包装后的对象调用原生方法时,无法正确访问实例本身的私有字段,直接抛出类型错误
你之前发现回调内的原生pdf变量可以正常调用getPage,存到this.pdf(响应式代理对象)就报错,以及传props后page.render报错,都是这个原因导致的。
另外你写的worker引入路径在Vite环境下有隐患,直接引用node_modules相对路径在开发/生产构建时大概率出现404问题。
修复方案
遵循一个核心原则即可解决所有私有字段错误:所有pdfjs返回的原生实例,绝对不要纳入Vue的响应式系统,具体落地规则:
- 用
shallowRef存储PDF文档实例,shallowRef只会对.value的赋值做响应式追踪,不会深度代理内部对象 - PDF页实例、渲染任务实例直接用普通闭包/组件局部变量存储,不要放进data、ref等响应式容器
- 如果必须把这类实例放到响应式数组/对象中,先用
markRaw()包裹实例,强制Vue跳过对它的代理 - Vite环境下用
?url后缀导入worker文件,拿到构建后正确的静态资源路径 - 补充组件销毁时的实例、渲染任务清理逻辑,避免内存泄漏
修正后可运行代码
PDFDocument.vue
<template> <div class="pdf-document"> <PDFPage v-for="pageNum in totalPages" :key="pageNum" :page-num="pageNum" :pdf-doc="pdfInstance" :scale="scale" /> </div> </template> <script lang="ts" setup> import { ref, shallowRef, onBeforeUnmount } from 'vue'; import * as pdfjsLib from 'pdfjs-dist'; // Vite 环境下正确引入worker的方式 import pdfWorker from 'pdfjs-dist/build/pdf.worker.js?url'; import PDFPage from './PDFPage.vue'; pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker; const props = defineProps<{ url: string; scale: number; }>() // shallowRef存储文档实例,不会做深度代理 const pdfInstance = shallowRef<pdfjsLib.PDFDocumentProxy | null>(null); const totalPages = ref(0); let loadingTask: pdfjsLib.PDFDocumentLoadingTask | null = null; const loadPdf = async () => { loadingTask = pdfjsLib.getDocument(props.url); const pdf = await loadingTask.promise; pdfInstance.value = pdf; totalPages.value = pdf.numPages; } loadPdf(); onBeforeUnmount(() => { loadingTask?.destroy(); pdfInstance.value?.destroy(); }) </script>
这里没有提前加载所有PDF页存入数组,大PDF场景下提前加载所有页会占用大量内存,改为传递页码和文档实例给子组件,子组件按需加载对应页面,性能更好。如果你确实需要预加载所有页,在
getPage拿到实例后用markRaw(page)包裹再存入数组即可。
PDFPage.vue
<template> <canvas class="pdf-page" ref="canvasRef" :width="canvasSize.width" :height="canvasSize.height" :style="canvasStyle" ></canvas> </template> <script lang="ts" setup> import { ref, computed, watch, onBeforeUnmount } from "vue"; import type { PDFDocumentProxy, PDFPageProxy, RenderTask } from 'pdfjs-dist'; const props = defineProps<{ pageNum: number; pdfDoc: PDFDocumentProxy | null; scale: number; }>() const emit = defineEmits<{ rendered: [page: PDFPageProxy] }>() const canvasRef = ref<HTMLCanvasElement | null>(null); const canvasSize = ref({width: 0, height: 0}); // 页实例、渲染任务用普通变量存储,完全不进入响应式系统 let pageInstance: PDFPageProxy | null = null; let renderTask: RenderTask | null = null; // 适配设备像素比,避免高清屏渲染模糊 const renderScale = computed(() => props.scale * (window.devicePixelRatio || 1)); const canvasStyle = computed(() => { const pxRatio = window.devicePixelRatio || 1; return `width: ${Math.ceil(canvasSize.value.width / pxRatio)}px; height: ${Math.ceil(canvasSize.value.height / pxRatio)}px;` }); const drawPage = async () => { if (!props.pdfDoc || !canvasRef.value) return; // 取消未完成的渲染任务,避免重复渲染冲突 if (renderTask) { renderTask.cancel(); renderTask = null; } // 直接调用原生实例方法,全程不做代理包装 if (!pageInstance) { pageInstance = await props.pdfDoc.getPage(props.pageNum); } const viewport = pageInstance.getViewport({scale: renderScale.value}); canvasSize.value = { width: viewport.width, height: viewport.height }; const canvasContext = canvasRef.value.getContext('2d'); if (!canvasContext) return; renderTask = pageInstance.render({canvasContext, viewport}); try { await renderTask.promise; emit('rendered', pageInstance); } catch (e) { // 主动取消渲染抛出的异常不需要处理 if ((e as Error).name !== 'RenderingCancelledException') console.error(e); } finally { renderTask = null; } } // 文档加载完成、缩放变化时重新渲染 watch(() => [props.pdfDoc, props.scale], () => drawPage(), {immediate: true}); onBeforeUnmount(() => { renderTask?.cancel(); pageInstance?.cleanup(); }) </script>
内容的提问来源于stack exchange,提问作者Connor Hainje
相关产品推荐
相关产品推荐

