You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 02:33:07