从零构建UniApp蓝牙扫码器:权限配置与设备交互实战指南
在仓库管理、零售盘点等需要快速录入条码的场景中,蓝牙扫码器能大幅提升工作效率。传统方案往往需要采购专用硬件设备,而利用UniApp框架,开发者完全可以用普通手机连接蓝牙扫码模块,快速构建跨平台扫码应用。本文将带您完整实现一个具备权限动态申请、设备自动配对、数据持久化存储的工业级解决方案。
1. 项目初始化与环境配置
在HBuilderX中新建UniApp项目时,建议选择"uni-app"模板而非"uni-app vite"版本,避免潜在的蓝牙API兼容性问题。项目创建后需进行三项基础配置:
Manifest权限声明
打开manifest.json文件,在"app-plus" → "distribute" → "android"节点下添加权限声明:"permissions": [ "android.permission.BLUETOOTH", "android.permission.BLUETOOTH_ADMIN", "android.permission.ACCESS_FINE_LOCATION", "android.permission.WRITE_EXTERNAL_STORAGE" ]iOS额外配置
对于iOS平台,需在manifest.json的"ios"节点添加:"UIBackgroundModes": ["bluetooth-central"], "NSBluetoothAlwaysUsageDescription": "需要蓝牙权限连接扫码设备"全局样式调整
修改App.vue的全局样式,确保页面能全屏显示扫描界面:page { height: 100%; background-color: #f5f5f5; }
提示:Android 6.0+系统要求动态申请危险权限,即使已在manifest声明,运行时仍需用户授权。
2. 动态权限管理策略
现代移动应用的最佳实践是在需要使用功能时才申请对应权限。我们设计分层请求机制:
2.1 权限状态检查封装
创建permission.js工具文件,实现统一权限检查接口:
const permissionMap = { bluetooth: 'scope.bluetooth', location: 'scope.userLocation', storage: 'scope.writePhotosAlbum' // 实际使用中替换为存储权限scope } export function checkPermission(type) { return new Promise((resolve, reject) => { uni.getSetting({ success(res) { const scope = permissionMap[type] if (res.authSetting[scope] === true) { resolve(true) } else if (res.authSetting[scope] === false) { // 已拒绝过,需要引导到设置页 showAuthModal(type).then(resolve).catch(reject) } else { // 首次询问 requestPermission(type).then(resolve).catch(reject) } }, fail: reject }) }) }2.2 优雅的权限请求流程
设计渐进式授权策略,当用户拒绝时展示解释性弹窗:
function showAuthModal(type) { const messages = { bluetooth: '扫码功能需要蓝牙权限搜索附近设备', location: 'Android系统要求定位权限才能发现蓝牙设备', storage: '需要存储权限保存扫描记录' } return new Promise((resolve) => { uni.showModal({ title: '权限申请', content: messages[type], confirmText: '去设置', success(res) { if (res.confirm) { uni.openSetting({ success() { resolve(true) }, fail() { resolve(false) } }) } else { resolve(false) } } }) }) }2.3 实际调用示例
在页面中按需请求权限组:
async function prepareForScan() { try { const results = await Promise.all([ checkPermission('bluetooth'), checkPermission('location') ]) if (results.every(Boolean)) { startBluetoothDiscovery() } else { uni.showToast({ title: '必要权限未授权', icon: 'none' }) } } catch (err) { console.error('权限检查异常:', err) } }3. 蓝牙设备发现与连接
3.1 设备扫描优化方案
常规的startBluetoothDevicesDiscovery存在耗电问题,我们实现带超时和节流的扫描:
let scanTimer = null function startSmartScan(duration = 10000) { return new Promise((resolve) => { const devicesMap = new Map() uni.startBluetoothDevicesDiscovery({ success: () => { scanTimer = setTimeout(() => { uni.stopBluetoothDevicesDiscovery() resolve(Array.from(devicesMap.values())) }, duration) // 监听发现新设备 uni.onBluetoothDeviceFound((res) => { res.devices.forEach(device => { if (device.name && !devicesMap.has(device.deviceId)) { devicesMap.set(device.deviceId, device) } }) }) } }) }) }3.2 设备筛选策略
针对扫码枪设备特征进行智能筛选:
1. **名称过滤**:常见扫码枪品牌命名特征 - Honeywell: "BT*" - Zebra: "RSD*" - Socket: "CHS*" 2. **服务UUID过滤**: ```javascript const SCANNER_SERVICES = [ '0000FF00-0000-1000-8000-00805F9B34FB', '0000FFE0-0000-1000-8000-00805F9B34FB' ] function isTargetDevice(device) { return device.name?.match(/BT|RSD|CHS/i) || SCANNER_SERVICES.some(uuid => device.advertisServiceUUIDs?.includes(uuid) ) }- 信号强度过滤:RSSI > -70dBm
### 3.3 稳定连接实现 建立连接后需要处理Android与iOS的差异行为: ```javascript function createConnection(deviceId) { return new Promise((resolve, reject) => { uni.createBLEConnection({ deviceId, success: () => { // Android需要延迟获取服务 setTimeout(() => { resolve(setupServices(deviceId)) }, uni.getSystemInfoSync().platform === 'android' ? 500 : 0) }, fail: reject }) }) } async function setupServices(deviceId) { try { const { services } = await uni.getBLEDeviceServices({ deviceId }) const targetService = services.find(s => s.uuid.toLowerCase().includes('ff00') ) if (targetService) { const { characteristics } = await uni.getBLEDeviceCharacteristics({ deviceId, serviceId: targetService.uuid }) const notifyChar = characteristics.find(c => c.properties.notify) if (notifyChar) { await enableNotifications(deviceId, targetService.uuid, notifyChar.uuid) return true } } return false } catch (err) { console.error('服务初始化失败:', err) return false } }4. 数据接收与存储方案
4.1 扫码数据解析
处理不同扫码枪的数据格式差异:
let buffer = '' uni.onBLECharacteristicValueChange((res) => { const value = new Uint8Array(res.value) // 常见结束符:回车(0x0A) 或 特殊字符 if (value.includes(0x0A)) { const parts = buffer.split('\n') buffer = parts.pop() || '' parts.forEach(code => { if (code.trim()) { saveToDatabase(code.trim()) } }) } else { buffer += String.fromCharCode.apply(null, value) } })4.2 本地存储优化
使用uniCloud.database实现离线优先策略:
const db = uniCloud.database() const scanCollection = db.collection('scan_records') const localCache = [] // 定时批量写入 setInterval(() => { if (localCache.length && navigator.onLine) { scanCollection.add(localCache.splice(0, 100)) } }, 5000) function saveToDatabase(code) { const record = { code, scanTime: Date.now(), device: currentDevice.name } localCache.push(record) uni.setStorageSync('last_scan', record) // 实时UI更新 uni.$emit('scan', record) }4.3 性能优化技巧
| 优化方向 | 具体措施 | 效果 |
|---|---|---|
| 蓝牙连接 | 连接池管理 | 减少重复连接开销 |
| 数据解析 | 流式处理 | 降低内存占用 |
| 存储写入 | 批量提交 | 减少数据库操作 |
| 界面渲染 | 虚拟列表 | 流畅显示历史记录 |
在实际仓库管理场景中,我们还需要考虑以下异常情况处理:
uni.onBLEConnectionStateChange((res) => { if (!res.connected) { showReconnectDialog(res.deviceId) } }) function showReconnectDialog(deviceId) { // 显示带倒计时自动重连的UI组件 let retryCount = 0 const maxRetry = 3 function attemptReconnect() { if (retryCount++ < maxRetry) { createConnection(deviceId).then(success => { if (!success) setTimeout(attemptReconnect, 2000) }) } } attemptReconnect() }通过这套完整实现,开发者可以快速构建出适用于零售、物流等行业的专业级扫码解决方案。在真实项目部署时,建议增加设备管理界面,支持多台扫码枪同时工作,并加入扫描统计报表功能。对于需要更高性能的场景,可以考虑使用原生插件来优化蓝牙通信层。