
给项目加打印功能听起来不就是调一下window.print()吗但凡是接过自定义打印模板需求的人都不会这么想。业务方要的不是把网页原样打出来而是按照他们的格式来——订单要一张三联单质检要一张带条码的报表财务要求A4纵向、页边距固定、每个字段的位置分毫不差。如果你试过用 CSSmedia print去精调这些大概率体会过调了三天、换个浏览器就全部错位的崩溃。我这次要聊的vue-plugin-hiprint就是专门解决这个问题的开源方案。它本质上是把 jQuery 生态里成熟的 hiprint 打印能力封装成了 Vue 组件内置了一个可视化拖拽设计器可以在网页里直接设计打印模板、拖拽文本和条码元素、调整位置大小、绑定动态数据然后一键打印或导出 PDF。适合正在用 Vue2/Vue3 做后台管理系统、并且被打印模板折磨过的同学。这篇文章我会从设计器、代码接入、踩坑记录到模板持久化把整个链路讲透。1. 为什么是 vue-plugin-hiprint打印功能的核心痛点与解法1.1 传统打印方案无法回避的三个问题大部分后台项目最初接打印功能时走的都是浏览器原生打印。页面里写一个隐藏的打印区域用 CSS 控制media print的显示隐藏再调window.print()。这套方案对于简单的文本输出够用一旦进入真实业务场景问题就会接踵而至。第一个问题是样式不稳定。原生打印基于浏览器渲染引擎不同版本的 Chrome、Edge 对page规则、margin合并、break-inside分页避让的支持程度都不一样。你在开发环境调好的样式换一台电脑可能就多打印出半页空白。更别提同一个模板要给多条数据复用字段长短一变整个布局就被撑破。第二个问题是模板改起来太慢。后端管理系统里打印模板大概率不是固定的。今天订单要加一个收货人电话明天报表要调一下列宽后天财务要求把合计挪到右上角。每一次调整都意味着前端改代码、发版、测试来回折腾大半天。业务方还经常在你面前站着说要现场看效果——原生 CSS 方案在这种场景下是提不动刀的。第三个问题是分页控制困难。多行明细数据要自动分页每页都要保留表头最后一页还要保证合计字段出现在页尾。CSS 的break-inside: avoid在复杂表格面前经常失效最后只能靠人工估算行高去硬拆数据丑且易错。1.2 vue-plugin-hiprint 的设计思路模板数据与渲染逻辑分离vue-plugin-hiprint解决上述问题的思路很清晰它把打印模板长什么样和打印模板怎么渲染彻底拆开。模板是一份 JSON 描述里面记录了纸张大小、页边距、元素的坐标与宽度高度、字体样式、数据绑定字段。渲染引擎拿到这份 JSON在浏览器里用 Canvas 或 DOM 方式绘制出所见即所得的效果再调用原生打印窗口输出。这个设计带来的直接收益是模板可以动态下发。你可以把模板 JSON 存到后端数据库里业务方想要调整版式时直接在网页设计器里拖一拖保存后所有用户立即生效完全不需要前端发版。这一点对于报表需求变动频繁的企业系统来说意义很大。其次因为模板结构本质上是元素数组 布局参数它可以很方便地跟业务数据结合。渲染时传入一个 data 对象模板里的字段占位符会自动替换成真实值再通过getHtml()方法生成打印用的 HTML 字符串然后交给打印窗口。整个过程你不需要关心分页和样式引擎会按照预设参数自动处理。还有一个隐藏优势是生态兼容。hiprint 在 jQuery 时代就有了一批成熟的打印元素文本、条形码、二维码、图片、表格、横线、矩形等vue-plugin-hiprint把它们原样带进了 Vue 世界。如果你之前用过 hiprint切换到 Vue 版本基本零成本如果你是新上手也不需要去学习 jQuery 的写法组件封装已经把这些细节消化掉了。2. 安装与接入版本兼容性和第一个坑2.1 环境准备与依赖安装先说明一下vue-plugin-hiprint目前同时支持 Vue2 和 Vue3但接入方式和样式路径有一些差异这里以 Vue3 Vite 为例讲一遍完整流程Vue2 的差异我会单独标注。第一步是安装核心包npm install vue-plugin-hiprint这个包会把 hiprint 核心、设计器面板、打印引擎一起带进来不需要再单独安装 jQuery组件内部已经处理了依赖关系。不过有一点要留意如果你在项目里同时使用了其他依赖 jQuery 的插件可能会出现 jQuery 版本冲突后面踩坑部分我会细说。第二步是在入口文件里引入样式和组件// main.jsVue3 import { createApp } from vue import App from ./App.vue import vue-plugin-hiprint/dist/print-lock.css import vue-plugin-hiprint/dist/vue-plugin-hiprint.css import { hiprint } from vue-plugin-hiprint const app createApp(App) app.use(hiprint) app.mount(#app)注意样式文件的顺序不要搞反了print-lock.css负责打印窗口的样式锁定vue-plugin-hiprint.css负责设计器面板的样式。先引入锁定的样式再引入设计器样式能避免一些奇怪的样式覆盖问题。Vue2 的用法类似只是app.use变成Vue.use。2.2 可视化组件启用与初始配置样式和依赖引入完成之后还需要对 hiprint 做初始配置。这一步很多人会漏掉导致后面点击打印没有任何反应。核心配置项如下import { hiprint } from vue-plugin-hiprint hiprint.Config.Config { host: http://localhost:8081, // 后端服务地址用于获取/保存模板 getTpl: getTpl, // 获取模板的接口路径 print: print, // 打印接口路径 autoRender: true, // 数据变化后自动重新渲染 }这里的host、getTpl是给从远端加载模板功能用的。如果你暂时不想接后端可以把host留空直接在代码里传入模板 JSON 对象。我的建议是初期先不接后端把所有精力放在理解模板结构上等设计器跑通了再考虑模板持久化。另外组件默认会在全局注册两个标签vue-plugin-hiprint和hiprint-setting-panel。其中设置面板是设计器右侧的属性编辑区域如果你自定义过 UI也可以不注册它手动渲染。2.3 版本选择的一个重要教训我在接入过程中踩过的最大的坑是版本问题。vue-plugin-hiprint在 npm 上的版本迭代比较快早期版本和当前版本的 API 有些差异。最典型的是hiprint.PrintTemplate构造参数的名称变化旧版本用settingContainer新版本改成了settingsContainer传错的话设计器右侧的属性面板就是不显示控制台也不报错排查起来相当费时间。所以建议尽量锁定一个大版本内的最新版不要随手装 latest。如果项目里已经装了旧版本要升级的话先去 GitHub 的 changelog 看一眼 API 变更别直接升。我之前就是没看变更直接升结果几十个打印模板全部起不来回滚又花了一个小时。提示安装后可以在控制台打印一下hiprint对象看里面有没有PrintTemplate、PrintElementTypeManager这些关键属性。如果这些属性是 undefined说明版本不兼容或者引入方式不对。3. 模板设计与打印流程从设计器到纸张的全链路3.1 可视化设计器的三个核心区域打印机接入之后下一步是理解可视化设计器。设计器界面分为三个区域左侧是元素面板中间是画布区右侧是属性编辑区。左侧元素面板里能看到所有可拖拽的元素类型。常用的有普通文本、多行文本、条形码、二维码、图片、表格、横线、矩形。拖拽的方式很简单鼠标按住元素拖到画布上就可以。每个元素放到画布上之后都会有一个默认的坐标和宽高这些数值会在右侧属性面板里显示可以手动精调。中间画布区模拟的是真实的纸张。设计器会根据你设置的纸张类型A4、A5、自定义等自动计算画布尺寸并且会显示页边距的虚线区域。元素不能拖到虚线外面这个限制跟实际打印机的可打印区域是对应的避免了设计的时候把元素放到纸外打印出来却缺了一截。右侧属性编辑区是这个插件的精髓。选中画布上的任意元素右侧就会列出这个元素的所有属性文本内容、字体大小、字体粗细、对齐方式、旋转角度、边框样式、透明度以及最关键的数据绑定字段。举个例子你放了一个文本元素希望它在打印时显示订单编号那么你可以在文本内容里写[orderNo]然后在绑定字段设置里指定orderNo。渲染时引擎会把真实数据里的orderNo字段填进去。3.2 从模板 JSON 到打印输出的完整流程设计器里拖出来的模板本质上是一份 JSON。下面的代码模拟了一份最简模板的结构{ paperType: A4, paperWidth: 210, paperHeight: 297, orientation: portrait, elements: [ { type: text, left: 14.1, top: 14.1, width: 100, height: 10, options: { textContent: 订单编号[orderNo], fontSize: 12, fontWeight: normal } }, { type: table, left: 14.1, top: 30, width: 181.8, height: 80, options: { columns: [ { title: 商品名称, field: name, width: 50 }, { title: 数量, field: qty, width: 30 }, { title: 单价, field: price, width: 40 } ] } } ] }拿到这份 JSON 之后在代码里创建打印模板并输出到页面上import { hiprint } from vue-plugin-hiprint const tplJson { ... } // 上面那份 JSON // 创建打印模板实例 const template new hiprint.PrintTemplate({ template: tplJson, // 挂载设计器画布的容器 // 如果不传 designContainer模板不会渲染画布 }) // 方法一渲染设计器 template.design(#hiprint-printTemplate) // 方法二直接获取渲染后的 HTML const html template.getHtml({ orderNo: PO202405001, items: [ { name: 螺丝, qty: 100, price: 0.5 }, { name: 螺母, qty: 200, price: 0.3 }, ] }) // 方法三弹出打印窗口 template.print({ orderNo: PO202405001, items: [ { name: 螺丝, qty: 100, price: 0.5 }, { name: 螺母, qty: 200, price: 0.3 }, ] })这里需要解释一下getHtml和print的区别。getHtml只负责把模板和数据渲染成一段 HTML 字符串不会弹出打印窗口适合用来做预览。print会把 HTML 塞进一个隐藏的 iframe自动调用 iframe 里的打印方法弹出来的就是浏览器原生的打印窗口用户可以选择打印机、纸张、缩放比例等。3.3 数据绑定与表格自动分页的原理数据绑定是模板设计里最影响使用体验的部分。文本元素用[字段名]占位符渲染时会被替换成传入数据对象里对应字段的值表格元素则需要在options.columns里定义列标题和字段名引擎会根据传入的数组数据自动生成行。表格的自动分页逻辑值得一提。当表格数据行数超过一页剩余空间时引擎会在分页处自动将剩余行移到下一页并且默认情况下每一页都会重复表头。这个行为由模板配置里的repeatHeader控制默认是true。如果你不希望每一页都出现表头比如某些明细报表只需要第一页有表头可以在设计器的纸张设置里关掉这个选项。我在实际使用中遇到过一个细节表格分页后如果最后一页剩余空间不足以放下合计行合计会被挤到下一页的开头。解决方法是把合计行也做成绑定字段的单元格并在模板上给它预留固定位置而不是依赖表格自身的合计功能。这个处理方式虽然麻烦一点但打印结果最可控。4. 踩坑实录样式丢失、分页错乱和 window 对象的诱惑4.1 打印样式丢失的根因排查与修复很多人第一次用vue-plugin-hiprint打印时会遇到一个奇怪的现象设计器里模板展示得非常完美点打印之后浏览器打印预览里样式全乱了字体变小、背景色不见了、间距错位。这个问题的根因在 iframe。hiprint.print会把模板 HTML 放进一个新的 iframe然后从这个 iframe 里发起打印。iframe 中的文档是独立于主应用页面的它不会继承主应用里的全局样式。如果模板 HTML 里的元素依赖了某些全局类名比如你的项目里给所有文本框统一设置过border-box打印出来的效果就会跟你预期的不一样。排查思路很简单把template.getHtml()打印出来直接放到一个空白 HTML 文件里用浏览器打开看看样式还在不在。如果不在说明模板依赖了外部全局样式需要把这些样式内联进模板或者在使用getHtml之前手动提取公共样式。更稳妥的做法是在使用前显式设置模板的样式快照。hiprint 提供了getStyle相关能力可以在模板里保存每个元素的精确样式不让它受外部环境干扰。建议一开始就把所有打印元素用模板内置的样式属性精确定义不要图省事依赖全局 CSS。4.2 分页错乱从纸张类型到页边距的连锁反应分页错乱是另一个高频踩坑点。最典型的表现是设计器里显示一页打印出来却多出来一页或者第二页上只有一行数据孤零零地躺在那里。我排查了几次之后发现根因通常是纸张尺寸和实际打印机驱动不匹配。vue-plugin-hiprint的设计器默认支持多种纸张类型A4、A5、B5、Letter、自定义。如果你在设计器里选了 A4但打印窗口的纸张设置的是其他尺寸打印结果就会被浏览器自动缩放。缩放后原计划的元素位置就会偏移可能溢出到第二页。解决方法是两个对齐一是设计器里的纸张类型必须与打印机驱动默认纸张一致二是在写模板 JSON 时明确指定纸张宽高和页边距不要只给一个paperType。举例来说{ paperType: A4, paperWidth: 210, paperHeight: 297, orientation: portrait, pageMargin: 5, pageMarginTop: 10, pageMarginBottom: 10, pageMarginLeft: 5, pageMarginRight: 5 }这几个字段会直接影响分页计算。注意页边距的单位是毫米设计器里的元素坐标也是毫米制不要换算成像素。很多人在这里栽跟头拿像素去设置边距结果实际打印出来边距偏差非常大。另外如果你的模板里使用了多列布局比如一个订单要打印两栏信息分页错乱的概率会更高。我的建议是在设计器里预留足够的上下边距并且把两栏元素都放在同一页面高度的范围内不要靠刚好放得下的心态去设计稍微溢出一点点就会触发分页。4.3 window 对象与单例诱惑为什么你的页面打印一次就卡死这个坑比较隐蔽而且只在部分版本里出现。之前有段时间我在项目里加入了打印功能后用户反馈打印一次之后整个页面就变得很卡刷新才能恢复。排查了半天最后定位到的问题出在 iframe 的清理逻辑上。hiprint.print内部会创建一个隐藏 iframe 来承载打印内容打印结束后正常情况会销毁这个 iframe。但如果你在打印前手动调用了window.print()或者在模板预览过程中对全局window对象做了持久化引用某些版本下 iframe 的清理时机就会被打乱导致多个打印 iframe 同时存活内存占用持续飙升页面自然越来越卡。这里我的建议是一旦决定使用vue-plugin-hiprint就不要在应用里混用原生window.print()。打印入口统一走template.print()确保 iframe 的创建和销毁都由插件统一管理。如果你需要在打印前做一些校验或确认操作在调用print之前完成就好不要打断插件内部的渲染流程。此外如果你在代码里保存了template实例作为全局变量记得在页面卸载或不需要继续使用时手动执行实例的销毁方法。有些版本没有自动释放设计器画布监听器时间长了事件监听会积累同样会导致页面卡顿。注意如果你用了多个template实例同时设计多份模板每个实例都要绑定到你自己的容器 DOM 上不要复用一个容器。复用的后果是事件监听重复绑定画布错乱、元素拖不动、属性面板失控这些问题往往很难一眼看出来。5. 进阶玩法模板持久化、动态数据与多实例管理5.1 模板 JSON 的持久化方案选择设计器的价值在于设计出来的模板能被复用所以模板 JSON 的持久化是关键一环。网上常见的有两种方案前端 localStorage 简单存储、后端数据库存储。这里我重点说后一种因为它才是报表系统真正需要的。后端存储的思路很直接设计器完成模板设计后调用template.getTpl()拿到 JSON 字符串通过接口保存到数据库。需要打印时根据业务类型比如订单、报表、质检单从后端拉取对应的模板 JSON再构造PrintTemplate实例。这里有一个设计细节值得注意模板 JSON 里包含的是纸张参数和元素参数不包含业务数据。动态渲染时数据是后传的。所以后端接口设计时getTpl接口返回的只是模板本身不要试图让接口把业务数据也一起返回那样会把模板变复杂而且业务数据可能涉及到权限校验混在一起会让打印接口的权限边界变得模糊。如果你不想自己维护模板存储的后端接口hiprint.Config.Config.host和getTpl的配置组合可以直接满足从服务端拉模板的需求。它内部会请求${host}/${getTpl}拿到返回的 JSON 后自动填充到模板里。不过这样做的前提是后端要按它约定的返回格式来给数据否则拿不到模板会直接报错。5.2 动态数据填充的三种姿势动态数据填充是打印功能最核心的使用场景总结起来常见的有三种姿势各有适用场景。第一种是直接传对象。适合简单单据比如订单、发票数据量不大一层结构就够了。上面的示例代码已经是这种用法不再赘述。第二种是嵌套对象和数组。适合带明细行的报表、送货单主表字段和明细数组混合在一起。模板里文本元素用[orderId]绑主表字段表格元素用columns里的field绑定明细数组的元素属性。这里要特别记住表格列里写字段名时不要加前缀。我见过有的人在列字段里写item.name结果渲染不出来因为引擎拿到当前行的数据对象后只按对象属性名取值不会做路径解析。第三种是函数计算字段。模板里有些字段不是直接来自接口而是需要前端算出来的。比如总金额 单价 × 数量。这种字段我建议在传数据之前先把数据处理好不要指望模板里写表达式。hiprint 的文本绑定是纯粹的字段名替换不是模板引擎所以你需要在业务代码里维护好数据计算逻辑把算好的字段塞进数据对象。5.3 二维码、图片与条码的动态处理除了文本和表格我在实际业务中使用频率很高的还有二维码、条码和图片元素。这三种元素在动态数据填充时有各自的特殊逻辑这里单独讲一下。二维码和条形码的绑定比较简单。元素面板里拖入二维码后在属性面板的数据绑定字段里填上数据字段名。渲染时引擎会自动把该字段的值编码成二维码图案。这里要注意编码格式问题如果你的数据字段包含中文字符二维码的容错级别和编码模式可能影响扫描识别率建议数据侧先做 URL 编码或转成 UTF-8 字符串。图片元素则需要区分两种方式。一种是把图片链接放在数据字段里渲染时引擎自动拉取远程图片另一种是直接把图片 Base64 编码塞进字段。第一种方式很简单但受限于图片服务器域名是否允许跨域以及是否有防盗链。第二种方式兼容性最好打印时完全不依赖网络缺点是模板 JSON 会膨胀——如果你把一张 100KB 的图片塞进去模板字符串会多出一百多K对存储和传输都不友好。我的建议是标准场景用第一种远程图片链接把图片放在稳定的 CDN 或对象存储上对于需要离线打印的生产环境才考虑 Base64。还有一点用远程图片的时候渲染时机要控制好。引擎在图片没加载完时就调print可能出现打印出来的图片区域是空白。如果你发现这个情况可以手动预加载图片再调用template.getHtml()或print()。5.4 多台打印机与多实例管理的实战建议最后说一下多实例管理。有些系统里有多种打印模板而且需要同时打开多个浏览器标签页各自做打印设计这时候template实例的隔离管理就特别重要。我的做法是在全局唯一的地方维护一个模板实例的 Map以业务类型作为 key。每次选择业务类型时先从 Map 里取实例如果不存在则重新创建。切换业务类型时先销毁旧实例再创建新实例。这样既能保证实例不被重复创建也能避免多个模板同时监听同一个容器。销毁实例的操作在 Vue 组件卸载时尤其重要。如果你在beforeUnmount里忘了做清理设计器画布上的元素虽然从视觉上消失了但事件监听还在一旦你重新进入这个页面就会出现元素拖拽失效、工具栏点击一次执行两次的诡异问题。另外关于打印窗口的预览确认我建议在正式打印前用getHtml()生成一个预览视图给用户看确认数据无误后再走print()。不要直接跳过预览因为打印一旦输出到纸张上错误就很难挽回。对于打印量大的场景更是要在预览接口里加上数据校验提示比如某些关键字段为空时直接拦截而不是打出一张内容不全的废纸。6. 从设计器出发的扩展思路vue-plugin-hiprint 的核心能力其实在设计器这一层已经比较完整了但如果你的项目还需要更深入的定制有两条扩展路径可以走一是自定义元素类型二是二次开发设计器的工具栏。自定义元素类型的难度不算低因为要理解引擎内部元素渲染机制但一旦掌握你可以把公司的特殊业务组件比如带有特殊边框的图章、自定义签名区域都封装成可拖拽的可视化元素业务方在设计时直接拖出来用体验会好很多。工具栏的二次开发相对更实用。默认设计器里提供了一些基础功能按钮比如撤销、重做、缩放、清空等。在实际交付项目时我常常需要额外增加保存模板到服务器载入历史版本一键替换数据源这些操作。这些功能可以通过在模板容器外自己挂接按钮然后调用template.getTpl()、template.design()等公开方法实现。这样做的好处是不侵入原组件源码升级依赖时不容易产生冲突。我个人的体会是这类可视化打印组件的引入成本不高但真正要把它用好功夫还是花在业务理解上——先搞清楚你的打印单据究竟有哪些数据来源、哪些字段需要变动、分页规则是什么再回头去设计模板结构往往事半功倍。相反一上来就打开设计器拖几个元素等打印需求多起来模板会越改越乱。最后再分享一个小技巧在开发阶段把模板 JSON 全部打印出来存档每次调整设计器时做一次版本对比。这样即使后来改出了问题也能快速找回之前稳定的模板结构比依赖 Git 记录代码更直接因为模板 JSON 往往不是放在代码仓库里的。