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的文档缓存优化
评论