欢迎光临
我们一直在努力

web打印插件四选一:从浏览器打印到可视化模板设计器

后台系统做到一定阶段,打印一定会变成最脏的活。

业务最初说"能打印就行",上线三个月后需求变成:表头要在每页重复、这张单要套打、批量打 200 张、不要弹框、能不能让业务自己改模板。等你把这些都补上,代码里已经堆了三千行排版逻辑,还没人敢动。

问题往往不在实现,而在第一步:方案选错了。

项目地址:https://gitee.com/theGreatOldFive/vue-print-designer

一、四条路线的本质区别

先说结论:这四条路线的差异,不在"能不能打出纸",而在排版逻辑放在哪、模板归谁维护。

配图1:四条打印路线的取舍四条路线的取舍

路线排版在哪分页谁控制模板谁维护典型接入成本
window.print 浏览器排版引擎 CSS 前端 0.5-2 人天
html2canvas + jsPDF 前端 canvas 手动切片 前端 2-5 人天
Puppeteer 服务端 Chromium 引擎控制 前端 + 后端 5-10 人天
模板设计器 渲染引擎 模板内置 业务自己 3-8 人天

成本不在开发,在变更。前三条路线,业务每改一次版式,都要走一遍"需求 → 开发 → 测试 → 发版";第四条只改配置。一年改 10 次模板,前三条累计要吃掉 20-40 人天。

二、window.print:能走多远

最短路径,也是最适合内部单据的一条。

@media print {
@page {
size: A4;
margin: 12mm 10mm;
}

/* 表头每页重复 */
thead { display: table-header-group; }

/* 行不要被劈开 */
tr { break-inside: avoid; page-break-inside: avoid; }

/* 交互元素不打印 */
.no-print, .toolbar, .pagination { display: none !important; }
}

它的边界很清楚:

  • 必须弹打印对话框,用户要自己选打印机、点确认;
  • 边距受浏览器影响,不同浏览器默认页边距不一致;
  • 套打基本不可行,毫米级定位会被浏览器默认边距吃掉;
  • 静默打印做不到,这是浏览器安全模型决定的。

所以它适合:内部审批单、对账单、报表——用户点一下、选打印机、确认,这个流程可以接受。

如果业务只是"把页面打出来",这条路能覆盖 60% 的需求,别过度设计。

三、截图方案:一页以内才划算

用户要的不是"打印",而是"下载一个 PDF 文件"时,html2canvas + jsPDF 最快。

import html2canvas from 'html2canvas'
import { jsPDF } from 'jspdf'

async function exportPdf(el, filename) {
const canvas = await html2canvas(el, {
scale: 2, // 关键:至少 2 倍,否则模糊
useCORS: true, // 跨域图片
backgroundColor: '#ffffff',
logging: false,
})

const pdf = new jsPDF('p', 'mm', 'a4')
const pageW = 210
const imgH = (canvas.height * pageW) / canvas.width

pdf.addImage(canvas.toDataURL('image/jpeg', 0.95), 'JPEG', 0, 0, pageW, imgH)
pdf.save(filename)
}

三个硬伤必须提前知道:

  • 文字变成图片,不可搜索、不可复制、放大就糊;
  • 长内容撞 canvas 上限,Chrome 单边最大 32767px,几十页的报告直接崩;
  • 切页会把字劈成两半,因为它是按图片高度硬切,不认行边界。
  • 结论:一页以内能用,超过两页就该换路。它最适合电子回单、短凭证这类"打完就存档、不会二次编辑"的场景。

    四、服务端渲染:最准,也最重

    Puppeteer 出 PDF 的好处,是分页、页码、页眉页脚全部交给 Chromium 的排版引擎:

    const browser = await puppeteer.launch({ args: ['–no-sandbox'] })
    const page = await browser.newPage()

    await page.setContent(html, { waitUntil: 'networkidle0' })
    await page.evaluate(() => document.fonts.ready)

    await page.pdf({
    path: 'out.pdf',
    format: 'A4',
    printBackground: true,
    displayHeaderFooter: true,
    headerTemplate: '<div style="font-size:9px;width:100%;text-align:center">合同编号 {{no}}</div>',
    footerTemplate:
    '<div style="font-size:9px;width:100%;text-align:center">' +
    '第 <span class="pageNumber"></span> / <span class="totalPages"></span> 页</div>',
    })

    await browser.close()

    代价也很实在:

    • 服务器要装 Chromium,镜像 400MB 起步;
    • 并发高时内存吃紧,一台 2C4G 的机器同时跑 5 个渲染任务就开始排队;
    • 前端还要额外维护一套"给后端用的 HTML",和页面样式容易漂移;
    • 调试链路长:改一个像素,要重新部署才能看到。

    适合:医院报告、检验单、对账单这类必须归档、分页严格的场景。

    五、模板设计器:把排版从代码里拿出来

    前三条路线的共同点是:模板写死在代码里。

    业务想加一列规格、换一下公司抬头、把表头字体改大,都要提需求、排期、发版、回归测试。一个中型 ERP,这类需求一年能有几十条。

    模板设计器的思路是把"排版"抽出来:业务在页面上拖字段、设字号、画表格线,导出 JSON;渲染时用同一套引擎填数据、算分页、出纸。

    {
    "schemaVersion": 3,
    "paper": { "width": 210, "height": 297, "unit": "mm" },
    "bands": [
    { "type": "pageHeader", "height": 25, "repeat": true },
    { "type": "detail", "height": 8, "repeat": true, "source": "items" },
    { "type": "summary", "height": 40, "repeat": false }
    ],
    "elements": [
    { "type": "text", "x": 10, "y": 8, "w": 80, "h": 8, "binding": "title" },
    { "type": "barcode", "x": 8, "y": 220, "w": 84, "h": 20, "binding": "orderNo" }
    ]
    }

    关键要求只有一条:设计态和打印态共用一套渲染。如果设计器用一套 DOM、打印用另一套,设计稿和成品永远差两毫米,用户会觉得"这工具不靠谱"。

    配图2:模板市场里现成的单据模板模板市场

    这套结构我参考的是 vue-print-designer 的实现:模板 JSON + 统一渲染器 + 带(band)分页,三块都开源,优点是可以照着它的字段设计自己复刻一个渲染器,不用担心被锁死。

    六、六个维度横向对比

    维度window.print截图方案Puppeteer模板设计器
    输出质量 矢量 位图 矢量 矢量
    分页可控性 中(靠 CSS) 差 高 高
    静默打印 不支持 不支持 需配合客户端 需配合客户端
    套打精度 低 低 中 高(mm 定位)
    模板维护 前端 前端 前端+后端 业务自助
    运维成本 无 低 高(Chromium) 低

    七、选型决策流程

    按顺序问四个问题,问完基本就定了:

  • 模板一年改几次? 0-2 次 → 走前三条;5 次以上 → 直接看设计器。
  • 内容会不会超过一页? 会 → 排除截图方案。
  • 要不要归档 PDF? 要 → 服务端渲染或设计器渲染。
  • 有没有套打/标签? 有 → 必须能控制毫米级坐标,前两条直接排除。
  • 四个问题里只要有一个落在"设计器"那侧,就说明你的系统已经进入"打印是长期成本"的阶段了。

    八、从 window.print 迁移到设计器的路径

    很多团队不是从零开始,而是已经有一堆 @media print 的样式。迁移可以分三步,不用推倒重来:

    第一步:把版式数据化。 把现在写死的表格列、字段,抽成一份配置(先不用做成可视化设计器)。

    第二步:统一渲染入口。 所有打印走同一个 <PrintRenderer :template :data>,先只支持"配置渲染",不支持拖拽编辑。

    第三步:接设计器。 业务方开始自己改模板,前端只维护渲染器。

    这三步每步都能独立上线,风险可控。跳过前两步直接上设计器,通常会在"老模板怎么兼容"上卡住。

    九、几个容易忽略的细节

  • 纸张尺寸必须在打印机驱动里建好,否则会回退到 A4 并缩放,套打全废;
  • 打印缩放必须 100%,浏览器"适应纸张"会破坏毫米级布局;
  • 字体要随应用分发,别赌用户机器装了什么;
  • @page { margin: 0 } 之后要自己用 padding 撑内容区,否则内容贴着纸边;
  • 条码一律 SVG,位图在 203dpi 下会糊。
  • 配图3:热敏小票——最常见的打印形态之一热敏小票

    十、一张决策表收尾

    你的处境选它理由
    内部审批单,模板不改 window.print 半天干完,别过度设计
    要下载 PDF,一页内 html2canvas + jsPDF 前端自足,不用服务端
    必须归档,分页严格 Puppeteer 分页页码最稳
    模板由业务维护 模板设计器 改版式不发版
    既要套打又要多联 设计器 + mm 定位 手写坐标维护不动

    选型的关键不是技术先进,而是你愿意为"模板变更"付多少成本。前期省下的接入时间,后期会以排期的形式还回去。

    赞(0)
    未经允许不得转载:171主机测评 » web打印插件四选一:从浏览器打印到可视化模板设计器
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址