背景
在发票管理系统中,遇到了一个典型的技术挑战:第三方服务只提供了发票的 PDF 文件下载地址,而业务需求要求在 H5 界面上直接展示发票的图片预览。
- 发票文件由第三方服务提供,后端无法直接获取发票的图片地址
- 后端只能提供 PDF 文件的下载链接
- 前端需要在移动端 H5 页面中展示发票预览图
- 需要支持跨域请求和身份认证(携带 Cookie)
实现方案
1. 安装依赖
npm install pdfjs-dist@5.4.149
2. 关键代码实现
2.1 Worker 配置
PDF.js 使用 Web Worker 进行后台解析,需要正确配置 Worker 路径:
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.min.mjs';
// 配置 PDF.js worker - 使用本地文件
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
import.meta.url
).toString();
注意:使用 new URL() 和 import.meta.url 可以确保在 Vite 构建后 Worker 文件路径正确。
2.2 加载 PDF 文档
加载 PDF 时需要处理跨域和身份认证:
const pdfDoc = await pdfjsLib.getDocument({
url, // PDF 下载地址
disableWorker: false,
disableAutoFetch: false,
disableStream: false,
// 配置请求头以携带 cookie
httpHeaders: {
'X-Requested-With': 'XMLHttpRequest'
},
// 启用跨域请求携带 cookie
withCredentials: true
}).promise;
关键配置说明:
withCredentials: true:允许跨域请求携带 Cookie,用于身份认证httpHeaders:自定义请求头,某些服务可能需要特定的请求头
2.3 获取并渲染 PDF 页面
// 获取第一页,发票只有一页
const page = await pdfDoc.getPage(1);
// 计算合适的缩放比例
const containerWidth = pdfContainer.value?.clientWidth || 750;
const baseScale = containerWidth / page.getViewport({ scale: 1 }).width;
const devicePixelRatio = window.devicePixelRatio || 1;
const scale = Math.min(baseScale * devicePixelRatio, 3); // 最大3倍缩放
const viewport = page.getViewport({ scale });
// 创建 canvas
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// 设置 canvas 的实际尺寸(高分辨率)
canvas.width = viewport.width;
canvas.height = viewport.height;
// 设置 canvas 的显示尺寸(CSS 像素)
canvas.style.width = '100%';
canvas.style.height = 'auto';
canvas.style.display = 'block';
// 启用图像平滑以获得更好的渲染质量
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = 'high';
// 渲染 PDF 页面
const renderContext = {
canvasContext: context,
viewport,
textLayer: false,
annotationLayer: false,
enableWebGL: false,
renderInteractiveForms: false
};
await page.render(renderContext).promise;
3.4 清晰度优化
为了在高分辨率屏幕上显示清晰的图片,做了以下优化:
JavaScript 层面:
- 根据设备像素比(
devicePixelRatio)动态调整缩放比例 - 限制最大缩放为 3 倍,避免内存占用过大
- 启用高质量图像平滑:
imageSmoothingQuality = 'high'
CSS 层面:
canvas {
// 确保 canvas 在高分辨率屏幕上显示清晰
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
image-rendering: pixelated;
// 防止图像模糊
backface-visibility: hidden;
transform: translateZ(0);
}
遇到的问题和解决方案
问题 1:跨域请求无法携带 Cookie
问题描述:PDF 文件在第三方域名,请求时无法携带 Cookie,导致认证失败。
解决方案:
- 在
getDocument配置中设置withCredentials: true - 确保服务端设置了正确的 CORS 响应头:
Access-Control-Allow-Credentials: true
问题 2:Worker 文件路径错误
问题描述:开发环境正常,但构建后 Worker 文件找不到。
解决方案: 使用 new URL() 和 import.meta.url 动态生成 Worker 路径,确保在构建后路径正确:
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
import.meta.url
).toString();
问题 3:Promise.withResolvers 兼容性问题
问题描述:新版本 PDF.js 使用了 Promise.withResolvers(),部分浏览器不支持。
解决方案: 使用 legacy 版本:pdfjs-dist/legacy/build/pdf.min.mjs
完整组件代码
以下是完整的 Vue 3 组件实现:
<template>
<div class="pdf-viewer">
<div v-if="loading" class="loading">发票加载中...</div>
<div v-else-if="error" class="error">发票加载失败</div>
<div ref="pdfContainer" class="pdf-container" />
</div>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.min.mjs';
const props = defineProps({
url: {
type: String,
default: ''
}
});
// 配置 PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const pdfContainer = ref(null);
const loading = ref(true);
const error = ref(false);
const loadPDF = async() => {
try {
// 加载 PDF 文档
const pdfDoc = await pdfjsLib.getDocument({
props.url, // 发票下载地址
disableWorker: false,
disableAutoFetch: false,
disableStream: false,
httpHeaders: {
'X-Requested-With': 'XMLHttpRequest'
},
withCredentials: true
}).promise;
const page = await pdfDoc.getPage(1);
// 设置视口,根据容器宽度自适应
const containerWidth = pdfContainer.value?.clientWidth || 750;
const baseScale = containerWidth / page.getViewport({ scale: 1 }).width;
const devicePixelRatio = window.devicePixelRatio || 1;
const scale = Math.min(baseScale * devicePixelRatio, 3);
const viewport = page.getViewport({ scale });
// 创建 canvas
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.style.width = '100%';
canvas.style.height = 'auto';
canvas.style.display = 'block';
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = 'high';
if (pdfContainer.value) {
pdfContainer.value.innerHTML = '';
pdfContainer.value.appendChild(canvas);
}
// 渲染 PDF 页面
const renderContext = {
canvasContext: context,
viewport,
textLayer: false,
annotationLayer: false,
enableWebGL: false,
renderInteractiveForms: false
};
await page.render(renderContext).promise;
loading.value = false;
} catch (err) {
console.error('PDF 加载失败:', err);
error.value = true;
loading.value = false;
}
};
onMounted(() => {
loadPDF();
});
</script>
<style lang="scss" scoped>
.pdf-viewer {
width: 100%;
min-height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.loading,
.error {
padding: 40px;
color: #666;
font-size: 28px;
}
.error {
color: #ff6b6b;
}
.pdf-container {
width: 100%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
canvas {
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
image-rendering: pixelated;
backface-visibility: hidden;
transform: translateZ(0);
}
}
}
</style>