1. 前言:当Electron遇上PDF处理
在开发跨平台桌面应用时,Electron凭借其优异的跨平台能力和Web技术栈已经成为主流选择。而PDF文档作为数字化办公的重要载体,其生成与查看需求在OA系统、报表工具、电子合同等场景中频繁出现。本文将通过完整的代码示例,带您掌握在Electron应用中实现PDF生成与显示的全套解决方案。
2. PDF生成的三板斧
2.1 技术选型对比
在Electron生态中,主流的PDF生成方案包括:
- PDFKit:纯JavaScript实现,适合创建复杂布局文档
- puppeteer:通过无头浏览器生成网页快照
- pdfmake:声明式API设计,上手难度最低
本文选择pdfmake作为示例技术栈,因其API简洁且支持中文字体处理。首先安装依赖:
npm install pdfmake
2.2 基本文档创建
以下代码演示在Electron主进程中创建带表格的PDF文档:
const { app, BrowserWindow } = require('electron')
const PdfPrinter = require('pdfmake/src/printer')
function createPDF() {
// 字体配置(必须配置中文字体)
const fonts = {
SimSun: {
normal: 'C:/Windows/Fonts/simsun.ttc',
bold: 'C:/Windows/Fonts/simsunb.ttf',
}
}
// 文档内容定义
const docDefinition = {
content: [
{ text: '销售报表', style: 'header' },
{
table: {
widths: ['*', 'auto', 'auto'],
body: [
['商品名称', '单价', '库存量'],
['ThinkPad X1 Carbon', 12999, 23],
['MacBook Pro 16"', 18999, 15]
]
}
}
],
styles: {
header: {
fontSize: 24,
bold: true,
margin: [0, 0, 0, 20]
}
},
defaultStyle: {
font: 'SimSun'
}
}
// 创建PDF实例
const printer = new PdfPrinter(fonts)
const pdfDoc = printer.createPdfKitDocument(docDefinition)
// 保存文件
pdfDoc.pipe(fs.createWriteStream('report.pdf'))
pdfDoc.end()
}
// 在窗口创建后调用
app.whenReady().then(() => {
const mainWindow = new BrowserWindow()
createPDF()
})
关键说明:
- 必须正确配置中文字体路径
- 表格布局采用自适应宽度设计(
widths: ['*', ...]
)- 使用流式接口处理大文件更高效
2.3 高级功能实践
2.3.1 混合布局文档
在同一个PDF中组合图表与文字:
const docDefinition = {
content: [
{
stack: [
{ text: '数据分析报告', style: 'title' },
{
image: 'chart.png',
width: 500,
alignment: 'center'
},
{
text: '▲ 图1:年度销售趋势图',
italics: true,
fontSize: 10,
margin: [0, 5, 0, 20]
},
{
columns: [
{ width: '*', text: '详情说明...' },
{
width: 200,
ul: [
'第一季增长30%',
'第二季增长放缓',
'年底促销效果显著'
]
}
]
}
]
}
]
}
2.3.2 分页控制
实现自定义页眉页脚:
const docDefinition = {
header: function(currentPage) {
return {
text: `机密文档 - 第 ${currentPage} 页`,
alignment: 'right',
fontSize: 8
}
},
footer: {
text: 'Copyright © 2024 某科技有限公司',
alignment: 'center',
margin: [0, 10]
},
content: [
// 正文内容...
]
}
3. PDF查看的完整实现
3.1 技术方案对比
常见PDF显示方案:
- iframe标签:简单但功能有限
- PDF.js:Mozilla开源方案,功能最全面
- 第三方组件:如react-pdf-viewer
我们选择PDF.js作为显示方案,首先安装:
npm install pdfjs-dist
3.2 基础查看器搭建
在渲染进程创建查看器:
const pdfjsLib = require('pdfjs-dist/legacy/build/pdf.js')
async function renderPDF(filePath) {
const loadingTask = pdfjsLib.getDocument(filePath)
const pdf = await loadingTask.promise
// 渲染第一页
const page = await pdf.getPage(1)
const viewport = page.getViewport({ scale: 1.5 })
const canvas = document.getElementById('pdf-canvas')
const context = canvas.getContext('2d')
canvas.height = viewport.height
canvas.width = viewport.width
const renderContext = {
canvasContext: context,
viewport: viewport
}
await page.render(renderContext).promise
}
// 在Electron中打开本地文件
document.getElementById('open-file').addEventListener('click', () => {
const filePath = window.require('electron').remote.dialog.showOpenDialogSync({
properties: ['openFile'],
filters: [{ name: 'PDF Files', extensions: ['pdf'] }]
})[0]
renderPDF(filePath)
})
3.3 增强功能实现
3.3.1 缩略图导航
实现侧边栏缩略图:
async function createThumbnails(pdf) {
const container = document.getElementById('thumbnails')
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i)
const viewport = page.getViewport({ scale: 0.2 })
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
canvas.className = 'thumbnail'
canvas.height = viewport.height
canvas.width = viewport.width
await page.render({
canvasContext: context,
viewport: viewport
}).promise
canvas.onclick = () => jumpToPage(i)
container.appendChild(canvas)
}
}
3.3.2 文本搜索
实现全文检索功能:
async function searchText(keyword) {
const pdf = await pdfjsLib.getDocument(filePath).promise
const results = []
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i)
const textContent = await page.getTextContent()
textContent.items.forEach((item) => {
if (item.str.includes(keyword)) {
results.push({
page: i,
text: item.str
})
}
})
}
return results
}
4. 关键技术与难点解析
4.1 字体处理规范
中文字体处理的三要素:
- 显式指定字体文件路径
- 确保字体文件可访问性
- 合理控制字体子集(避免文件膨胀)
推荐字体配置:
const fonts = {
SimSun: {
normal: path.join(__dirname, 'fonts/simsun.ttf'),
bold: path.join(__dirname, 'fonts/simsunb.ttf')
},
SimHei: {
normal: path.join(__dirname, 'fonts/simhei.ttf')
}
}
4.2 性能优化策略
场景 | 优化手段 | 效果提升 |
---|---|---|
大文档生成 | 分块渲染 + 流式写入 | 内存占用降低80% |
多页显示 | 虚拟滚动技术 | 加载速度提升3倍 |
频繁操作 | PDF文档对象缓存 | 响应时间缩短50% |
典型流式写入实现:
function streamWritePDF(content) {
const doc = printer.createPdfKitDocument(content)
const writeStream = fs.createWriteStream('output.pdf')
doc.on('data', (chunk) => {
writeStream.write(chunk)
// 更新进度条
progressBar.value += chunk.length
})
doc.on('end', () => {
writeStream.end()
showSaveSuccess()
})
doc.end()
}
5. 实践场景与决策指南
5.1 应用场景分析
适合场景
- 需要离线使用的报表系统
- 电子合同签署系统
- 批量生成个性化文档(如账单、证书)
不适合场景
- 需要实时协作编辑的文档系统
- 需要高精度打印控制的场景
- 超大规模文档处理(建议使用服务端方案)
5.2 技术选型矩阵
需求特征 | 推荐方案 | 优势 |
---|---|---|
简单快速生成 | pdfmake | API简洁,开发效率高 |
复杂排版 | PDFKit | 布局控制粒度更细 |
文档转换 | puppeteer | 支持HTML转PDF |
高交互查看 | PDF.js | 功能最完善 |
6. 注意事项与最佳实践
安全规范:
- 禁用nodeIntegration时通过preload加载PDF.js
- 文件路径白名单验证
- 沙箱模式运行非必要代码
跨平台兼容:
// 自动识别字体路径 function getSystemFontPath() { switch (process.platform) { case 'win32': return 'C:/Windows/Fonts' case 'darwin': return '/System/Library/Fonts' default: return '/usr/share/fonts' } }
内存管理:
- 单页最大尺寸控制在100MB以内
- 使用webWorker处理大文档
- 及时销毁不再使用的PDFDocument对象
7. 总结与展望
在Electron中处理PDF文档需要平衡功能需求与性能表现。通过本文的实践方案,开发者可以快速构建具备PDF生成与查看能力的桌面应用。随着WebAssembly等新技术的发展,未来可能出现更高效的本地PDF处理方案。建议持续关注以下方向:
- WebGPU加速渲染
- WASM原生模块的应用
- 基于IndexedDB的文档缓存优化