一、前言
在前端开发中,批量 PDF 打印场景(如工单打印、文档批量导出打印、图纸批量打印)十分常见,但实际开发中往往会遇到一系列痛点:频繁创建 Canvas 导致浏览器卡顿、大量 PDF 并行处理引发内存溢出、打印内容无法添加定制化水印/二维码、不同浏览器打印样式错乱、PDF 渲染模糊等。
本文基于指定版本 pdfjs-dist,以“实战落地”为核心,手把手实现 PDF 转 Canvas 批量打印方案,重点解决性能瓶颈与复用性问题,所有代码可直接拷贝集成到 Vue 项目,兼顾高性能、高定制性与可复用性,帮你快速搞定批量 PDF 打印需求。
二、核心技术栈与依赖配置
本文方案聚焦“可复用、易集成”,选用成熟稳定的技术栈,所有依赖版本固定,避免版本冲突,具体配置如下:
2.1 依赖清单与安装命令
核心依赖为 pdfjs-dist(PDF 解析与渲染核心),辅助依赖用于时间格式化、二维码生成与 UI 提示,安装命令直接拷贝执行即可:
# 核心依赖:pdfjs-dist(指定版本2.2.228,稳定无兼容bug)
npm install "pdfjs-dist": "2.2.228" --save
# 辅助依赖(按需安装,项目已存在则无需重复)
# moment:时间格式化(水印时间生成)
# qrcode:生成二维码(打印内容标识)
# element-ui:加载提示与错误提示(Vue项目常用)
npm install moment@2.29.4 qrcode@1.5.3 element-ui@2.15.13 --save
2.2 依赖引入与基础配置
在 Vue 组件或入口文件中引入依赖,重点配置 pdfjs-dist 的 worker 路径与字体路径,避免中文渲染乱码,代码直接拷贝可用:
// 1. pdfjsLib 核心依赖引入(适配npm安装的2.2.228版本)
import pdfjsLib from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
// 关键配置:设置worker路径,避免渲染报错(npm安装后默认路径)
pdfjsLib.GlobalWorkerOptions.workerSrc = require("pdfjs-dist/build/pdf.worker.entry");
// 2. moment.js 引入与配置(中文时间格式)
import moment from "moment";
import "moment/locale/zh-cn";
moment.locale("zh-cn"); // 时间格式化默认中文(如:2026-02-12 15:30:00)
// 3. 二维码生成依赖引入与方法封装
import QRCode from "qrcode";
// 二维码生成方法(与打印逻辑联动,直接拷贝无需修改)
async function generateQrcode(text) {
return new Promise((resolve, reject) => {
QRCode.toDataURL(
text,
{
width: 50,
margin: 1,
color: { dark: "#000000", light: "#ffffff" },
},
(err, url) => {
if (err) reject(err);
resolve(url);
}
);
});
}
// 4. Element UI 引入(用于加载提示、错误提示,Vue项目适配)
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
Vue.use(ElementUI);
三、核心样式封装
批量打印的样式适配是关键,需兼顾“浏览器打印兼容性”与“定制化需求”(水印、二维码、页面分页),以下样式直接拷贝到 Vue 组件的 style 标签或项目样式文件,无需修改:
/* 批量打印专用样式 - 水印样式(工单号+产品名+时间) */
.watermark {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 2;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
opacity: 0.1;
user-select: none;
overflow: hidden;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.watermark-item {
display: flex;
align-items: center;
justify-content: center;
transform: rotate(-45deg);
font-size: 16px;
color: #000;
white-space: nowrap;
overflow: hidden;
}
/* 二维码+产品信息容器样式 */
.info-container {
position: absolute;
z-index: 1;
background: transparent;
display: flex;
flex-direction: column;
align-items: center;
}
.info-container .qr-code {
margin-bottom: 5px;
padding: 2px;
background: white;
border-radius: 4px;
border: 1px solid #ccc;
}
.info-container .product-info {
text-align: center !important;
font-size: 6px !important;
line-height: 1.4;
color: #333;
}
.info-container .product-info div {
margin: 2px 0;
font-size: 6px !important;
}
/* 打印适配核心样式(解决浏览器兼容性、空白页、分页问题) */
@media print {
@page {
margin: 0 !important;
padding: 0 !important;
size: auto;
}
html,
body {
margin: 0 !important;
padding: 0 !important;
height: 100%;
width: 100%;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
overflow: visible !important;
}
.page-container {
box-sizing: border-box;
page-break-after: always;
page-break-inside: avoid;
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 !important;
padding: 0 !important;
position: relative;
background: white;
}
.page-container:last-child {
page-break-after: avoid;
}
.page-content {
width: 98%;
height: 98%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.page-content img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
margin: 0 !important;
padding: 0 !important;
display: block;
}
}
四、核心逻辑实战开发
本章节是全文核心,所有代码封装为 Vue 组件 methods 方法,直接拷贝到组件中即可使用,重点实现“PDF 转 Canvas”“批量分批处理”“性能优化”“定制化内容添加”四大核心功能,每一步都有详细注释,便于理解与修改。
4.1 数据格式定义(必看)
批量打印需传入选中的打印数据(selectTableData),需包含指定字段,用于水印、二维码、PDF 地址获取,格式如下(直接拷贝到 Vue 组件 data 中,替换为真实数据即可):
data() {
return {
// 选中的打印数据(批量打印的核心数据源)
selectTableData: [
{
id: 1, // 可选:数据唯一标识,无实际业务作用
productImageUrl: "https://example.com/product1.pdf", // 必选:产品PDF地址(真实可访问)
workOrderNo: "WO2026021201", // 必选:工单号(用于水印、二维码内容)
porUrl: "https://example.com/por1.pdf", // 可选:POR文件PDF地址,无则传null
productName: "测试产品A", // 可选:产品名称(用于水印、页面标识)
figureCode: "FIG-001" // 可选:图号(用于打印文档标题)
},
{
id: 2,
productImageUrl: "https://example.com/product2.pdf",
workOrderNo: "WO2026021202",
porUrl: null, // 无POR文件,传null即可
productName: "测试产品B",
figureCode: "FIG-002"
}
]
};
}
4.2 入口方法:批量打印触发
该方法为批量打印的总入口,负责初始化加载提示、Canvas 复用池、分批处理 PDF、调用打印执行方法,同时处理异常捕获与内存清理,直接拷贝可用:
/**
* 批量打印处理 - 性能优化版(入口方法,直接调用此方法触发批量打印)
* 核心功能:初始化配置、分批处理、异常捕获、内存清理
*/
async handleBatchPrint() {
const { selectTableData } = this;
// 校验:无选中数据则提示,直接返回
if (!selectTableData || selectTableData.length === 0) {
this.$message.warning("请选择需要打印的数据");
return;
}
// 加载提示(Element UI),提升用户体验
const loading = this.$loading({
lock: true,
text: "准备打印数据...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
});
// Canvas复用池 - 核心性能优化:避免频繁创建/销毁Canvas,降低性能损耗
let canvasPool = {
canvas: null, // 主Canvas(PDF渲染)
context: null, // 主Canvas上下文
tempCanvas: null, // 临时Canvas(图片增强)
tempContext: null // 临时Canvas上下文
};
try {
// 1. 初始化Canvas复用池(仅创建一次,后续复用)
canvasPool.canvas = document.createElement("canvas");
canvasPool.context = canvasPool.canvas.getContext("2d", {
alpha: false, // 关闭透明通道,提升渲染性能
willReadFrequently: true, // 允许频繁读取像素,适配高分辨率渲染
desynchronized: true // 解除渲染同步,提升帧率
});
canvasPool.tempCanvas = document.createElement("canvas");
canvasPool.tempContext = canvasPool.tempCanvas.getContext("2d", {
alpha: false,
willReadFrequently: true,
desynchronized: true
});
// 2. 分批处理PDF(核心性能优化:降低内存峰值,避免卡顿)
const BATCH_SIZE = 5; // 每批处理5个PDF,可根据项目调整(建议5-10个)
const totalItems = selectTableData.length;
const allPages = []; // 存储所有处理完成的打印页面
// 分批循环处理
for (let batchStart = 0; batchStart < totalItems; batchStart += BATCH_SIZE) {
const batchEnd = Math.min(batchStart + BATCH_SIZE, totalItems);
const batch = selectTableData.slice(batchStart, batchEnd);
// 更新进度提示,让用户感知处理状态
loading.text = `处理中... (${batchStart + 1}-${batchEnd}/${totalItems})`;
// 并行处理当前批次的PDF,提升处理效率(不阻塞后续批次)
await Promise.all(
batch.map(async (item, batchIndex) => {
const index = batchStart + batchIndex;
const { productImageUrl, workOrderNo, porUrl, productName, figureCode } = item;
// 跳过无效数据(避免影响整体流程)
if (!productImageUrl || !workOrderNo) {
console.warn(`跳过无效打印数据(第${index + 1}条):`, item);
this.$message.warning(`跳过无效数据(第${index + 1}条):缺少工单号或PDF地址`);
return;
}
try {
// 生成二维码(工单号为二维码内容,用于打印标识)
const qrCode = await generateQrcode(workOrderNo);
// 处理产品PDF(第一页显示二维码,核心:PDF转Canvas)
const productPages = await this.processPdfWithOptimization(
productImageUrl,
workOrderNo,
productName,
qrCode,
canvasPool,
true // 产品PDF第一页显示二维码
);
allPages.push(...productPages);
// 处理POR PDF(若有,不显示二维码)
if (porUrl) {
const porPages = await this.processPdfWithOptimization(
porUrl,
workOrderNo,
productName,
null,
canvasPool,
false // POR PDF不显示二维码
);
allPages.push(...porPages);
}
// 批次最后一条数据处理完成后,触发垃圾回收(辅助释放内存)
if (batchIndex === batch.length - 1 && window.gc) {
window.gc();
}
} catch (error) {
// 单PDF处理失败,不阻塞整体流程,仅提示错误
console.error(`处理第 ${index + 1} 个PDF时出错:`, error);
this.$message.error(`处理第 ${index + 1} 个PDF时出错: ${error.message}`);
}
})
);
}
// 校验:无可用打印页面,抛出异常并提示
if (allPages.length === 0) {
throw new Error("没有可打印的页面,请检查PDF地址是否有效");
}
// 生成打印文档并执行打印
loading.text = "生成打印文档...";
await this.executePrint(allPages, selectTableData);
} catch (error) {
// 整体打印失败,提示用户并打印错误日志
console.error("批量打印失败:", error);
this.$message.error(`打印失败: ${error.message}`);
} finally {
// 核心内存优化:清理Canvas池,释放内存,避免内存泄漏
if (canvasPool.canvas) {
canvasPool.canvas.width = 0;
canvasPool.canvas.height = 0;
canvasPool.canvas = null;
}
if (canvasPool.tempCanvas) {
canvasPool.tempCanvas.width = 0;
canvasPool.tempCanvas.height = 0;
canvasPool.tempCanvas = null;
}
canvasPool.context = null;
canvasPool.tempContext = null;
// 重置文档标题,关闭加载提示
document.title = "批量PDF打印";
loading.close();
}
}
4.3 核心方法:PDF 转 Canvas 优化处理
该方法是“PDF 转 Canvas”的核心,负责加载 PDF、高分辨率渲染、图片增强、页面清理,复用 Canvas 池提升性能,直接拷贝可用:
/**
* 优化的PDF处理方法 - 核心:PDF转Canvas,复用Canvas池提升性能
* @param {string} pdfUrl - PDF文件URL(必填)
* @param {string} workOrderNo - 工单号(用于水印/二维码)
* @param {string} productName - 产品名称(用于水印/标识)
* @param {string} qrCode - 二维码Base64(可选)
* @param {Object} canvasPool - Canvas复用池(直接传入,无需处理)
* @param {boolean} showQrOnFirst - 是否在第一页显示二维码
* @returns {Promise<Array>} 处理后的页面容器数组(供打印使用)
*/
async processPdfWithOptimization(
pdfUrl,
workOrderNo,
productName,
qrCode,
canvasPool,
showQrOnFirst
) {
const pages = [];
try {
// 加载PDF文档(适配pdfjs-dist,配置cMap避免中文乱码)
const pdfDoc = await pdfjsLib.getDocument({
url: pdfUrl,
cMapUrl: 'pdfjs-dist/cmaps/', // npm安装后默认字体路径,无需修改
cMapPacked: true,
standardFontDataUrl: 'pdfjs-dist/standard_fonts/', // 标准字体路径
enableXfa: true,
disableFontFace: false,
useSystemFonts: true,
maxCanvasPixels: 33554432 // 限制Canvas最大像素,避免内存溢出
}).promise;
const numPages = pdfDoc.numPages; // PDF总页数
// 循环处理PDF的每一页
for (let pageIndex = 0; pageIndex < numPages; pageIndex++) {
const pageNum = pageIndex + 1;
const page = await pdfDoc.getPage(pageNum);
// 复用Canvas池中的元素,动态调整Canvas尺寸(适配当前PDF页面)
const { canvas, context, tempCanvas, tempContext } = canvasPool;
const viewport = page.getViewport({ scale: 8.0 }); // 高分辨率渲染(保证打印清晰)
canvas.width = viewport.width;
canvas.height = viewport.height;
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
// 设置渲染质量,启用图片增强(提升打印清晰度)
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = "high";
context.fillStyle = "white";
context.fillRect(0, 0, canvas.width, canvas.height);
// 渲染PDF页面到Canvas(核心:启用WebGL加速,提升渲染速度)
await page.render({
canvasContext: context,
viewport: viewport,
intent: "print", // 打印模式渲染,提升打印质量
renderInteractiveForms: true,
background: "white", // 背景设为白色,避免打印透明
enableWebGL: true, // WebGL加速渲染,核心性能优化
disableRange: false,
disableStream: false,
disableAutoFetch: false,
}).promise;
// 应用图片增强滤镜(锐化、对比度调整),进一步提升打印质量
tempContext.filter = "contrast(1.3) saturate(1.2) brightness(1.05)";
tempContext.drawImage(canvas, 0, 0);
// 转换为高质量PNG图片(质量1.0,无损打印)
const imgDataUrl = tempCanvas.toDataURL("image/png", 1.0);
const img = await this.createImageFromDataUrl(imgDataUrl);
// 创建页面容器(添加水印、二维码、产品信息)
const pageContainer = this.createPageContainer(
img,
workOrderNo,
productName,
qrCode,
showQrOnFirst && pageNum === 1,
viewport
);
pages.push(pageContainer);
// 清理页面对象,释放内存(核心优化)
page.cleanup();
}
// 清理PDF文档,释放内存
pdfDoc.cleanup();
pdfDoc.destroy();
} catch (error) {
console.error("PDF处理失败:", error);
throw new Error(`PDF处理失败(${pdfUrl}): ${error.message}`);
}
return pages;
}
4.4 工具方法:辅助功能封装
以下方法为辅助功能,负责图片创建、页面容器创建、水印创建、信息容器创建,直接拷贝到 methods 中即可,无需修改:
/**
* 工具方法:从DataURL创建图片元素(适配Canvas转图片渲染)
* @param {string} dataUrl - 图片DataURL
* @returns {Promise<HTMLImageElement>} 图片元素
*/
createImageFromDataUrl(dataUrl) {
return new Promise((resolve, reject) => {
const img = document.createElement("img");
img.onload = () => resolve(img); // 图片加载完成回调
img.onerror = () => reject(new Error("图片加载失败")); // 加载失败回调
img.src = dataUrl;
});
},
/**
* 工具方法:创建页面容器(添加水印、二维码、产品信息)
* @param {HTMLImageElement} img - 图片元素(PDF转Canvas后的图片)
* @param {string} workOrderNo - 工单号
* @param {string} productName - 产品名称
* @param {string} qrCode - 二维码
* @param {boolean} showQr - 是否显示二维码
* @param {Object} viewport - 视口对象
* @returns {HTMLElement} 页面容器(供打印使用)
*/
createPageContainer(img, workOrderNo, productName, qrCode, showQr, viewport) {
// 页面容器(适配打印分页,每一页一个容器)
const pageContainer = document.createElement("div");
pageContainer.style.position = "relative";
pageContainer.style.width = "100%";
pageContainer.style.height = "100vh";
pageContainer.style.margin = "0";
pageContainer.style.padding = "0";
pageContainer.style.pageBreakAfter = "always";
// 添加水印(工单号+产品名+当前时间,避免打印内容混淆)
const watermark = this.createWatermark(workOrderNo, productName);
pageContainer.appendChild(watermark);
// 添加PDF转换后的图片(核心打印内容)
img.style.width = "98%";
img.style.height = "98%";
img.style.maxWidth = "98%";
img.style.maxHeight = "98%";
img.style.objectFit = "contain";
pageContainer.appendChild(img);
// 第一页添加二维码和产品信息(打印标识)
if (showQr && qrCode) {
const infoContainer = this.createInfoContainer(
qrCode,
workOrderNo,
productName,
viewport
);
pageContainer.appendChild(infoContainer);
}
return pageContainer;
},
/**
* 工具方法:创建水印元素(工单号+产品名+时间)
* @param {string} workOrderNo - 工单号
* @param {string} productName - 产品名称
* @returns {HTMLElement} 水印容器
*/
createWatermark(workOrderNo, productName) {
const watermark = document.createElement("div");
watermark.className = "watermark";
// 水印内容:工单号+产品名+当前时间(精确到分秒,便于追溯)
const watermarkText = `${workOrderNo} ${productName || ""} ${moment().format("YYYY-MM-DD HH:mm:ss")}`;
// 创建4x4网格水印(16个元素,均匀分布在页面,避免遮挡核心内容)
for (let i = 0; i < 16; i++) {
const watermarkItem = document.createElement("div");
watermarkItem.className = "watermark-item";
watermarkItem.textContent = watermarkText;
watermark.appendChild(watermarkItem);
}
return watermark;
},
/**
* 工具方法:创建信息容器(二维码+产品信息)
* @param {string} qrCode - 二维码Base64
* @param {string} workOrderNo - 工单号
* @param {string} productName - 产品名称
* @param {Object} viewport - 视口对象
* @returns {HTMLElement} 信息容器
*/
createInfoContainer(qrCode, workOrderNo, productName, viewport) {
const qrSize = 50; // 二维码尺寸
const qrPadding = 2; // 二维码内边距
const scale = viewport.scale || 1;
const safeMargin = 3 * scale; // 安全边距,避免内容被浏览器裁剪
const infoContainer = document.createElement("div");
infoContainer.className = "info-container";
infoContainer.style.top = `${safeMargin}px`;
infoContainer.style.left = `${safeMargin}px`;
// 二维码容器(添加边框和背景,提升识别度)
const qrContainer = document.createElement("div");
qrContainer.className = "qr-code";
qrContainer.style.padding = `${qrPadding}px`;
qrContainer.style.background = "white";
qrContainer.style.borderRadius = "4px";
qrContainer.style.border = "1px solid #ccc";
// 二维码图片
const qrImage = document.createElement("img");
qrImage.src = qrCode;
qrImage.style.width = `${qrSize}px`;
qrImage.style.height = `${qrSize}px`;
qrImage.style.display = "block";
qrContainer.appendChild(qrImage);
infoContainer.appendChild(qrContainer);
// 产品信息容器(显示工单号和产品名,便于识别)
const productInfo = document.createElement("div");
productInfo.className = "product-info";
const workOrderNoDiv = document.createElement("div");
workOrderNoDiv.textContent = `${workOrderNo}`;
productInfo.appendChild(workOrderNoDiv);
const productNameDiv = document.createElement("div");
productNameDiv.textContent = `${productName || "-"}`;
productInfo.appendChild(productNameDiv);
infoContainer.appendChild(productInfo);
return infoContainer;
}
4.5 执行打印方法:触发浏览器打印
该方法负责创建打印容器(iframe)、拼接打印内容、触发浏览器打印,打印完成后清理 iframe 释放内存,直接拷贝可用:
/**
* 执行打印操作(核心方法,触发浏览器打印)
* @param {Array} allPages - 所有页面容器(processPdfWithOptimization返回)
* @param {Array} selectTableData - 选中的表格数据(用于设置打印标题)
*/
async executePrint(allPages, selectTableData) {
// 创建隐藏iframe作为打印容器(避免污染主页面DOM,提升兼容性)
const printFrame = document.createElement("iframe");
printFrame.style.display = "none";
document.body.appendChild(printFrame);
// 拼接打印内容(包含所有页面+打印样式,确保打印格式正确)
const printContent = `
<!DOCTYPE html>
批量打印
${allPages
.map((page) => {
const pageContent = document.createElement("div");
pageContent.className = "page-content";
while (page.firstChild) {
pageContent.appendChild(page.firstChild);
}
page.appendChild(pageContent);
page.className = "page-container";
return page.outerHTML;
})
.join("")}
`;
// 写入iframe并执行打印
const frameDoc = printFrame.contentWindow.document;
frameDoc.open();
frameDoc.write(printContent);
frameDoc.close();
// 等待内容加载完成(避免打印空白,适配不同浏览器加载速度)
await new Promise((resolve) => setTimeout(resolve, 1000));
// 设置打印文档标题(工单号+产品名,便于识别打印任务)
const productName = selectTableData[0].productName || "批量打印";
const figureCode = selectTableData[0].figureCode || "";
document.title = `${figureCode}_${productName}`;
// 触发浏览器打印
printFrame.contentWindow.print();
// 打印完成后,移除iframe,释放内存(核心优化)
printFrame.contentWindow.onafterprint = () => {
document.body.removeChild(printFrame);
};
}
五、调用方式
所有代码拷贝完成后,仅需添加一个触发按钮,调用 handleBatchPrint 方法即可实现批量打印,直接拷贝到 Vue 模板中:
<!-- Vue模板:批量打印触发按钮 -->
<!-- 批量打印按钮(Element UI样式,可替换为自身项目按钮) -->
<el-button type="primary" icon="el-icon-printer" @click="handleBatchPrint">
批量打印
</el-button>
六、常见问题排查
集成过程中可能遇到以下问题,整理了对应的排查方法,快速解决集成难题:
-
问题 1:PDF 加载失败/中文渲染乱码 → 排查:1. PDF URL 是否真实可访问、跨域是否配置 CORS;2. pdfjs-dist 的 cMap 路径是否正确(本文配置无需修改);3. 确认 pdfjs-dist 版本为 2.2.228;
-
问题 2:打印空白页 → 排查:1. iframe 内容是否写入成功(可临时注释 display: none 查看 iframe 内容);2. setTimeout 等待时间是否足够(可调整为 1500ms);3. Canvas 渲染是否正常;
-
问题 3:二维码不显示 → 排查:1. generateQrcode 方法是否返回正确的 Base64 格式;2. showQrOnFirst 参数是否为 true;3. 二维码容器样式是否被遮挡;
-
问题 4:浏览器卡顿/内存溢出 → 排查:1. 减小批次大小(如改为 3 个/批);2. 检查是否有未清理的 Canvas/iframe;3. 升级浏览器到最新版本(推荐 Chrome);
-
问题 5:打印样式错乱 → 排查:1. 打印样式是否完整拷贝;2. 浏览器打印设置中是否勾选“背景图形”(部分浏览器默认不勾选,导致水印不显示);3. 确认@media print 样式未被覆盖。
原创不易,承蒙厚爱,感谢每一份认可与赞赏✨