引言
在Web应用开发中,将HTML内容转换为PDF并进行打印一直是一个技术难点。传统的解决方案如window.print()
存在样式丢失、兼容性差等问题。最近在研究一个基于Electron的Web打印项目npm包web-print-pdf,发现它巧妙地使用了Playwright无头浏览器来解决这个问题
传统Web打印的痛点
1. 浏览器兼容性问题
javascript
// 传统的打印方式
window.print();
这种方式存在以下问题:
- 不同浏览器渲染效果差异巨大
- CSS样式支持不完整
- 无法精确控制打印参数
2. 样式控制困难
css
@media print {
/* 打印样式经常失效 */
.no-print { display: none; }
.print-only { display: block; }
}
3. 功能单一
- 无法批量处理
- 缺乏预览功能
- 打印参数配置有限
Playwright的解决方案
核心思路
使用Playwright无头浏览器来渲染HTML内容,然后生成高质量的PDF文档。这种方式完美解决了传统Web打印的各种痛点。
技术架构
css
HTML内容 → Playwright无头浏览器 → PDF生成 → 打印服务
关键技术实现
1. Playwright浏览器启动
javascript
const { chromium } = require('playwright');
class GeneratePdf {
constructor() {
this.browser = null;
this.context = null;
this.page = null;
}
async _getLaunchOptions() {
return {
headless: true,
ignoreHTTPSErrors: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'-disable-web-security',
"--disable-audio-output"
],
};
}
async _launchBrowser() {
const launchOptions = this._getLaunchOptions();
this.browser = await chromium.launch(launchOptions);
this.context = await this.browser.newContext();
this.page = await this.context.newPage();
}
}
2. HTML内容渲染
javascript
async _generatePdfByChrome(url, args = {}, extraOptions = {}) {
try {
await this._launchBrowser();
// 设置页面内容
await this.page.setContent(htmlContent, {
timeout: extraOptions.requestTimeout || 15000,
waitUntil: 'networkidle'
});
// 等待页面完全加载
await this.page.waitForLoadState('networkidle');
// 生成PDF
const pdfBuffer = await this.page.pdf({
...args,
format: 'A4',
printBackground: true,
margin: {
top: '20px',
bottom: '20px',
left: '20px',
right: '20px'
}
});
return pdfBuffer;
} finally {
await this.cleanup();
}
}
3. 高级PDF配置
javascript
const pdfOptions = {
// 纸张格式
format: 'A4', // A0, A1, A2, A3, A4, A5, A6, Letter, Legal等
// 自定义尺寸
width: '210mm',
height: '297mm',
// 页边距
margin: {
top: '20px',
bottom: '20px',
left: '20px',
right: '20px'
},
// 打印背景
printBackground: true,
// 横向/纵向
landscape: false,
// 页面范围
pageRanges: '1-5, 7, 9-12',
// 缩放
scale: 1.0,
// 首选项
preferCSSPageSize: false
};
4. 网络请求处理
javascript
// 设置请求头
await this.page.setExtraHTTPHeaders({
'Authorization': 'Bearer token123',
'User-Agent': 'Custom Agent'
});
// 设置Cookie
await this.context.addCookies([
{
name: 'sessionId',
value: 'abc123',
domain: '.example.com',
path: '/'
}
]);
// 设置本地存储
await this.page.evaluate(() => {
localStorage.setItem('theme', 'dark');
localStorage.setItem('language', 'zh-CN');
});
5. 错误处理与重试机制
javascript
async generatePdfWithRetry(htmlContent, options, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await this._generatePdfByChrome(htmlContent, options);
} catch (error) {
console.error(`PDF生成失败 (尝试 ${attempt}/${maxRetries}):`, error);
if (attempt === maxRetries) {
throw new Error(`PDF生成失败,已重试${maxRetries}次: ${error.message}`);
}
// 等待后重试
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
性能优化策略
1. 浏览器实例复用
javascript
class BrowserManager {
constructor() {
this.browser = null;
this.contextPool = [];
this.maxPoolSize = 5;
}
async getBrowser() {
if (!this.browser) {
this.browser = await chromium.launch(this._getLaunchOptions());
}
return this.browser;
}
async getContext() {
if (this.contextPool.length > 0) {
return this.contextPool.pop();
}
const browser = await this.getBrowser();
const context = await browser.newContext();
return context;
}
async releaseContext(context) {
if (this.contextPool.length < this.maxPoolSize) {
this.contextPool.push(context);
} else {
await context.close();
}
}
}
2. 内存管理
javascript
async cleanup() {
if (this.page) {
await this.page.close();
this.page = null;
}
if (this.context) {
await this.context.close();
this.context = null;
}
if (this.browser) {
await this.browser.close();
this.browser = null;
}
}
3. 并发控制
javascript
class TaskQueue {
constructor(maxConcurrent = 3) {
this.maxConcurrent = maxConcurrent;
this.running = 0;
this.queue = [];
}
async addTask(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.running >= this.maxConcurrent || this.queue.length === 0) {
return;
}
this.running++;
const { task, resolve, reject } = this.queue.shift();
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
this.processQueue();
}
}
}
实际应用场景
1. 企业报表生成
javascript
async generateFinancialReport(data) {
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 8px; }
.header { text-align: center; margin-bottom: 20px; }
</style>
</head>
<body>
<div class="header">
<h1>财务报表</h1>
<p>生成时间: ${new Date().toLocaleString()}</p>
</div>
<table>
<thead>
<tr>
<th>项目</th>
<th>金额</th>
<th>占比</th>
</tr>
</thead>
<tbody>
${data.map(item => `
<tr>
<td>${item.name}</td>
<td>${item.amount}</td>
<td>${item.percentage}%</td>
</tr>
`).join('')}
</tbody>
</table>
</body>
</html>
`;
const pdfBuffer = await this.generatePdf(htmlContent, {
format: 'A4',
printBackground: true,
margin: { top: '20px', bottom: '20px', left: '20px', right: '20px' }
});
return pdfBuffer;
}
2. 电商订单打印
javascript
async generateOrderInvoice(orderData) {
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<style>
.invoice { max-width: 800px; margin: 0 auto; }
.header { text-align: center; border-bottom: 2px solid #333; padding: 20px 0; }
.items { margin: 20px 0; }
.total { text-align: right; font-weight: bold; }
</style>
</head>
<body>
<div class="invoice">
<div class="header">
<h1>订单发票</h1>
<p>订单号: ${orderData.orderId}</p>
<p>日期: ${orderData.date}</p>
</div>
<div class="items">
${orderData.items.map(item => `
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
<span>${item.name}</span>
<span>¥${item.price}</span>
</div>
`).join('')}
</div>
<div class="total">
<p>总计: ¥${orderData.total}</p>
</div>
</div>
</body>
</html>
`;
return await this.generatePdf(htmlContent, {
format: 'A5',
printBackground: true
});
}
技术优势
1. 完美样式还原
- 支持所有现代CSS特性
- 完美处理复杂布局
- 保持字体和颜色一致性
2. 高性能处理
- 无头浏览器,资源占用低
- 并发处理能力
- 内存优化管理
3. 灵活配置
- 丰富的PDF生成选项
- 支持自定义纸张尺寸
- 精确的页边距控制
4. 稳定可靠
- 完善的错误处理
- 自动重试机制
- 资源自动清理
总结
通过使用Playwright无头浏览器,我们成功解决了Web打印生成PDF的各种技术难题。这种方案具有以下优势:
- 技术先进性:基于最新的浏览器引擎
- 功能完整性:支持复杂的HTML渲染和PDF生成
- 性能优异:无头模式,资源占用低
- 稳定可靠:完善的错误处理和资源管理
这种技术方案为Web应用提供了高质量的PDF生成能力,特别适合需要精确控制打印效果的场景,如企业报表、电商订单、文档系统等。
Playwright无头浏览器为Web打印提供了完美的技术解决方案!
技术方案延伸
基于上述Playwright的技术实现,市面上已经有一些成熟的解决方案。比如web-print-pdf
这个npm包,它将Playwright的PDF生成能力与WebSocket通信相结合,为开发者提供了一个完整的Web打印解决方案。
该方案的特点:
- 基于Playwright实现高质量的PDF生成
- 支持多种输入格式(HTML、URL、Base64、图片)
- 提供丰富的PDF和打印配置选项
- 支持批量处理和预览功能
- 简单易用的API接口
对于有Web打印需求的开发者来说,这种基于Playwright的技术方案值得考虑。