1. 项目背景与核心需求
在移动应用开发领域,蓝牙打印功能一直是个高频需求场景。最近接手了一个超市收银系统的升级项目,核心诉求是要在uniapp框架下实现小票打印功能。市面上常见的58mm热敏打印机基本都支持蓝牙连接,这比传统的网络打印方案更灵活,尤其适合没有固定WiFi覆盖的移动收银场景。
选择uniapp主要考虑三点:一是客户要求同时支持Android和iOS双端;二是团队对Vue技术栈更熟悉;三是项目周期紧张需要快速迭代。实际开发中发现,虽然uniapp官方文档提供了基础蓝牙API,但完整实现打印流程需要处理不少细节问题。
2. 蓝牙打印技术架构解析
2.1 蓝牙协议栈选择
热敏打印机通常采用BLE(蓝牙4.0+)或经典蓝牙(SPP协议)两种通信方式。实测发现市面80%的打印机如芯烨XP-58B、佳博GP-5890X都同时支持两种模式:
- BLE模式功耗更低但传输速率较慢(约1KB/s)
- SPP模式传输稳定(可达30KB/s)但配对流程复杂
考虑到小票打印数据量不大(普通小票约2-3KB),最终选择BLE方案。关键优势在于:
- 无需系统级配对(iOS限制)
- 支持同时连接多台设备
- 自动重连机制更完善
2.2 打印指令集处理
所有热敏打印机都遵循ESC/POS指令标准,核心指令包括:
// 基本指令示例 const commands = { INIT: [0x1B, 0x40], // 打印机初始化 ALIGN_LEFT: [0x1B, 0x61, 0x00], // 左对齐 CUT_PAPER: [0x1D, 0x56, 0x41, 0x00] // 全切纸 }实际开发中需要处理中文编码转换问题。经过测试,需要先将UTF-8文本转为GB18030编码(兼容GBK):
function strToBytes(text) { const gbBuffer = new GB18030().encode(text) return [...new Uint8Array(gbBuffer)] }3. Uniapp蓝牙模块实战
3.1 设备发现与连接
uniapp的蓝牙API封装了平台差异,但iOS和Android仍有细节差异:
// 初始化蓝牙模块 uni.openBluetoothAdapter({ success: () => { this.startDiscovery() }, fail: (err) => { console.error('蓝牙初始化失败:', err) // iOS需提示用户开启蓝牙权限 if(plus.os.name === 'iOS') { uni.showModal({ content: '请在系统设置中开启蓝牙权限' }) } } }) // 搜索设备 startDiscovery() { uni.onBluetoothDeviceFound((devices) => { this.deviceList = devices.filter(device => device.name.includes('POS') || device.localName.includes('58mm') ) }) uni.startBluetoothDevicesDiscovery() }关键经验:Android设备需要先调用
getBluetoothAdapterState检查蓝牙状态,而iOS在第一次调用时会自动弹出授权框。实测发现华为手机需要额外处理位置权限才能搜索到设备。
3.2 数据通信实现
建立连接后需要处理的核心流程:
- 获取服务UUID:
const services = await uni.getBLEDeviceServices({ deviceId: this.deviceId }) this.serviceId = services.services.find(s => s.uuid.startsWith('0000ffe0') ).uuid- 订阅特征值:
uni.notifyBLECharacteristicValueChange({ deviceId, serviceId, characteristicId: this.charId, state: true })- 数据分包发送(BLE单包限制20字节):
function sendData(data) { const chunkSize = 18 // 保留2字节头尾 for(let i=0; i<data.length; i+=chunkSize) { const chunk = data.slice(i, i+chunkSize) uni.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId: this.charId, value: this.arrayBufferToBase64(chunk) }) // 添加50ms间隔防止丢包 await new Promise(r => setTimeout(r, 50)) } }4. 打印功能完整实现
4.1 小票排版引擎设计
实现了一个简单的DSL来描述小票格式:
const ticket = { header: { type: 'text', content: '**星巴克咖啡**', align: 'center', bold: true, size: 2 }, items: [ { type: 'line', text: '商品名称 单价 数量 小计' }, { type: 'item', name: '拿铁', price: 32, count: 2 }, { type: 'separator' } ], footer: { type: 'qrcode', content: 'https://pos.example.com/order/123' } }转换器实现核心逻辑:
function buildESCCommands(ticket) { let buffer = [] // 添加初始化指令 buffer.push(...commands.INIT) // 处理标题 buffer.push(...commands.ALIGN_CENTER) buffer.push(...commands.TEXT_SIZE_LARGE) buffer.push(...strToBytes(ticket.header.content)) // 处理商品列表 ticket.items.forEach(item => { if(item.type === 'line') { buffer.push(...commands.ALIGN_LEFT) buffer.push(...strToBytes(item.text + '\n')) } // 其他类型处理... }) return new Uint8Array(buffer) }4.2 打印状态监控
通过监听特征值变化实现状态反馈:
uni.onBLECharacteristicValueChange((res) => { const value = res.value // 解析打印机状态字节 const status = { paperLow: (value[0] & 0x04) !== 0, coverOpen: (value[0] & 0x20) !== 0 } if(status.paperLow) { uni.showToast({ title: '纸张不足', icon: 'none' }) } })5. 跨平台兼容性处理
5.1 iOS特殊处理
后台运行限制:
- 需要在manifest.json配置
UIBackgroundModes包含bluetooth-central - 应用退到后台后,iOS会限制蓝牙操作,需要添加心跳包保持连接
- 需要在manifest.json配置
状态恢复:
// App唤醒时检查已有连接 uni.getConnectedBluetoothDevices({ services: ['0000FFE0-0000-1000-8000-00805F9B34FB'], success: (res) => { if(res.devices.length > 0) { this.deviceId = res.devices[0].deviceId this.autoReconnect() } } })5.2 Android厂商适配
- 小米手机需要在
AndroidManifest.xml添加:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>- 华为EMUI需要额外处理:
// 检测到华为设备时 if(plus.device.vendor === 'HUAWEI') { uni.authorize({ scope: 'scope.bluetooth', success: () => console.log('蓝牙授权成功') }) }6. 性能优化实践
6.1 数据压缩策略
对小票中的重复内容采用压缩编码:
function compressText(text) { // 将常用商品名称映射为1字节编码 const dict = { '拿铁': 0x81, '美式': 0x82 } return text.replace(/拿铁|美式/g, m => String.fromCharCode(dict[m]) ) }实测使传输数据量减少40%,打印速度提升明显。
6.2 连接池管理
维护一个活跃连接池避免重复连接:
class BluetoothPool { constructor(max = 3) { this.connections = new Map() } getConnection(deviceId) { if(!this.connections.has(deviceId)) { const conn = new BluetoothConnection(deviceId) this.connections.set(deviceId, conn) } return this.connections.get(deviceId) } }7. 实际踩坑记录
字节对齐问题: 发现部分打印机在接收UTF-8文本时会丢失字节,最终定位是BLE MTU设置问题。解决方案:
// 安卓需要手动设置MTU uni.setBLEMTU({ deviceId, mtu: 128, success: () => console.log('MTU设置成功') })打印乱码问题:
- 现象:中文显示为问号
- 原因:未正确处理GBK编码
- 解决:在转换Buffer时强制指定编码:
const encoder = new TextEncoder('gb18030')iOS连接不稳定:
- 现象:频繁断开连接
- 原因:系统节能策略
- 解决:添加5秒一次的心跳包:
setInterval(() => { this.writeBLEValue([0x00]) // 空指令 }, 5000)
8. 扩展功能实现
8.1 打印预览功能
通过canvas生成预览图:
const ctx = uni.createCanvasContext('preview') ctx.setFontSize(16) ctx.fillText('商品名称 单价', 10, 20) // 绘制表格线 ctx.moveTo(10, 25) ctx.lineTo(200, 25) ctx.stroke() ctx.draw()8.2 批量打印模式
实现队列管理:
class PrintQueue { constructor() { this.queue = [] this.isPrinting = false } add(task) { this.queue.push(task) this.next() } next() { if(!this.isPrinting && this.queue.length) { this.isPrinting = true const task = this.queue.shift() task().finally(() => { this.isPrinting = false this.next() }) } } }9. 安全与稳定性保障
9.1 数据传输加密
对敏感订单信息进行AES加密:
function encryptData(data, key) { const CryptoJS = require('crypto-js') return CryptoJS.AES.encrypt( JSON.stringify(data), key ).toString() }9.2 异常恢复机制
实现自动重连策略:
let retryCount = 0 function reconnect() { if(retryCount > 3) return uni.createBLEConnection({ deviceId, success: () => { retryCount = 0 this.initPrinter() }, fail: () => { setTimeout(() => { retryCount++ this.reconnect() }, 1000 * retryCount) } }) }10. 项目部署与监控
10.1 灰度发布策略
通过版本号控制功能开启:
// 在云函数中控制功能开关 const features = { bluetoothPrint: { version: '1.2.0', enable: true } }10.2 打印日志收集
建立监控系统收集异常:
uni.onBLEConnectionStateChange((res) => { if(!res.connected) { this.logError({ type: 'disconnect', deviceId: res.deviceId, timestamp: Date.now() }) } }) function logError(data) { uni.request({ url: 'https://api.example.com/logs', method: 'POST', data }) }整个项目从零开始到上线用了3周时间,最终实现了:
- 平均打印速度:2秒/张小票
- 连接成功率:Android 98.7%,iOS 95.2%
- 异常自动恢复率:89%
最大的收获是深入理解了BLE在移动端的实现细节,特别是不同厂商设备的兼容性处理。建议后续开发者重点关注:
- 建立完善的设备指纹识别系统
- 实现指令级重试机制
- 设计可扩展的打印模板引擎