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()
})

关键说明

  1. 必须正确配置中文字体路径
  2. 表格布局采用自适应宽度设计(widths: ['*', ...])
  3. 使用流式接口处理大文件更高效

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 字体处理规范

中文字体处理的三要素:

  1. 显式指定字体文件路径
  2. 确保字体文件可访问性
  3. 合理控制字体子集(避免文件膨胀)

推荐字体配置:

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. 注意事项与最佳实践

  1. 安全规范

    • 禁用nodeIntegration时通过preload加载PDF.js
    • 文件路径白名单验证
    • 沙箱模式运行非必要代码
  2. 跨平台兼容

    // 自动识别字体路径
    function getSystemFontPath() {
      switch (process.platform) {
        case 'win32':
          return 'C:/Windows/Fonts'
        case 'darwin':
          return '/System/Library/Fonts'
        default: 
          return '/usr/share/fonts'
      }
    }
    
  3. 内存管理

    • 单页最大尺寸控制在100MB以内
    • 使用webWorker处理大文档
    • 及时销毁不再使用的PDFDocument对象

7. 总结与展望

在Electron中处理PDF文档需要平衡功能需求与性能表现。通过本文的实践方案,开发者可以快速构建具备PDF生成与查看能力的桌面应用。随着WebAssembly等新技术的发展,未来可能出现更高效的本地PDF处理方案。建议持续关注以下方向:

  • WebGPU加速渲染
  • WASM原生模块的应用
  • 基于IndexedDB的文档缓存优化