跳转至

将 DOM 打印为 PDF 的实现方案

通用前端技巧,不依赖任何框架。将指定 DOM 区域克隆后在新窗口中打印,支持完整 CSS 样式,可直接"另存为 PDF"。


核心思路

  1. 收集样式 — 遍历页面中所有 <link> 和 <style> 标签,构建完整 CSS 文本
  2. 克隆 DOM — 将目标区域 cloneNode(true) 完整复制,保留原始样式
  3. 注入样式 — 将收集的 CSS 注入到打印窗口的 <head>
  4. 触发打印 — 通过 window.print() 唤起浏览器打印对话框,用户选择"另存为 PDF"

适用于任何 HTML / JS 环境:Vue、React、原生页面均可。


核心函数

收集所有样式表

function getAllStyleSheets() {
  const doc = window.top.document
  const sheets = []
  for (const link of doc.querySelectorAll('link[rel="stylesheet"]')) {
    sheets.push(link.sheet)
  }
  for (const style of doc.querySelectorAll('style')) {
    if (style.sheet) sheets.push(style.sheet)
  }
  return sheets
}

合并为 CSS 文本

function buildStylesHtml() {
  const styleSheets = getAllStyleSheets()
  let cssText = ''
  for (const sheet of styleSheets) {
    try {
      if (!sheet || !sheet.cssRules) continue
      for (const rule of sheet.cssRules) {
        cssText += rule.cssText + '\n'
      }
    } catch (e) {
      // 跨域样式表,用 @import 兜底
      try {
        const href = sheet.href
        if (href) cssText += `@import url("${href}");\n`
      } catch (_) {}
    }
  }
  return cssText
}

打印函数

/**
 * 将指定 DOM 区域打印为 PDF
 * @param {string} selector - CSS 选择器,不传则打印整个页面
 */
function printToPdf(selector) {
  const dom = selector
    ? document.querySelector(selector)
    : document.body

  if (!dom) {
    console.error('未找到目标 DOM:', selector)
    return
  }

  const clone = dom.cloneNode(true)
  const stylesHtml = buildStylesHtml()

  const printContent = `
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>打印预览</title>
  <style>
    @page { margin: 10mm; }
    * { box-sizing: border-box; }
    body {
      margin: 0;
      padding: 20px;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    }
    @media print {
      body { padding: 0; }
    }
  </style>
  <style id="__injected-styles__">
${stylesHtml}
  </style>
</head>
<body>
  <div id="print-target"></div>
  <script>
    document.getElementById('print-target').innerHTML = ${JSON.stringify(clone.innerHTML)};
    window.addEventListener('load', function () {
      window.print()
      window.addEventListener('afterprint', function () { window.close() })
      setTimeout(function () { window.close() }, 3000)
    })
  <\/script>
</body>
</html>
  `

  const printWindow = window.open('', '_blank', 'width=900,height=700,scrollbars=yes')
  if (!printWindow) {
    alert('弹窗被拦截,请允许本页面弹出窗口后重试')
    return
  }
  printWindow.document.open()
  printWindow.document.write(printContent)
  printWindow.document.close()
}

使用方式

// 打印整个页面
printToPdf()

// 打印指定选择器区域
printToPdf('#my-report')
printToPdf('.invoice-container')
printToPdf('div.invoice-wrapper')

流程图

printToPdf(selector?)
          │
          ▼
   document.querySelector(selector) 查找目标 DOM
          │
          ▼ (未找到 → console.error)
          │
   cloneNode(true) 克隆目标 DOM
          │
   收集所有 CSS 样式表
   buildStylesHtml()
          │
   构造打印窗口 HTML
          │
   window.open 新窗口
          │
   写入 HTML → document.write()
          │
   触发 window.print()
          │
   用户在打印对话框选择"另存为 PDF"
          │
   afterprint 或 3s 兜底 → 窗口关闭

注意事项

  1. 弹窗拦截 — window.open 可能被浏览器拦截,需要用户允许弹出窗口
  2. 跨域样式 — CDN 等第三方样式表无法读取 cssRules,会使用 @import url() 兜底(依赖目标资源允许跨域)
  3. 字体一致性 — 打印窗口字体依赖用户系统,建议使用 Web 安全字体或嵌入字体以确保一致性
  4. 打印边距 — 通过 @page { margin: 10mm } 统一设置,按需调整
  5. afterprint 兼容性 — 部分浏览器不支持 afterprint,使用 3 秒定时器兜底关闭

适用场景

  • 报表、数据看板导出 PDF
  • 表单、合同、发票等打印需求
  • 任意 Web 页面生成可存档的 PDF 文件

通用前端打印方案,Obsidian 笔记存档。