打印功能在前端开发中一直是个令人头疼的问题。我见过太多项目因为打印样式问题导致交付延期,也遇到过不少开发者因为打印兼容性问题加班到深夜。常见的打印翻车场景包括:
这些问题的根源在于浏览器打印机制的特殊性。打印时浏览器会创建一个新的打印上下文,这个上下文与屏幕渲染环境存在诸多差异:
常见的应对方案各有缺陷:
css复制/* 方案1:使用@media print */
@media print {
.no-print { display: none; }
.only-print { display: block; }
}
问题:需要为每个项目重新编写大量打印样式,维护成本高
javascript复制// 方案2:使用window.print()的beforeprint/afterprint事件
window.addEventListener('beforeprint', () => {
// 临时修改DOM
});
问题:逻辑复杂,容易产生副作用,无法解决跨浏览器兼容性
经过多个项目的实践验证,我强烈推荐使用react-to-print这个NPM库(React项目)或其底层依赖的print-js(通用项目)。它们解决了以下核心问题:
安装:
bash复制npm install react-to-print
基础用法:
jsx复制import React, { useRef } from 'react';
import { useReactToPrint } from 'react-to-print';
const PrintableComponent = () => (
<div className="print-area">
{/* 打印内容 */}
</div>
);
export default function App() {
const printRef = useRef();
const handlePrint = useReactToPrint({
content: () => printRef.current,
});
return (
<>
<PrintableComponent ref={printRef} />
<button onClick={handlePrint}>打印</button>
</>
);
}
关键配置项:
javascript复制const handlePrint = useReactToPrint({
pageStyle: `
@media print {
@page { size: A4 landscape; margin: 0; }
body { -webkit-print-color-adjust: exact; }
}
`,
removeAfterPrint: true,
onBeforeGetContent: () => new Promise(resolve => {
// 打印前数据处理
resolve();
})
});
问题:大型表格跨页时表头丢失、边框断裂
解决方案:
css复制@media print {
table {
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
tr {
break-inside: avoid;
}
}
配合库的onBeforePrint回调动态计算分页:
javascript复制onBeforePrint: () => {
const rows = document.querySelectorAll('tr');
rows.forEach((row, index) => {
if (index % 30 === 0 && index !== 0) {
row.style.pageBreakBefore = 'always';
}
});
}
关键CSS:
css复制@page {
size: 210mm 297mm; /* A4标准尺寸 */
margin: 10mm;
marks: crop cross; /* 裁切标记 */
}
推荐方案:
jsx复制<button
onClick={handlePrint}
style={{
padding: '8px 16px',
background: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px'
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M6 9V2H18V9" stroke="currentColor"/>
<path d="M6 18H4C2.89543 18 2 17.1046 2 16V11C2 9.89543 2.89543 9 4 9H20C21.1046 9 22 9.89543 22 11V16C22 17.1046 21.1046 18 20 18H18" stroke="currentColor"/>
<path d="M18 14H6V22H18V14Z" stroke="currentColor"/>
</svg>
打印文档
</button>
检查清单:
-webkit-print-color-adjust: exact解决方案:
css复制.print-content {
break-inside: avoid;
overflow: visible !important;
}
特征检测方案:
javascript复制const isFirefox = navigator.userAgent.includes('Firefox');
const handlePrint = useReactToPrint({
pageStyle: isFirefox ? firefoxPrintStyles : standardPrintStyles
});
javascript复制const handlePrint = async () => {
const { default: printJS } = await import('print-js');
printJS(...);
}
javascript复制onBeforeGetContent: () => {
const buttons = document.querySelectorAll('button');
buttons.forEach(btn => {
btn.disabled = true;
});
}
html复制<link rel="preload" href="print.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="print.css"></noscript>
对于复杂系统建议采用以下架构:
code复制打印服务层
├── 模板管理
├── 数据预处理
├── 样式注入
└── 输出控制
实现示例:
javascript复制class PrintService {
constructor() {
this.templates = new Map();
}
registerTemplate(name, component) {
this.templates.set(name, component);
}
async print(templateName, data) {
const template = this.templates.get(templateName);
const instance = ReactDOM.render(template(data), document.createElement('div'));
await new Promise(resolve => {
useReactToPrint({
content: () => instance,
onAfterPrint: resolve
})();
});
ReactDOM.unmountComponentAtNode(instance);
}
}
jsx复制<button
onTouchEnd={handlePrint}
style={{
minWidth: '44px',
minHeight: '44px'
}}
>
打印
</button>
css复制@media print and (max-width: 768px) {
.print-content {
font-size: 12px !important;
}
}
javascript复制// 在渲染打印内容前进行清理
import DOMPurify from 'dompurify';
const cleanHTML = DOMPurify.sanitize(userContent);
javascript复制onBeforeGetContent: () => {
document.querySelectorAll('.sensitive').forEach(el => {
el.style.display = 'none';
});
}
自动化测试策略:
javascript复制describe('打印功能', () => {
beforeAll(() => {
window.print = jest.fn();
});
test('应触发打印对话框', () => {
render(<PrintButton />);
fireEvent.click(screen.getByText('打印'));
expect(window.print).toHaveBeenCalled();
});
test('打印内容应包含关键数据', () => {
const { container } = render(<PrintableComponent data={testData} />);
expect(container).toHaveTextContent(testData.key);
});
});
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| react-to-print | React友好,API简洁 | 仅限React项目 | 现代React应用 |
| print-js | 通用性强,支持多种格式 | 配置稍复杂 | 多框架环境 |
| window.print() | 无需依赖 | 功能有限 | 简单需求 |
| PDF生成后打印 | 格式精确 | 服务器负载高 | 复杂报表 |
Chrome打印预览调试:
实时样式检查:
javascript复制const checkPrintStyles = () => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.media = 'print';
link.href = 'print.css';
document.head.appendChild(link);
// 检查应用样式
console.log(getComputedStyle(element));
};
css复制body {
font-family: "Times New Roman", Times, serif;
}
打印性能埋点:
javascript复制const startTime = performance.now();
handlePrint().then(() => {
const duration = performance.now() - startTime;
analytics.track('print_completed', { duration });
});
异常监控:
javascript复制try {
await handlePrint();
} catch (error) {
Sentry.captureException(error);
showToast('打印失败,请重试');
}
屏幕阅读器支持:
jsx复制<button
aria-label="打印文档"
onClick={handlePrint}
>
<span aria-hidden="true">🖨️</span>
打印
</button>
键盘导航:
javascript复制useEffect(() => {
const handler = (e) => {
if (e.key === 'p' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
handlePrint();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, []);
异步数据加载:
javascript复制const [printReady, setPrintReady] = useState(false);
useEffect(() => {
fetchData().then(() => {
setPrintReady(true);
});
}, []);
const handlePrint = useReactToPrint({
onBeforeGetContent: () => {
if (!printReady) {
return Promise.reject('数据未加载完成');
}
return Promise.resolve();
}
});
与PDF库结合:
javascript复制import { jsPDF } from 'jspdf';
const exportPDF = async () => {
const { contentWindow } = document.getElementById('printFrame');
const pdf = new jsPDF();
await pdf.html(contentWindow.document.body);
pdf.save('document.pdf');
};
Next.js示例:
javascript复制import dynamic from 'next/dynamic';
const PrintButton = dynamic(
() => import('react-to-print').then(mod => ({
default: mod.PrintButton
})),
{ ssr: false }
);
暗黑模式打印处理:
css复制@media print {
:root {
--text-color: #000 !important;
--bg-color: #fff !important;
}
}
队列实现:
javascript复制class PrintQueue {
constructor() {
this.queue = [];
this.isProcessing = false;
}
add(task) {
this.queue.push(task);
this.process();
}
async process() {
if (this.isProcessing) return;
this.isProcessing = true;
while (this.queue.length) {
await this.queue.shift()();
}
this.isProcessing = false;
}
}
打印进度提示:
jsx复制const [printState, setPrintState] = useState('idle');
const handlePrint = useReactToPrint({
onBeforeGetContent: () => {
setPrintState('preparing');
return prepareData();
},
onAfterPrint: () => setPrintState('completed')
});
return (
<div>
{printState === 'preparing' && <Spinner />}
<button onClick={handlePrint}>
{printState === 'preparing' ? '准备中...' : '打印'}
</button>
</div>
);
多语言打印:
javascript复制const printContent = {
en: <EnglishTemplate />,
zh: <ChineseTemplate />
};
const handlePrint = useReactToPrint({
content: () => printContent[i18n.language]
});
用户行为追踪:
javascript复制const printTracker = () => {
const startTime = Date.now();
let printed = false;
window.addEventListener('beforeprint', () => {
printed = true;
analytics.track('print_started', {
duration: Date.now() - startTime
});
});
window.addEventListener('afterprint', () => {
if (printed) {
analytics.track('print_completed');
}
});
};
样式缓存方案:
javascript复制const printStyleCache = new Map();
const getPrintStyle = (id) => {
if (!printStyleCache.has(id)) {
const style = generateStyle(id);
printStyleCache.set(id, style);
}
return printStyleCache.get(id);
};
React错误边界:
jsx复制class PrintErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <FallbackUI />;
}
return this.props.children;
}
}
<PrintErrorBoundary>
<PrintableContent />
</PrintErrorBoundary>
虽然当前方案已经成熟,但打印技术仍在演进。值得关注的趋势包括:
@page规则增强这些发展将进一步简化前端打印的实现方式,但现阶段react-to-print和print-js仍然是最可靠的解决方案。在实际项目中,建议结合业务需求选择合适的打印策略,并建立完善的打印样式管理体系。