1. Vue3项目中集成Print.js的核心价值
在前端开发中,打印功能一直是刚需但容易被忽视的环节。传统window.print()方法存在样式不可控、无法指定打印区域等问题,而Print.js这个轻量级库(仅14KB gzipped)完美解决了这些痛点。特别是在Vue3组合式API环境下,配合ref获取DOM节点的能力,可以实现精准的局部打印控制。
我曾在一个电商后台系统中遇到这样的需求:订单详情页需要支持打印发货单,但页面上同时存在导航栏、操作按钮等不需要打印的元素。通过Print.js的方案,我们最终实现了:
- 仅打印订单明细区域
- 自动应用打印专用样式
- 保持原始页面布局不受影响
- 支持PDF导出功能
2. 基础集成与配置
2.1 安装与引入
首先通过npm安装最新版Print.js(当前稳定版为1.6.0):
npm install print-js --save在Vue3中有两种引入方式:
全局引入(推荐):
// main.js import { createApp } from 'vue' import printJS from 'print-js' import App from './App.vue' const app = createApp(App) app.config.globalProperties.$print = printJS app.mount('#app')按需引入:
<script setup> import printJS from 'print-js' const handlePrint = () => { printJS({ printable: 'printArea', type: 'html', style: '@page { size: A4; margin: 0 }' }) } </script>2.2 打印区域准备
在模板中准备打印区域时,推荐使用独立的打印组件模式:
<template> <div class="page-container"> <!-- 页面常规内容 --> <button @click="handlePrint">打印</button> <!-- 隐藏的打印专用内容 --> <div ref="printContent" class="print-content"> <h2>订单详情</h2> <table>...</table> </div> </div> </template> <style> .print-content { display: none; } @media print { body * { visibility: hidden; } .print-content, .print-content * { visibility: visible; } .print-content { position: absolute; left: 0; top: 0; width: 100%; } } </style>3. 高级打印功能实现
3.1 动态内容打印
实际项目中经常需要打印动态生成的内容。我们可以结合Vue的响应式特性实现:
<script setup> import { ref } from 'vue' import printJS from 'print-js' const printData = ref({ title: '动态报表', items: [] }) const generateReport = () => { // 模拟API请求 setTimeout(() => { printData.value.items = [ { id: 1, name: '商品A', price: 99 }, { id: 2, name: '商品B', price: 199 } ] printJS({ printable: { html: ` <div class="report"> <h2>${printData.value.title}</h2> <table> ${printData.value.items.map(item => ` <tr> <td>${item.id}</td> <td>${item.name}</td> <td>${item.price}</td> </tr> `).join('')} </table> </div> ` }, type: 'html', style: ` .report { padding: 20px; } table { width: 100%; border-collapse: collapse; } td, th { border: 1px solid #ddd; padding: 8px; } ` }) }, 300) } </script>3.2 表格打印优化
Element Plus等UI库的表格直接打印常会出现样式问题,这里提供专业解决方案:
const printElTable = (tableRef) => { const tableHtml = tableRef.value.$el.outerHTML const styles = ` .el-table { width: 100% !important; } .el-table__header { display: table-header-group !important; } .el-table__body { display: table-row-group !important; } @page { size: A4 landscape; margin: 5mm; } ` printJS({ printable: tableHtml, type: 'html', style: styles, scanStyles: false // 禁用自动扫描样式 }) }3.3 PDF打印集成
结合后端服务实现PDF打印:
const printPDF = async (content) => { try { const response = await fetch('/api/generate-pdf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ html: content }) }) const blob = await response.blob() const pdfUrl = URL.createObjectURL(blob) printJS({ printable: pdfUrl, type: 'pdf', onLoadingEnd: () => URL.revokeObjectURL(pdfUrl) // 释放内存 }) } catch (error) { console.error('PDF生成失败:', error) } }4. 样式控制与打印优化
4.1 打印专用CSS
创建独立的print.css文件:
/* print.css */ @media print { body { background: white !important; color: black !important; font-size: 12pt; } .no-print { display: none !important; } .page-break { page-break-after: always; } table { page-break-inside: avoid; } h1, h2, h3 { page-break-after: avoid; } }在Vue组件中引入:
<style> @import url('@/assets/print.css'); </style>4.2 分页控制技巧
对于长内容打印,需要手动控制分页:
<div class="print-section"> <!-- 第一页内容 --> </div> <div class="page-break"></div> <div class="print-section"> <!-- 第二页内容 --> </div>对应的CSS:
.page-break { display: none; } @media print { .page-break { display: block; height: 0; page-break-after: always; } }5. 常见问题与解决方案
5.1 打印空白页问题
原因分析:
- 打印内容高度为0
- 打印时机过早,DOM未渲染完成
- CSS visibility属性冲突
解决方案:
const safePrint = (elementId) => { // 确保DOM更新完成 nextTick(() => { // 临时显示打印区域 const el = document.getElementById(elementId) el.style.display = 'block' printJS({ printable: elementId, type: 'html', onPrintDialogClose: () => { // 恢复隐藏状态 el.style.display = 'none' } }) }) }5.2 表格跨页断裂
优化表格打印的CSS方案:
@media print { table { page-break-inside: auto; } tr { page-break-inside: avoid; page-break-after: auto; } thead { display: table-header-group; } tfoot { display: table-footer-group; } }5.3 图片打印模糊
高DPI图片打印方案:
printJS({ printable: 'high-res-image', type: 'html', style: ` @page { size: A4; margin: 0; } img { width: 100%; height: auto; -webkit-print-color-adjust: exact; print-color-adjust: exact; } `, targetStyles: ['*'] // 继承所有样式 })6. 性能优化实践
6.1 延迟加载打印资源
<script setup> import { defineAsyncComponent } from 'vue' const PrintButton = defineAsyncComponent(() => import('@/components/PrintButton.vue') ) </script>6.2 打印内容缓存
const printCache = new Map() const getCachedPrintContent = (id, generator) => { if (printCache.has(id)) { return printCache.get(id) } const content = generator() printCache.set(id, content) return content }6.3 批量打印优化
对于批量打印场景,建议:
- 使用Promise.all并行处理
- 添加延迟避免浏览器阻塞
- 提供进度反馈
const batchPrint = async (items) => { const results = [] for (let i = 0; i < items.length; i++) { const result = await printItem(items[i]) results.push(result) // 每打印5个暂停一下 if (i % 5 === 0) { await new Promise(resolve => setTimeout(resolve, 500)) } } return results }7. 企业级解决方案
7.1 打印服务架构设计
前端层(Vue3) ↓ HTTP/WebSocket API网关 ↓ 打印服务集群 ├── 任务队列(Redis) ├── PDF生成器(Puppeteer) └── 打印机管理7.2 安全控制方案
// 打印权限校验中间件 const printAuth = (req, res, next) => { const licenseKey = req.headers['x-print-license'] if (!validateLicense(licenseKey)) { return res.status(403).json({ error: 'Invalid print license' }) } next() } // 前端调用带鉴权 const printWithAuth = (content) => { return fetch('/api/print', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Print-License': 'your-license-key' }, body: JSON.stringify({ content }) }) }7.3 打印日志审计
// 打印日志记录 const printLogger = { records: [], log(action, content) { const record = { timestamp: new Date(), action, contentHash: md5(content), user: currentUser } this.records.push(record) this.saveToServer(record) }, saveToServer(record) { // 发送到日志服务 } } // 包装打印方法 const auditedPrint = (content) => { printLogger.log('print', content) return printJS(content) }8. 移动端适配方案
8.1 响应式打印样式
@media print and (max-width: 768px) { .print-content { font-size: 14px; padding: 5px; } table { display: block; overflow-x: auto; } }8.2 移动端唤醒打印
const mobilePrint = () => { if (/Android|iPhone/i.test(navigator.userAgent)) { // 移动端使用新窗口打印 const printWindow = window.open('', '_blank') printWindow.document.write(printContent.value) printWindow.document.close() setTimeout(() => { printWindow.print() }, 500) } else { // PC端正常打印 printJS({ printable: printContent.value, type: 'html' }) } }9. 测试方案设计
9.1 单元测试示例
import { mount } from '@vue/test-utils' import PrintComponent from '@/components/PrintComponent.vue' describe('PrintComponent', () => { it('should generate correct print content', async () => { const wrapper = mount(PrintComponent) await wrapper.setData({ items: [{ id: 1, name: 'Test' }] }) const printContent = wrapper.vm.generatePrintContent() expect(printContent).toContain('<td>Test</td>') }) it('should call printJS when button clicked', async () => { const printJS = jest.fn() const wrapper = mount(PrintComponent, { global: { mocks: { $print: printJS } } }) await wrapper.find('button').trigger('click') expect(printJS).toHaveBeenCalled() }) })9.2 E2E测试方案
describe('Print Functionality', () => { it('should print order details', () => { cy.visit('/orders/123') cy.get('[data-test="print-button"]').click() cy.window().then(win => { const printStub = cy.stub(win, 'print') cy.get('[data-test="print-dialog"]').should('be.visible') expect(printStub).to.be.calledOnce }) }) })10. 项目实战经验
在最近一个ERP系统项目中,我们实现了以下打印增强功能:
- 模板系统:允许用户保存常用打印模板
const savedTemplates = ref([]) const saveTemplate = (name, html) => { savedTemplates.value.push({ id: Date.now(), name, html, createdAt: new Date() }) }- 打印预览:基于iframe实现WYSIWYG预览
<template> <div class="preview-container"> <iframe ref="previewFrame" class="preview-frame"></iframe> <button @click="updatePreview">更新预览</button> </div> </template> <script setup> const previewFrame = ref(null) const updatePreview = () => { const iframeDoc = previewFrame.value.contentDocument iframeDoc.open() iframeDoc.write(printContent.value) iframeDoc.close() } </script>- 多语言打印:动态切换打印内容语言
const printInLanguage = (lang) => { const i18nContent = { en: { title: 'Order' }, zh: { title: '订单' } } printJS({ printable: ` <div class="print-content"> <h2>${i18nContent[lang].title}</h2> ... </div> `, type: 'html' }) }- 打印队列管理:处理批量打印任务
class PrintQueue { constructor() { this.queue = [] this.isPrinting = false } add(task) { this.queue.push(task) this.process() } async process() { if (this.isPrinting || this.queue.length === 0) return this.isPrinting = true const task = this.queue.shift() try { await printJS(task.config) task.onSuccess?.() } catch (error) { task.onError?.(error) } finally { this.isPrinting = false this.process() } } }