简介electron-vue-print-demo 是一款基于 Electron 与 Vue CLI 3 的桌面端小票打印示例项目面向需要快速实现静默打印、打印机配置与本地状态保存的 JavaScript 开发者。项目完整演示了从用户点击打印、读取 electron-store 本地存储、判断是否已设置打印机名称到未设置时弹出配置框、确认后自动打印的整个业务闭环代码结构清晰适合直接借鉴或二次开发。压缩包共 25 个文件以 14 个 JS 文件为主涵盖主进程、后台任务与业务逻辑另有 3 个 Vue 组件负责界面交互2 个 JSON 文件用于项目配置以及 HTML 模板、说明文档和图标素材整体仅 117KB轻量易读。已有 3660 人学习/下载。通过该项目可以掌握 electron-store 的用法、渲染进程与主进程通信方式以及设置打印机并静默打印小票的完整实现思路对开发收银系统、订单打印等场景有直接参考价值。1. electron-vue-print-demo 在解决什么问题把浏览器打印换成可控的小票输出做收银机、门店订单、外卖接单这类系统时小票打印是上线前最容易翻车的环节。浏览器里的 window.print() 只能弹系统打印对话框纸张宽度、静默打印、指定打印机全都不可控换成 electronvuecli3 实现打印小票功能之后Electron 主进程可以直接调 webContents.print()指定 58mm 或 80mm 小票纸、静默输出、拿到打印结果回调。标题里的 electron-vue-print-demo 就是这类需求的最小可运行样本Vue CLI 3 搭建界面Electron 负责打印通道前端只管拼装小票内容。适合正在做桌面收银软件、门店终端工具或者打算把 Web 端订单系统改造成客户端形态的开发者。2. 打印链路怎么选webContents.print 与隐藏打印窗口的取舍2.1 浏览器打印为什么不适合小票场景小票纸很窄58mm 小票纸的有效打印宽度通常只有 48mm。浏览器打印面向的是 A4 这类标准幅面就算网页里写了 media print系统打印对话框给出的纸张列表、用户默认纸张都不受网页控制。也就是说小票功能无法保证打印出来的内容宽度对上纸宽。更深一层的问题是静默。收银场景里用户点一次打印程序希望直接出票不要让用户面对“选择打印机”和“确定”两次确认。传统 Web 应用做不到这一点因为网页权限模型禁止网页直接访问打印机设备。即使浏览器有打印预览它也是系统级 UI网页拿不到打印成功或失败的状态。做生产工具时这是硬伤。2.2 webContents.print() 与 window.print() 的本质区别window.print() 是渲染进程里由浏览器实现的打印它的行为是唤起打印对话框返回值永远是 void。开发者看到的打印过程其实是用户在对话框里确认后完成的程序无法干涉打印机选择、无法静默执行、也无法拿到成功失败状态。webContents.print(options, callback) 是 Electron 主进程的 API直接操作的是 Chromium 打印栈数据从渲染管线出来后可以直接进入系统打印机队列。它支持 silent 参数为 true 时绕过打印对话框支持 deviceName 指定打印机回调里返回成功标志和失败原因。差别最大的一点window.print() 是渲染进程的单向请求webContents.print() 是主进程的双向控制。做小票打印这类需要确定性的功能只能走主进程 API。2.3 两条实现路线当前页打印与隐藏窗口打印实现时有两条路线。路线 A 是直接在 Vue 应用当前页面里写打印模板用 media print 设置样式再调用 webContents.print() 打印当前窗口。好处是改动量小本页数据直接可用问题是打印模板和操作界面混在一起按钮、侧边栏都要用 CSS 隐藏偶尔会出现打印内容带上操作界面残留或者样式互相污染。路线 B 是开一个隐藏的 BrowserWindow单独加载一个打印模板页面主进程在 did-finish-load 之后对该窗口调用 print。我一般选 B原因是小票模板可以单独调试数据从主进程传进去模板本身不依赖业务页面状态打印坏了不会影响主界面。electron-vue-print-demo 这类项目的典型结构也是 B主窗口负责业务操作打印窗口是独立页面。缺点是数据要通过一次 IPC但打印本身是低频操作这点性能损耗可以忽略。路线 B 的数据传递方式是渲染进程通过 IPC 把要打印的数据发给主进程主进程新开一个隐藏窗口加载打印模板的 URL等页面渲染完成后再把数据注入模板窗口。这样模板里只用一段脚本读取注入的数据拼完 HTML 交给 CSS 排版打印逻辑和业务界面完全解耦。3. 把 electron vuecli3 打印小票的项目跑起来初始化到第一个打印弹窗3.1 用 Vue CLI 3 初始化项目并接入 electron-builder常见做法是用 vue add electron-builder 把 Electron 接入 Vue CLI 3。命令如下npm install -g vue/cli vue create electron-vue-print-demo cd electron-vue-print-demo vue add electron-builder第一行安装 Vue CLI如果你已经装过可以跳过。第二行创建 Vue CLI 3 项目模板选 default 就能继续不用急着上 TypeScript。vue add electron-builder 会自动安装 Electron、修改 package.json、生成一个主进程入口文件一般是 background.js。我建议在执行 vue add 时注意它的提示它会问“如何安装 Electron”选 npm 即可。如果下载慢把 Electron 的二进制下载地址换成国内镜像这一步不换会卡很久。装完之后 package.json 里会多出两个脚本electron:serve 用于开发模式启动 Electron 窗口electron:build 用于打包安装包。开发时只要跑 npm run electron:serveElectron 会加载当前 dev server。3.2 主进程创建打印窗口并接收渲染进程请求electron-builder 生成的主进程入口是 background.js里面已经有创建主窗口的逻辑。我们在此基础上加上打印窗口和 IPC 监听const { app, BrowserWindow, ipcMain } require(electron) const path require(path) let mainWindow null let printWin null function createMainWindow() { mainWindow new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, preload.js), contextIsolation: true, nodeIntegration: false } }) if (process.env.WEBPACK_DEV_SERVER_URL) { mainWindow.loadURL(process.env.WEBPACK_DEV_SERVER_URL) } else { mainWindow.loadFile(path.join(__dirname, index.html)) } } ipcMain.handle(print-receipt, async (event, payload) { const { url, deviceName, silent, ticketData } payload printWin new BrowserWindow({ width: 240, height: 600, show: false, webPreferences: { contextIsolation: true, nodeIntegration: false } }) await printWin.loadURL(url) await printWin.webContents.executeJavaScript( window.__ticketData ${JSON.stringify(ticketData)} ) printWin.webContents.on(did-finish-load, () { printWin.webContents.print( { silent: silent || false, deviceName: deviceName || , printBackground: true }, (success, failureReason) { console.log(print result:, success, failureReason) printWin.close() printWin null } ) }) return true }) app.whenReady().then(createMainWindow)代码逻辑渲染进程用 ipcRenderer.invoke 发起 print-receipt 请求主进程收到后先开一个隐藏窗口加载打印模板的 URL然后用 executeJavaScript 把要打印的数据挂到模板页面的 window 对象上。等 did-finish-load 触发再对该窗口调用 print。回调里能拿到 success 和 failureReason出纸后关闭窗口。参数说明width 和 height打印窗口的尺寸直接决定渲染内容宽度58mm 小票我常用 240px 宽对应约 48mm 打印区。silent为 true 时不弹系统打印对话框直接把作业送进打印机队列为 false 时弹对话框方便调试。deviceName传打印机设备名不传就用系统默认打印机。ticketData通过 executeJavaScript 注入的数据放在 loadURL 之后执行避免页面先渲染出空数据。3.3 渲染进程触发打印的最小代码与 preload 桥接为了在 Vue 组件里安全调用 Electron 能力我用 preload 暴露一个最小接口// preload.js const { contextBridge, ipcRenderer } require(electron) contextBridge.exposeInMainWorld(receiptPrinter, { print: (payload) ipcRenderer.invoke(print-receipt, payload) })contextIsolation 开启时网页上下文和 Electron 主进程隔离Vue 里不能用 require(electron)所以要用 contextBridge 暴露白名单方法。主窗口和打印窗口都指向同一个 preload后续打印模板如果要读取系统信息也能复用同一个能力。Vue 组件里的触发代码// PrintButton.vue script export default { methods: { async onClickPrint() { try { const res await window.receiptPrinter.print({ url: process.env.BASE_URL #/receipt-template, deviceName: 58mm 热敏打印机, silent: true, ticketData: { shopName: 某店, orderNo: NO.20250101, items: [ { name: 咖啡, qty: 1, price: 28.00 } ], total: 28.00 } }) console.log(print resolved, res) } catch (err) { console.error(print failed, err) } } } } /scripturl 是打印模板的 hash 路由地址。如果用 history 模式路由加载 #/receipt-template 时就要带路由 base 的完整地址更省事的做法是小票模板单独做一个静态页面不依赖 Vue Router减少加载时间。ticketData 在按钮事件里构造实际项目里一般是从订单状态对象里提取。4. 小票打印必调参数58mm 纸张、打印机名与静默打印的边界4.1 print 参数逐个拆解silent、deviceName、printBackgroundprint 方法最常用的参数如下表参数类型默认值说明silentbooleanfalsetrue 时静默送纸不弹打印对话框deviceNamestring系统默认打印机指定打印机设备名printBackgroundbooleanfalse是否打印背景色和背景图colorbooleantrue是否按彩色方式输出landscapebooleanfalse是否横版marginsobject默认边距可传 top / bottom / left / right小票场景必调的是前三个。printBackground 必须打开否则模板里的深色 header、logo 会变白块。color 对热敏小票意义不大但如果你用标签纸打印带色二维码保持 true。margins 也容易被忽略。热敏小票纸本身打印区域左右已经很窄默认边距再加上 5mm 到 10mm最后输出和模板预览会差很多。我一般把 margins 全部置 0用 CSS padding 来控制内容位置。这样排版更可控也避免不同打印机驱动对边距理解不一致。4.2 用 CSS 控制小票宽度page 与容器宽度的配合只靠 print 接口参数还不够渲染层必须匹配小票纸宽。我常用的 CSS 模板media print { page { size: 58mm 120mm; margin: 0; } body { margin: 0; width: 48mm; background: #fff; } .receipt { width: 48mm; padding: 2mm 1mm; font-size: 12px; line-height: 1.4; } .receipt table { width: 100%; } }page 里的 size 是告诉打印驱动期望的纸张规格这里的边长决定打印机走纸位置。.receipt 容器宽度 48mm 对应 58mm 小票纸的有效打印区。不同热敏驱动对 page 的支持不完全一致有些老驱动直接忽略 size只认驱动里预设纸张所以要同时在打印接口里控制窗口宽度和内容宽度两条腿走路才能最大概率对齐。小票字体有个经验值最小不要小于 10px。12px 正文字号在热敏纸上比较清楚低于 10px 容易糊成一片。同字号在白色不干胶标签纸上会更清晰因为热敏纸高速走纸时会晕色。二维码尺寸不要小于 36px太小的二维码在高速打印时容易断码、扫不出来。4.3 静默打印的边界驱动、默认打印机与切纸指令silent: true 做到点按钮直接出票但有三个边界必须清楚。第一silent 不等于绕过打印队列。作业还是会进系统假脱机如果打印机卡纸、缺纸任务会排在那里界面没有取消机会。生产环境里最好在打印前做一次设备状态检查或者至少给出“已送打”的提示。第二deviceName 必须和系统里显示的打印机名完全一致。空格也会导致找不到打印机最常见的翻车是设备名带后缀“副本 1”或“(Copy 1)”拷贝驱动后名称变了代码没更新。第三小票机切纸有两种方式驱动自动切纸和指令切纸。Electron 的 webContents.print 只负责把文档内容送到打印机切纸指令由驱动和打印机协议处理。如果换了打印机后任务显示成功但不出票去打印机驱动设置里找“切纸模式”改成自动切纸。关于纸张尺寸print 接口里的 pageSize 单位是微米。58mm 宽是 5800080mm 宽是 80000高度可以给一个较大值因为走纸长度由内容实际高度决定。单位传错会导致打印出异常大的白纸这个坑我见过不止一次。5. 打印翻车现场排查常见问题定位与虚拟打印机验收5.1 打印空白先查加载完成时机现象打印出纸纸上空白或 PDF 空白。原因多是 did-finish-load 触发时异步渲染还没完成隐藏窗口就开始打印。解决在 did-finish-load 后轮询模板页面的渲染完成标志再调 print。await printWin.webContents.executeJavaScript( new Promise(resolve { if (window.__renderDone) resolve(true) else setTimeout(() resolve(window.__renderDone), 500) }) )5.2 样式错乱直奔纸张定义三件套现象内容被左右裁切或留大片白边。先检查三件事打印接口 pageSize、CSS page 尺寸、打印机驱动里的纸张定义。容器宽度超过有效打印区也会裁切把 48mm 写死到模板宽度层。5.3 打印机不响应设备名与队列流水线现象silent: true 点击后无反应。先调用 getPrintersAsync 列出系统识别到的设备名核对 deviceName再把 silent 改 false 手动打一次能弹框说明驱动和队列正常问题只在设备名。如果手动也卡住去系统打印队列清掉积压任务。5.4 用虚拟打印机做验收低成本回归方法Windows 自带的“Microsoft Print to PDF”可以当验收工具。每次改打印模板先用虚拟 PDF 打印机跑一遍确认排版后再对真实热敏机出票。检查三件事单票内容完整、左右留白均匀、二维码能扫出内容。我最早做小票打印时连续几次遇到“打印空白”最后发现都是加载时机太早。后来养成习惯改一次模板先用 PDF 虚拟打印机回归一次再真机出票。Electron 的小票打印其实不算玄学把加载完成时机控制住、打印机名核对准稳定度比纯浏览器方案高一个数量级。希望这篇能帮你把 electron-vue-print-demo 跑通再改成自己的店铺模板。本文还有配套的精品资源点击获取