资讯动态

uni-app跨平台蓝牙自动开启与BLE连接实战解析

发布时间:2026/9/13 5:08:50 来源:尧图企业网站定制
简介这是一份基于uni-app框架的跨平台蓝牙APP开发Demo面向移动端开发者与智能硬件爱好者重点解决APP中自动开启蓝牙、设备扫描连接及数据交换等常见需求。工程从项目初始化、蓝牙插件集成入手完整覆盖状态检测、未开启时自动弹窗引导、扫描连接及读写操作并对iOS与Android的API差异做了适配适合智能家居、健康监测等物联网场景快速参考。压缩包共43个文件含png界面素材、js业务逻辑、css样式、ttf字体、json配置及db数据文件等同时附有可直接安装的apk安装包整体仅3.72MB轻量易用。目录按开发资源与打包产物组织便于定位源码、配置和说明。目前已有1210人学习下载是快速上手uni-app蓝牙开发、减少踩坑的不错参考资料。1. 跨平台蓝牙不是玄学为什么uni-app的自动开启蓝牙值得拆开看在接触APP蓝牙Demo.zip之前我一直认为跨平台蓝牙要跑通得用原生插件桥接直到把这个uni-app项目拆开看才意识到蓝牙业务的全流程其实可以用内置API串起来。这个Demo以Vue语法编写通过manifest.json声明权限在App、微信小程序和H5上共用一套逻辑核心交互是首次进入页面时检测蓝牙适配器状态弹窗引导用户开启蓝牙随后扫描、连接并读写设备特征值。真正值得拆开看的不是那几行工具函数而是“自动开启”在iOS和Android上的边界——系统不允许App直接打开蓝牙开关只能通过API初始化适配器并借助系统设置完成引导。这个边界决定了后续所有扫描、连接和断线重连代码的走向。适合刚接手蓝牙项目的前端、全栈和移动端开发也适合理清BLE协议层级后复用这套流程。2. 工程化准备初始化uni-app项目、权限声明与蓝牙API选型2.1 项目结构先从unpackage和manifest.json认识编译产物解压后你会发现Demo目录里没有复杂的源码分层但有一个容易误改的unpackage目录APP蓝牙Demo/ ├── manifest.json ├── pages/ │ ├── index/ │ │ └── index.vue │ └── common/ │ └── bluetooth.js ├── unpackage/ │ ├── dist/ │ │ └── dev/app-plus/ │ └── release/ ├── .dependencies └── README.mdunpackage是uni-app编译输出目录每次运行或打包都会被重写不能在它里面改任何代码。dist下按dev/app-plus与release区分调试包和正式包真正上架的apk/ipa产物在release对应平台目录。.dependencies由HBuilderX自动生成记录依赖解析结果提交git时建议忽略。真正决定蓝牙权限的是根目录的manifest.json。它的源码视图里包含mp-weixin、app-plus和h5三段配置其中app-plus下的权限描述会直接影响App是否能在系统设置中正常申请蓝牙授权。如果你改了unpackage/release/manifest.json下次打包会被根目录配置覆盖等于白改。2.2 蓝牙能力选型官方API与uni-ble插件的能力边界摘要里提到的uni-ble插件是社区封装但它不是唯一选项。uni-app官方已经内置了一套基于uni.*前缀的蓝牙API这个Demo实际上完全可以用内置API实现。两种方案的取舍如下对比项官方蓝牙APIuni-ble插件依赖内置零安装npm安装需处理版本返回风格success/fail回调链式调用与Promise跨端一致性HBuilderX统一适配跟随社区迭代调试透明度错误码直接暴露封装层可能吞掉部分错误适合场景Demo、核心蓝牙逻辑多设备管理封装我通常会先用官方API把蓝牙流程跑通再根据项目规模决定是否封装自己的工具层。对自动开启、扫描、连接这个Demo场景官方API的openBluetoothAdapter和createBLEConnection已经够用没有必要引入额外依赖。反而在社区插件的文档与uni-app版本不完全同步时一些Android新机型上的权限适配会变成黑盒出了问题只能去翻node_modules源码。2.3 创建项目并安装依赖三条命令背后的配置要复现这个Demo你可以使用CLI方式初始化项目npx degit dcloudio/uni-preset-vue#vite myBluetoothApp cd myBluetoothApp npm installdegit命令拉取的是vue3vite官方模板如果你要运行的Demo是基于vue2需要把分支换成#vue2否则main.js里的Vue实例化方式与vite编译插件不匹配。npm install完成后再用HBuilderX打开项目目录HBuilderX会读取manifest.json并补齐原生SDK配置。这里有一处容易踩坑HBuilderX里的项目类型识别失败时蓝色小程序图标不会出现此时manifest.json的app-plus配置不会被编译进真机运行包。还有一种方式是在HBuilderX里直接“导入项目”选择根目录它会自动读取.dependencies并安装缺失依赖。如果导入后控制台报uni is not defined优先检查package.json里是否把dcloudio/vue-cli-plugin-uni放进了devDependencies放错位置的依赖会导致编译产物里缺少运行时注入。2.4 权限声明与隐私合规iOS/Android manifest.json的差异在manifest.json源码视图中需要显式声明蓝牙权限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.BLUETOOTH_SCAN\ /, uses-permission android:name\android.permission.BLUETOOTH_CONNECT\ / ] }, ios: { privacyDescription: { NSBluetoothAlwaysUsageDescription: 需要使用蓝牙连接设备, NSBluetoothPeripheralUsageDescription: 需要使用蓝牙进行数据交换 } } } }Android 12以上BLUETOOTH_SCAN和BLUETOOTH_CONNECT是运行时权限必须在页面里调用uni.authorize二次申请只写manifest会被拒绝。iOS不同于Android它不会在打开适配器时自动弹权限框如果privacyDescription缺失openBluetoothAdapter会直接进入fail分支。这个差异是“自动开启蓝牙”功能是否能顺利弹出的第一道关卡。如果你的目标是微信小程序权限声明不在manifest.json里而是在项目根目录的app.json中配置permission字段的scope.bluetooth。uni-app编译时会把manifest.json中mp-weixin节点的相关配置映射过去但描述文案需要单独填。3. 自动开启蓝牙生命周期检测、弹窗授权与扫描参数3.1 入口文件中的蓝牙状态检测把蓝牙状态检测放到首页的onLoad里是最常见的做法但要注意区分“检测”和“初始化”两个动作。uni.getBluetoothAdapterState只返回当前状态不会初始化蓝牙适配器而uni.openBluetoothAdapter会主动初始化第一次调用时结果更可靠。所以我推荐用openBluetoothAdapter作为第一步失败分支再判断具体错误码onLoad() { this.initBluetooth() }, methods: { initBluetooth() { uni.openBluetoothAdapter({ success: () { // 初始化成功说明蓝牙适配器已就绪 this.startDiscovery() }, fail: (err) { // 10001 - 未开启蓝牙10002 - 未初始化 this.showOpenDialog(err.errCode) } }) } }如果之前已经初始化过再次调用openBluetoothAdapter不会重复弹权限框这在页面返回重进时很友好。反过来先调getBluetoothAdapterState再调openBluetoothAdapter会形成两次异步空档Android部分机型在关闭蓝牙瞬间会出现返回available为true、但open失败的情况处理起来更麻烦。3.2 弹窗引导开启蓝牙不能绕过用户的开关在“自动开启”的功能命名下最容易出现的产品误解是App直接点亮蓝牙图标。系统层不允许这样做唯一能做的是弹窗后唤起设置showOpenDialog(code) { uni.showModal({ title: 蓝牙未开启, content: 使用该功能需要打开蓝牙是否前往系统设置开启, confirmText: 去开启, cancelText: 暂不, success: (res) { if (res.confirm) { // #ifdef APP-PLUS plus.runtime.openURL(app-settings://) // #endif // #ifdef MP-WEIXIN uni.openSetting() // #endif } } }) }注意app-settings://打开的是App自身的设置页不是蓝牙总开关页面。iOS无法用URL直接定位到蓝牙设置所以弹窗文案要增加一句“请在控制中心打开蓝牙后回到本页面”。微信小程序端用uni.openSetting打开的是小程序自己的授权设置需要在调用前先uni.authorize才会出现蓝牙权限项。3.3 startBluetoothDevicesDiscovery扫描参数与设备过滤扫描阶段要处理好三个参数否则要么扫描结果太多要么重复回调造成列表抖动uni.startBluetoothDevicesDiscovery({ services: [], allowDuplicatesKey: false, interval: 200, powerLevel: high, success: () { uni.onBluetoothDeviceFound((res) { res.devices.forEach((device) { // device.deviceId, device.name, device.RSSI this.deviceList[device.deviceId] device }) }) } })关键参数的作用如下表参数类型作用建议值servicesArray服务UUID过滤只返回匹配的设备不确定时留空数组allowDuplicatesKeyBoolean是否上报重复设备测距设true列表设falseintervalNumber上报间隔毫秒数200-500部分Android机型忽略powerLevelString扫描功耗等级high/first低功耗场景用low这个循环里用this.deviceList[device.deviceId] device做去重插入可以避免同一设备重复渲染。如果你要基于RSSI做蓝牙测距就把allowDuplicatesKey设为true然后在回调里用时间戳过滤至少20ms的样本否则同一设备的信号会高频刷屏。3.4 stopDiscovery与连接蓝牙设备扫描到目标后先调用stopBluetoothDevicesDiscovery再连接这是很多新手容易忽略的时序。直接连接不会报错但在Android上会偶发连接成功后无法发现服务的问题uni.stopBluetoothDevicesDiscovery({ complete: () { uni.createBLEConnection({ deviceId: targetDeviceId, timeout: 10000, success: () { this.startServiceDiscover() }, fail: (err) { // Android上失败后先closeBluetoothAdapter再重新open uni.closeBluetoothAdapter({ complete: () this.initBluetooth() }) } }) } })连接的对象默认是低功耗蓝牙BLE设备。如果你的硬件是HC05这种经典蓝牙串口模块需要在项目里另外集成经典蓝牙能力不能直接复用createBLEConnection。HC05的数据交换走SPP协议而createBLEConnection面向GATT协议层两者在服务发现阶段就会分道扬镳。4. 连接后数据交换服务发现、特征值读写与断线重连4.1 拿到deviceId后如何找到可用的serviceIdBLE设备的数据模型是设备、服务、特征值三层连接成功后要先用getBLEDeviceServices枚举getBLEDeviceServices(deviceId) { uni.getBLEDeviceServices({ deviceId, success: (res) { res.services.forEach((service) { // 大部分健康设备使用标准服务UUID const uuid service.uuid.toUpperCase() if (uuid.includes(180D) || uuid.includes(FFF0)) { this.currentServiceId service.uuid this.getCharacteristics(deviceId, service.uuid) } }) } }) }180D是心率服务FFF0是很多国产低功耗模块的自定义服务。前者是蓝牙SIG标准定义后者是厂商私有定义。拿到一份Demo源码后第一步应该是去设备说明书里找服务UUID不要直接用示例里的UUID。还有一个细节服务和特征值UUID要么全小写要么全大写iOS回传格式与Android可能不同toUpperCase()统一处理能避免一次隐性bug。4.2 特征值读写writeBLEValue与readBLEValue参数找到特征值后读写之前要确认特征值是否具备对应属性。常见错误是拿到一个只读特征值去调用写入接口返回10004。getCharacteristics(deviceId, serviceId) { uni.getBLEDeviceCharacteristics({ deviceId, serviceId, success: (res) { const writeChar res.characteristics.find(c c.properties.write) const readChar res.characteristics.find(c c.properties.read) if (writeChar) { const cmd this.hexStringToArrayBuffer(AA 01 02 BB) uni.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId: writeChar.uuid, value: cmd, success: () console.log(指令下发成功) }) } } }) }, hexStringToArrayBuffer(hexString) { const cleanHex hexString.replace(/ /g, ) const buffer new ArrayBuffer(cleanHex.length / 2) const view new DataView(buffer) for (let i 0; i cleanHex.length; i 2) { view.setUint8(i / 2, parseInt(cleanHex.substr(i, 2), 16)) } return buffer }写入失败时优先检查value类型writeBLECharacteristicValue的value必须是ArrayBuffer直接传字符串或数组都会报参数错误。读取时readBLECharacteristicValue也有同样要求它在success回调里返回valueAndroid上同一时刻只能有一个read请求连续read多条会互相覆盖因此需要做一个串行队列。这是Demo代码里最容易暴露稳定性问题的地方。4.3 通过notify接收设备主动上报大多数蓝牙硬件不会被动等待App去read而是通过notify主动推数据。开启通知的代码如下const notifyChar res.characteristics.find(c c.properties.notify) if (notifyChar) { uni.notifyBLECharacteristicValueChange({ deviceId, serviceId, characteristicId: notifyChar.uuid, state: true, success: () { uni.onBLECharacteristicValueChange((res) { // 回调里解析数据 const dataView new DataView(res.value) const heartRate dataView.getUint8(1) this.currentHeartRate heartRate }) } }) }onBLECharacteristicValueChange是全局监听如果页面里同时存在多个蓝牙连接所有设备的数据都会进入同一个回调。常见做法是在回调开头加if (res.deviceId ! this.targetDeviceId) return把数据分流到当前页面。还要注意有些设备用indicate替代notify二者开启接口相同但indicate会要求App回复确认数据频率更高时最好在特征值查找阶段同时判断properties.indicate。4.4 异常断开与自动重连策略在真实使用中最影响体验的是设备断线后页面还留在“已连接”状态。官方会在连接状态变化时回调onBLEConnectionStateChange它正是做自动重连的依据uni.onBLEConnectionStateChange((res) { if (!res.connected) { this.reconnectTimes if (this.reconnectTimes 3) { setTimeout(() { uni.createBLEConnection({ deviceId: res.deviceId, success: () { this.reconnectTimes 0 this.setupServiceAndNotify() } }) }, 2000) } else { this.reconnectTimes 0 this.showDisconnectModal() } } })重连失败后的处理可以按错误码分段错误码场景处理方式10001适配器未打开重新openBluetoothAdapter10012连接已断开延迟2s后createBLEConnection10013操作超时增大timeout到15s10004特征值属性不支持检查properties标志Android机型的蓝牙栈更容易被破坏连续多次连接失败后必须closeBluetoothAdapter再重新初始化否则后续连接都会在openBluetoothAdapter阶段失败。把这个机制写进重连循环比无脑重试要高效得多。5. 验收清单从Demo到真机的排错记录5.1 真机调试时需要盯住的五个关键点复制这个Demo到自己项目后最常见的现象是“iOS能跑Android扫描不到设备”。原因多半不在业务代码而在权限声明和系统清理策略。先检查Android 12以上是否申请了BLUETOOTH_SCAN与BLUETOOTH_CONNECT再看定位权限是否打开。部分国产机型在蓝牙扫描时同时要求“附近设备”权限没有这个权限扫描列表会一直为空。5.2 实用排错命令和验证技巧在HBuilderX里没有adb logcat窗口的话用命令行抓取蓝牙相关日志adb logcat -s BluetoothAdapter:* BluetoothGatt:* D-BT:* | grep -i error这条命令能同时看到系统蓝牙协议栈到GATT层的关键错误。只要日志里出现10001就说明蓝牙适配器没在openBluetoothAdapter阶段成功出现10013大概率是设备响应慢先调大timeout参数而不是缩短扫描间隔。真机运行前要手动把系统蓝牙关闭再打开一次让App在冷启动状态下走完整的初始化流程。不要在一次测试里连续反复开关蓝牙那样容易让系统蓝牙服务进入异常状态误判成代码问题。打包验证时重点关注manifest.json中的appid是否与你申请的平台包名一致Android的包名不一致会导致权限配置被系统忽略但这个错误在真机运行模式下不会暴露。蓝牙设备字段里的deviceId在iOS上是系统生成的UUID在Android上是格式化MAC地址同一台设备在两端会得到不同值做缓存key时最好在前面拼上平台前缀否则双端会互相覆盖。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价