微信小程序/UniApp蓝牙开发:如何优雅地封装一个可复用的蓝牙通信库(Vue3 Composition API)
Vue3+UniApp蓝牙通信库架构设计:从零封装高可用BLE管理器
在物联网应用爆发式增长的今天,蓝牙低能耗(BLE)技术已成为连接智能设备的首选方案。作为前端开发者,当我们面对需要同时控制多台蓝牙打印机、门锁和传感器的商业项目时,直接在页面中堆砌蓝牙API调用显然不是可持续的方案。本文将带你从工程化角度,构建一个基于Vue3 Composition API的蓝牙通信库,解决以下痛点:
- 重复代码:每个页面都需要重写初始化、连接、监听流程
- 状态混乱:多设备连接时状态维护困难
- 异常处理缺失:断连重试、错误恢复机制不完善
- 类型安全弱:JavaScript的弱类型导致蓝牙操作易出错
1. 核心架构设计
1.1 分层模型设计
一个健壮的蓝牙库应该遵循清晰的分层原则:
应用层 (UI Components) ↓ 服务层 (Bluetooth Manager) ↓ 适配器层 (Platform API) ↓ 原生层 (iOS/Android/小程序)我们主要聚焦服务层的实现,它需要提供以下能力:
- 设备发现与连接管理
- 消息收发管道
- 状态机管理
- 错误恢复机制
1.2 技术选型对比
| 方案类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Class-based | 状态集中管理 | Vue集成不够自然 | 复杂多设备场景 |
| Hook-based | 与Vue3完美契合 | 复杂逻辑拆分困难 | 中小型应用 |
| Hybrid模式 | 兼顾灵活与组织性 | 实现复杂度较高 | 本文推荐方案 |
我们采用混合模式:用Class管理核心逻辑,通过Hook暴露API。
// 架构示意 class BluetoothCore { // 设备池管理 private devices: Map<string, Device> = new Map() // 连接状态机 private stateMachine: StateMachine = new StateMachine() // 平台API适配层 private adapter: BluetoothAdapter = uni // 或wx } export function useBluetooth() { const core = new BluetoothCore() return { scan: core.scan.bind(core), connect: core.connect.bind(core) } }2. 关键实现细节
2.1 设备发现优化策略
原始API的startBluetoothDevicesDiscovery存在性能问题,我们通过以下策略优化:
- 节流搜索:自动限制扫描频率
- 设备指纹:基于MAC地址和Service UUID过滤
- 缓存机制:记住最近连接设备
// 设备发现优化实现 async function enhancedDiscovery(options: DiscoveryOptions) { // 合并硬件厂商提供的服务UUID const services = [ ...DEFAULT_SERVICES, ...(options.vendorServices || []) ] // 添加防抖逻辑 return new Promise((resolve) => { this.discoveryTimer = setTimeout(async () => { await this.adapter.startBluetoothDevicesDiscovery({ services, success: (res) => { this.cacheDevices(res.devices) resolve(res) }, fail: this.errorHandler }) }, 300) // 300ms防抖阈值 }) }2.2 连接状态管理
蓝牙连接需要维护复杂的状态流转:
[Disconnected] ↓ [Discovering] → [Error] ↓ [Connecting] → [Error] ↓ [Connected] ↓ [Disconnecting] → [Error]使用状态机模式封装:
class ConnectionStateMachine { private states = { DISCONNECTED: { connect: 'CONNECTING' }, CONNECTING: { success: 'CONNECTED', fail: 'ERROR' }, CONNECTED: { disconnect: 'DISCONNECTING' } } private currentState = 'DISCONNECTED' transition(action: string) { const nextState = this.states[this.currentState][action] if (nextState) { this.currentState = nextState return true } return false } }2.3 自动重连机制
实现可靠的断线重连需要考虑:
- 指数退避:重试间隔逐渐增加
- 心跳检测:定期检查连接状态
- 最大重试:避免无限重连
// 重连策略实现 private async handleReconnect(deviceId: string) { let retries = 0 const maxRetries = 5 while (retries < maxRetries) { try { await this.connectDevice(deviceId) return } catch (error) { const delay = Math.min(1000 * 2 ** retries, 30000) await new Promise(resolve => setTimeout(resolve, delay)) retries++ } } throw new Error(`Max reconnect attempts (${maxRetries}) exceeded`) }3. 高级功能实现
3.1 多设备并行管理
商业场景常需同时控制多台设备,我们设计设备池模式:
class DevicePool { private devices: Map<string, DeviceContext> = new Map() addDevice(device: DeviceSpec) { const context = { ...device, connection: new Connection(), queue: new OperationQueue() } this.devices.set(device.id, context) } async broadcast(command: Command) { const results = await Promise.all( Array.from(this.devices.values()).map( device => device.queue.enqueue(() => this.sendCommand(device, command) ) ) ) return results } }3.2 消息队列设计
蓝牙通信需要严格顺序执行指令,我们实现优先级队列:
class OperationQueue { private pending: Array<() => Promise<any>> = [] private isProcessing = false enqueue(task: () => Promise<any>, priority = 0) { return new Promise((resolve, reject) => { const wrappedTask = async () => { try { const result = await task() resolve(result) } catch (error) { reject(error) } } this.pending.splice( this.pending.findIndex(item => item.priority < priority), 0, { execute: wrappedTask, priority } ) this.processNext() }) } private processNext() { if (this.isProcessing || this.pending.length === 0) return this.isProcessing = true const task = this.pending.shift() task.execute().finally(() => { this.isProcessing = false this.processNext() }) } }3.3 类型安全增强
使用TypeScript定义完整类型体系:
interface DeviceSpec { id: string name: string services: { main: UUID characteristics: { read: UUID write: UUID notify?: UUID } } } type BluetoothEvent = | { type: 'DEVICE_FOUND'; devices: DeviceSpec[] } | { type: 'CONNECTION_STATE_CHANGE'; deviceId: string; state: ConnectionState } | { type: 'MESSAGE_RECEIVED'; deviceId: string; data: ArrayBuffer } class TypedEventEmitter { private listeners = new Map<string, Set<Function>>() on<T extends BluetoothEvent>(type: T['type'], listener: (event: T) => void) { if (!this.listeners.has(type)) { this.listeners.set(type, new Set()) } this.listeners.get(type).add(listener) } }4. 实战应用示例
4.1 打印机控制集成
封装面向业务的打印机管理器:
class PrinterManager { constructor(private bluetooth: BluetoothManager) { bluetooth.on('MESSAGE_RECEIVED', (event) => { if (event.deviceId === this.currentPrinter?.id) { this.handlePrinterResponse(event.data) } }) } async printReceipt(content: ReceiptContent) { const commands = this.buildPrintCommands(content) await this.bluetooth.sendInBatch(this.currentPrinter.id, commands) } private buildPrintCommands(content: ReceiptContent): Buffer[] { return [ PrinterCommands.INIT, ...content.lines.map(line => Buffer.from(`${line}\n`, 'utf-8') ), PrinterCommands.CUT_PAPER ] } }4.2 在UniApp中的使用
提供符合Vue使用习惯的Hook:
// useBluetooth.ts export function useBluetooth() { const devices = ref<DeviceSpec[]>([]) const connectionState = ref<ConnectionState>('disconnected') const manager = new BluetoothManager({ onDeviceFound: (newDevices) => { devices.value = [...devices.value, ...newDevices] }, onStateChange: (state) => { connectionState.value = state } }) return { devices, connectionState, scan: manager.scan.bind(manager), connect: manager.connect.bind(manager) } }页面中使用:
<script setup> const { devices, scan, connect } = useBluetooth() onMounted(() => { scan({ services: ['0000FFE0-0000-1000-8000-00805F9B34FB'] }) }) </script> <template> <view v-for="device in devices" @click="connect(device.id)"> {{ device.name }} </view> </template>4.3 性能优化技巧
- 连接池预热:提前初始化常用设备连接
- 指令批处理:合并多次写入操作
- 二进制压缩:优化数据传输效率
// 指令批处理示例 async function sendInBatch(deviceId: string, commands: Buffer[]) { const device = this.getDevice(deviceId) const merged = Buffer.concat(commands) // 分片发送避免MTU限制 const chunkSize = device.mtu - 3 // 保留协议头 for (let i = 0; i < merged.length; i += chunkSize) { const chunk = merged.slice(i, i + chunkSize) await this.writeCharacteristic(deviceId, chunk) } }在开发过程中,我发现蓝牙通信最棘手的不是正常流程,而是对各种异常情况的处理。比如当用户突然切换系统蓝牙开关时,需要监听bluetoothAdapterStateChange事件并同步更新所有设备状态。经过多次迭代,最终形成的这套架构已经在生产环境稳定运行,支持同时管理20+台蓝牙设备。
