从零到一:uniapp蓝牙打印实战避坑指南

张开发
2026/4/13 18:03:06 15 分钟阅读

分享文章

从零到一:uniapp蓝牙打印实战避坑指南
1. 为什么选择uniapp开发蓝牙打印功能跨平台开发已经成为移动应用开发的主流趋势而uniapp作为国内最流行的跨平台框架之一确实为开发者提供了极大的便利。我在实际项目中多次使用uniapp开发蓝牙打印功能发现它相比原生开发有几个明显的优势。首先uniapp的代码复用率极高。一套代码可以同时运行在iOS和Android平台这对于需要快速迭代的项目来说简直是福音。记得我第一次用uniapp开发蓝牙打印功能时原本预计需要两周时间分别开发两个平台结果只用了一周就完成了双平台的适配。其次uniapp对蓝牙API的封装相当完善。它提供了统一的蓝牙操作接口包括设备发现、连接、数据读写等核心功能。这意味着开发者不需要关心底层平台差异可以专注于业务逻辑的实现。不过这里有个小坑需要注意不同平台对蓝牙设备的支持程度可能略有差异后面我会详细说明。蓝牙打印在零售、物流、医疗等行业应用广泛。比如超市的价签打印、快递面单打印、医院的检验报告打印等场景都很常见。uniapp的跨平台特性正好满足了这些行业多终端使用的需求。我最近参与的一个医药项目就是使用uniapp开发的医生可以在iPad上开处方护士用Android设备连接蓝牙打印机直接打印用药说明整个过程非常流畅。2. 开发前的准备工作2.1 硬件准备工欲善其事必先利其器。在开始编码前我们需要准备好开发环境。根据我的经验以下硬件是必备的支持蓝牙4.0及以上的手机或平板建议准备Android和iOS设备各一台进行测试兼容的蓝牙打印机市面上常见的有佳博、爱普生等品牌打印纸尺寸要与业务需求匹配这里特别提醒一点不同品牌的蓝牙打印机协议可能不同。我最初使用佳博打印机开发后来换用其他品牌时发现指令集不兼容不得不重写打印指令部分。建议在项目初期就确定打印机型号并获取对应的开发文档。2.2 开发环境配置uniapp开发蓝牙功能需要配置一些必要的权限。在manifest.json文件中需要添加以下权限配置{ mp-weixin: { appid: , requiredBackgroundModes: [audio, location], permission: { scope.userLocation: { desc: 你的位置信息将用于蓝牙设备扫描 } } }, app-plus: { distribute: { android: { permissions: [ uses-permission android:name\android.permission.BLUETOOTH\/, uses-permission android:name\android.permission.BLUETOOTH_ADMIN\/, uses-permission android:name\android.permission.ACCESS_COARSE_LOCATION\/, uses-permission android:name\android.permission.ACCESS_FINE_LOCATION\/ ] } } } }注意从Android 6.0开始蓝牙扫描需要位置权限。这是一个很容易被忽略的坑点我在第一个项目中就栽在这里调试了半天才发现是权限问题。3. 蓝牙连接全流程详解3.1 初始化蓝牙适配器任何蓝牙操作前都必须先初始化蓝牙适配器。uniapp提供了uni.openBluetoothAdapter方法来初始化蓝牙模块uni.openBluetoothAdapter({ success(res) { console.log(蓝牙适配器初始化成功); // 这里可以开始搜索设备 this.startDiscovery(); }, fail(err) { console.error(蓝牙适配器初始化失败, err); if (err.errCode 10001) { uni.showToast({ title: 当前设备不支持蓝牙功能, icon: none }); } } });这里有个重要经验一定要检查设备是否支持蓝牙。特别是在iOS设备上系统设置中关闭蓝牙后虽然硬件支持但API会返回不支持的错误。我建议在初始化失败时给用户明确的提示而不是简单的初始化失败。3.2 设备发现与连接发现蓝牙设备是连接的前提。uniapp提供了uni.startBluetoothDevicesDiscovery方法来搜索附近的蓝牙设备startDiscovery() { uni.startBluetoothDevicesDiscovery({ services: [], // 指定要搜索的服务UUID allowDuplicatesKey: false, // 是否允许重复上报同一设备 success: (res) { console.log(开始搜索设备, res); // 监听找到新设备的事件 this.onDeviceFound(); }, fail: (err) { console.error(搜索设备失败, err); } }); } onDeviceFound() { uni.onBluetoothDeviceFound((devices) { devices.devices.forEach(device { if (device.name !this.devices.some(d d.deviceId device.deviceId)) { this.devices.push({ name: device.name, deviceId: device.deviceId }); } }); }); }在实际项目中我发现设备名称过滤很重要。很多蓝牙设备会广播一些无意义的名称如果不加过滤会显示大量无用设备。建议根据业务需求设置名称过滤规则比如只显示特定前缀的设备。连接设备使用uni.createBLEConnection方法connectDevice(deviceId) { uni.showLoading({ title: 连接中... }); uni.createBLEConnection({ deviceId, success: (res) { console.log(设备连接成功, res); this.getServices(deviceId); }, fail: (err) { uni.hideLoading(); console.error(设备连接失败, err); } }); }这里有个关键点连接成功后要获取设备的服务(service)和特征值(characteristic)。这是后续数据通信的基础。4. 打印功能实现与数据分包处理4.1 获取服务与特征值连接设备后我们需要获取设备的服务和服务特征getServices(deviceId) { uni.getBLEDeviceServices({ deviceId, success: (res) { res.services.forEach(service { this.getCharacteristics(deviceId, service.uuid); }); }, fail: (err) { console.error(获取服务失败, err); } }); } getCharacteristics(deviceId, serviceId) { uni.getBLEDeviceCharacteristics({ deviceId, serviceId, success: (res) { res.characteristics.forEach(char { if (char.properties.write) { // 保存可写的特征值 this.characteristicId char.uuid; this.serviceId serviceId; uni.hideLoading(); uni.showToast({ title: 连接成功 }); } }); }, fail: (err) { console.error(获取特征值失败, err); } }); }这一步非常关键因为只有找到具有write属性的特征值才能发送打印数据。我在项目中遇到过特征值找不到的问题后来发现是服务UUID不对。建议打印出所有服务和特征值确认后再进行后续操作。4.2 打印指令生成与发送不同品牌的打印机使用不同的指令集。以ESC指令为例// 引入ESC指令库 const esc require(./esc.js); printText(content) { const deviceId this.deviceId; const serviceId this.serviceId; const characteristicId this.characteristicId; // 生成打印指令 let buffer esc .initialize() .text(content) .newLine(2) .cut() .encode(); // 发送数据 this.writeBLEValue(deviceId, serviceId, characteristicId, buffer); } writeBLEValue(deviceId, serviceId, characteristicId, buffer) { // 数据分包处理 const chunkSize 20; // 每次发送20字节 const chunks []; for (let i 0; i buffer.length; i chunkSize) { chunks.push(buffer.slice(i, i chunkSize)); } // 递归发送数据包 const sendChunk (index) { if (index chunks.length) return; const chunk chunks[index]; const arrayBuffer new ArrayBuffer(chunk.length); const dataView new DataView(arrayBuffer); for (let i 0; i chunk.length; i) { dataView.setUint8(i, chunk[i]); } uni.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId, value: arrayBuffer, success: () { sendChunk(index 1); }, fail: (err) { console.error(写入失败, err); } }); }; sendChunk(0); }这里有几个技术要点蓝牙数据传输有大小限制通常是20字节所以大数据要分包发送每包数据发送成功后再发送下一包打印指令要符合打印机的要求不同品牌指令可能不同5. 常见问题与解决方案5.1 设备连接不稳定这是最常见的问题之一。在我的项目中遇到过以下几种情况设备频繁断开连接这通常是因为没有保持蓝牙连接状态。解决方案是在App的onHide生命周期中不要关闭蓝牙连接或者实现自动重连机制。连接超时蓝牙连接有时会因为信号问题超时。建议设置合理的超时时间并在UI上给予用户反馈connectWithTimeout(deviceId, timeout 10000) { return new Promise((resolve, reject) { const timer setTimeout(() { reject(new Error(连接超时)); uni.closeBLEConnection({ deviceId }); }, timeout); uni.createBLEConnection({ deviceId, success: (res) { clearTimeout(timer); resolve(res); }, fail: (err) { clearTimeout(timer); reject(err); } }); }); }5.2 打印乱码或格式错误这类问题通常有三个原因字符编码问题确保发送的数据使用打印机支持的编码格式。中文通常需要GBK编码。指令格式错误不同打印机品牌使用不同的指令集。务必确认打印机型号和对应的指令集。数据分包问题如果分包处理不当会导致指令被截断。建议在关键指令前后加入延时。5.3 多平台兼容性问题uniapp虽然号称一次编写多端运行但在蓝牙实现上各平台还是有些差异iOS限制iOS对蓝牙设备名称获取有限制有时只能获取到设备UUID而非实际名称。Android碎片化不同厂商的Android设备蓝牙实现有差异特别是国产ROM可能修改了蓝牙栈。微信小程序差异在微信小程序中蓝牙API的调用方式与App端略有不同。针对这些问题我的经验是核心功能要在不同平台充分测试对平台差异进行抽象封装在UI上做好不同平台的提示和引导6. 性能优化与用户体验6.1 连接流程优化蓝牙打印的体验很大程度上取决于连接流程是否顺畅。我总结了几点优化建议设备缓存将已配对过的设备信息缓存起来下次优先尝试连接。// 尝试连接上次使用过的设备 tryConnectLastDevice() { const lastDevice uni.getStorageSync(lastBluetoothDevice); if (lastDevice) { this.connectDevice(lastDevice.deviceId) .then(() { uni.showToast({ title: 自动连接成功 }); }) .catch(() { this.startDiscovery(); }); } else { this.startDiscovery(); } }快速重连在连接失败时自动尝试其他方式连接而不是直接报错。状态管理清晰展示蓝牙连接状态让用户知道当前处于什么阶段。6.2 打印任务队列在实际使用中可能会遇到连续打印的需求。为了避免打印任务冲突我建议实现一个打印队列class PrintQueue { constructor() { this.queue []; this.isPrinting false; } addTask(task) { this.queue.push(task); if (!this.isPrinting) { this.processNext(); } } processNext() { if (this.queue.length 0) { this.isPrinting false; return; } this.isPrinting true; const task this.queue.shift(); task() .then(() { this.processNext(); }) .catch((err) { console.error(打印失败, err); this.processNext(); }); } } // 使用示例 const printQueue new PrintQueue(); function printReceipt(content) { return () { return new Promise((resolve, reject) { // 打印逻辑 printText(content) .then(resolve) .catch(reject); }); }; } // 添加打印任务 printQueue.addTask(printReceipt(订单1)); printQueue.addTask(printReceipt(订单2));这种设计可以确保打印任务有序执行避免数据混乱。6.3 错误处理与用户反馈良好的错误处理能显著提升用户体验。我建议分类处理错误将蓝牙错误分为可恢复和不可恢复两类分别处理。友好的提示避免直接显示技术性错误转换为用户能理解的语言。恢复引导在错误发生时提供明确的操作指引帮助用户解决问题。handleBluetoothError(err) { let message 蓝牙操作出错; switch (err.errCode) { case 10000: message 蓝牙未初始化请检查蓝牙是否开启; break; case 10001: message 当前设备不支持蓝牙功能; break; case 10002: message 蓝牙连接已断开请重新连接; break; // 其他错误码处理... } uni.showModal({ title: 提示, content: message, showCancel: false }); }7. 项目实战经验分享在最近的一个零售项目中我们使用uniapp开发了一个支持蓝牙打印的移动POS系统。这个项目让我积累了一些宝贵的实战经验设备兼容性测试我们测试了市面上主流的10款蓝牙打印机发现虽然都支持标准BLE协议但在具体实现上有很多差异。最终我们针对每款打印机都编写了特定的指令适配层。性能调优最初的打印速度较慢经过分析发现是数据分包策略不合理。调整分包大小和发送间隔后打印速度提升了3倍。用户反馈收集我们建立了用户反馈机制收集实际使用中的问题。比如有用户反映在嘈杂环境下连接困难我们增加了信号强度检测和自动重试机制。日志系统为了实现远程诊断我们开发了蓝牙操作日志系统可以记录完整的蓝牙操作流程方便排查问题。这个项目上线后客户反馈打印功能的稳定性比他们之前的原生应用还要好。这充分证明了uniapp在蓝牙开发方面的潜力。

更多文章