简介:面向物联网应用开发的完整实战示例,基于uniapp+Vue2框架接入中国移动OneNet平台,帮助开发者解决跨平台App与设备云端通信的关键难题,适用于课程设计、毕业设计或自学入门。资源包共103个文件,压缩后48.34MB,内容涵盖Vue页面与业务逻辑的js、vue源码,用于界面展示的png、svg素材,可安装的apk以及json、scss等配置文件,覆盖从界面搭建到设备数据互通的完整链路,便于直接运行、对照学习与二次开发。已有452人学习。除了可安装的安卓包,还附有前端工程代码、证书数据及辅助文件,可对照代码逐步理解设备接入流程、实时数据上报与拉取、消息推送、状态管理、异常处理和通信安全性等物联网应用中的常见环节,完整展现了uniapp跨端开发与OneNet平台融合时的项目组织方式,适合刚入门uniapp或打算将业务快速接入OneNet的开发者下载参考。 最近在给一套自制的温湿度传感器做配套App,硬件端已经用OneNET上报数据跑了好几天,结果卡在了移动端这步。网上搜“uniapp+vue2+OneNET”,要么是只讲能连上的Demo,要么是后台截图配一句“看我做出来了”,真正把MQTT连接细节、token计算、跨端打包的坑讲透的文章太少。这篇就把我从HBuilderX建项目到App上架前的完整流程掰开讲一遍,适合正在用OneNET做物联网项目、想用uniapp快速出App或者小程序端的人参考。
1. 为什么这个组合能省掉一半工作量
先说结论:这套组合不是技术最前沿的,但它在物联网App这个场景里,几乎是投入产出比最高的选择。我自己对比过几个方案,如果你也卡在“硬件能上云,手机端没人写”的阶段,看完这章就能少走弯路。
1.1 硬件端已经上车OneNET,移动端就别换道了
设备数据已经通过OneNET往上传了,移动端继续选OneNET是最顺的路。OneNET是中国移动的物联网开放平台,支持MQTT、HTTP、CoAP多种设备接入协议,设备端SDK很成熟,论坛和文档资料也攒了不少年。移动端要干的事其实就是“以合法身份订阅设备数据”,OneNET的MQTT接口把这件事封得很干净,你只需要按规则生成token、组topic,剩下的长连接、心跳、消息推拉都由MQTT协议本身解决。
反过来想,移动端如果选了和OneNET没有直接打通的第三方云,就得自己写设备数据转发层,或者用HTTP API轮询。数据量小的时候看不出问题,一旦设备数量上来,轮询延迟、请求频率限制、服务器成本全来了。所以只要硬件端定了OneNET,移动端就没有换平台的必要。
1.2 uniapp+vue2的边界在哪里
uniapp的卖点是一套代码编译到iOS、Android、微信小程序、H5。物联网App恰恰最需要这种跨端能力:你自己调试用App,给客户演示时可能得用小程序,后台管理页面顺手做成H5,如果每个端都单独写一套,维护成本直接翻倍。
vue2在2023年之后看着确实旧了,但这个项目选vue2有明确的现实原因:团队里最熟的版本就是vue2,项目用到的组件库、日期插件、图表库基本都是基于vue2的,改成vue3意味着这些生态要重新验证。OneNET接入本身不挑框架,Vue2在这条链路里足够稳定。不是vue3不好,而是“技术选型最贵的是切换成本”,你把MQTT长连接这种核心逻辑封装好之后,框架差异的影响真的不大。
2. OneNET设备接入参数:动手之前先理清这三样
接入OneNET的第一步不是写代码,是去控制台把产品、设备、数据流这三样概念理清楚,然后把参数拿到手。我见过不少人在这一步把ProductID和DeviceName搞混,后面调一整天“为什么连不上”。
2.1 产品ID、设备名、设备密钥:哪个对应哪个
在OneNET控制台创建产品后,你会得到一个ProductID,这是产品级别的唯一标识,相当于你App的“应用ID”。产品下面再创建设备,每个设备有DeviceName和DeviceSecret。DeviceName在产品内唯一,DeviceSecret是设备的“密码”,后面生成MQTT签名令牌时要用它做HMAC-SHA1的密钥。
创建产品的时候,OneNET会让你选接入协议和数据加密方式,项目里选MQTT协议、明文传输就行。设备名称建议用有意义且不含中文的名字,比如dev_temp_001。原因很简单:MQTT的clientId用的是设备名称,某些物联网设备TCP栈对中文clientId处理并不友好,直接避开能省很多通信层的奇奇怪怪问题。
提示:同一个产品下可以建多个设备。你做了几十个同型号硬件,不需要重复建产品,每个硬件对应一个设备即可,ProductID相同,DeviceName和DeviceSecret不同。
2.2 数据流和topic的对应关系
OneNET的MQTT接入里,topic规则第一眼有点绕,但记住一条主线就够了:设备上报数据走“dp/post”相关主题,平台下发命令走“cmd/request”相关主题。项目里的温度、湿度这类数据,在OneNET里叫“数据流”,你在控制台创建数据流时可以只定义名称,也可以指定数据类型。
设备上报数据之后,平台会通过$sys/{productId}/{deviceName}/dp/post/json/accepted返回成功状态,通过rejected返回失败原因。调试的时候一定要把这两个响应主题都订阅上,数据没上去到底是因为token过期、JSON格式不对还是数据流不存在,rejected报文的payload里都写得很清楚。
3. MQTT连接的核心代码:token签名和报文细节
这一节是整个项目里最容易翻车的地方。OneNET的MQTT接入不是简单把用户名密码填进去,它的password字段要放一个用设备密钥生成的签名令牌,这个令牌有时效性。很多新手第一次连接失败,不是网络不通,就是令牌字符串没拼对。
3.1 签名算法:res、et、sign分别解决什么问题
OneNET采用的是签名版token鉴权,生成规则不复杂,但每个字段的用途得清楚:
res:访问资源标识,格式是products/{产品ID}/devices/{设备名称},告诉OneNET你想访问哪个设备。et:过期时间,Unix时间戳。一般取当前时间加上86400秒,过期后需要重新生成。sign:用HMAC-SHA1算法对res + "\n" + et做签名,密钥就是设备的DeviceSecret。version:固定值2018-10-31,代表签名协议版本。
最后拼成一个查询字符串,放在MQTT的password字段里。代码实现如下:
// utils/oneNetToken.js import CryptoJS from 'crypto-js' export function generateToken(productId, deviceName, deviceSecret, expire = 86400) { const res = `products/${productId}/devices/${deviceName}` const et = Math.round(Date.now() / 1000) + expire const str = `${res}\n${et}` const sign = CryptoJS.HmacSHA1(str, deviceSecret).toString(CryptoJS.enc.Base64) return `version=2018-10-31&res=${encodeURIComponent(res)}&et=${et}&method=sha1&sign=${encodeURIComponent(sign)}` }这个token建议每次连接前都重新生成一次,不要复用旧值。我在实际开发中踩过坑:把token写死在本地配置里,第二天打开App连接一直失败,折腾半天才反应过来是et过期了。另外,如果你硬件端也用OneNET接入,可以复用同一套签名逻辑,只是换一门语言实现而已。
3.2 mqtt.js连接参数怎么填
uniapp里引入mqtt.js很直接,先装依赖:
npm install mqtt --save然后写一个连接模块。注意:App端用的是ws://或wss://协议头,微信小程序端要用wxs://协议头,用错的话连接会一直停在connecting状态,不报错也不成功,非常迷惑。
// utils/mqtt.js import mqtt from 'mqtt' import { generateToken } from './oneNetToken' let client = null export function connectOneNet({ productId, deviceName, deviceSecret }) { const token = generateToken(productId, deviceName, deviceSecret) const url = 'wss://183.230.40.39:8084/mqtt' // 以OneNET控制台实际接入点为准 client = mqtt.connect(url, { clientId: deviceName, username: productId, password: token, clean: false, reconnectPeriod: 5000, connectTimeout: 10000, protocolVersion: 4 }) client.on('connect', () => { console.log('MQTT连接成功,收到CONNACK') }) client.on('reconnect', () => { console.log('正在断线重连') }) client.on('error', (err) => { console.log('MQTT错误', err) }) return client }clean: false这个参数值得说一句。clean session设为false,表示断开重连后保留服务端的会话状态,这样订阅关系不会丢。如果你的App会频繁切后台、切网络,建议保持false,能减少重连后的重新订阅操作。
注意:clientId填DeviceName,username填ProductID,password填签名token。这三个字段填错任何一个,OneNET都会在握手阶段直接断开连接,控制台可能只看到一次“连接异常”,根本定位不到具体是哪个字段错了。
3.3 订阅/发布topic的报文长什么样
连接建立后,订阅方向和发布方向各有一组核心topic。我整理成表格方便对照:
| 方向 | 主题格式 | 说明 |
|---|---|---|
| 设备上报数据 | $sys/{pid}/{deviceName}/dp/post/json | 向平台发布数据点JSON |
| 平台响应 | $sys/{pid}/{deviceName}/dp/post/json/accepted | 上报成功后平台的回执 |
| 平台响应 | $sys/{pid}/{deviceName}/dp/post/json/rejected | 上报失败,payload里带原因 |
| 命令下发 | $sys/{pid}/{deviceName}/cmd/request/{cmdId} | 订阅后接收平台下发的指令 |
| 命令应答 | $sys/{pid}/{deviceName}/cmd/response/{cmdId} | 设备执行完指令后回复结果 |
订阅代码:
const pid = '你的产品ID' const deviceName = '你的设备名称' client.subscribe(`$sys/${pid}/${deviceName}/dp/post/json/accepted`) client.subscribe(`$sys/${pid}/${deviceName}/dp/post/json/rejected`) client.subscribe(`$sys/${pid}/${deviceName}/cmd/request/+`) client.on('message', (topic, payload) => { console.log('收到消息主题:', topic) console.log('收到消息内容:', payload.toString()) })发布数据点的payload格式是OneNET特有的:
{ "datastreams": [ { "id": "temperature", "datapoints": [ { "value": 26.5 } ] }, { "id": "humidity", "datapoints": [ { "value": 60 } ] } ] }即使你只上报一个值,datastreams也要用数组包一层,datapoints同样要包数组。这是OneNET的固定格式,少一层或多一层都会收到rejected回执。
4. 页面和数据流:MQTT消息怎么变成UI上的刷新
能连上MQTT只是第一步,App最终要给用户看实时数据、按按钮控制设备。这里的关键设计是不要让每个页面直接操作mqtt实例,而是把MQTT封装成全局模块,通过Vuex统一管理连接状态和消息分发。
4.1 用Vuex管理连接状态和设备数据
我推荐在Vuex里单独建一个mqtt模块,统一维护连接状态、最新设备数据。这样做的好处是:页面退出再回来时,连接状态还在;多个页面共享同一份实时数据,不会各自建连接。代码结构大概长这样:
// store/modules/mqtt.js const state = { status: 'disconnected', // disconnected / connecting / connected deviceData: { temperature: null, humidity: null }, lastMessage: null } const mutations = { SET_STATUS(state, status) { state.status = status }, SET_DEVICE_DATA(state, data) { state.deviceData = data }, SET_LAST_MESSAGE(state, msg) { state.lastMessage = msg } } const actions = { init({ commit }, config) { const client = connectOneNet(config) client.on('connect', () => commit('SET_STATUS', 'connected')) client.on('message', (topic, payload) => { const body = JSON.parse(payload.toString()) if (body.datastreams) { const data = {} body.datastreams.forEach(item => { const last = item.datapoints[item.datapoints.length - 1] data[item.id] = last.value }) commit('SET_DEVICE_DATA', { ...state.deviceData, ...data }) } }) } }页面里只需要用mapState拿到deviceData,配合computed属性,MQTT消息一到就会触发视图更新。比如首页展示温度和湿度卡片,数据变化时卡片上的数字自动刷新,用户感知不到底层是MQTT还是HTTP轮询,体验很顺滑。
4.2 指令下发的完整链路
远程控制本质上就是把用户操作转成一条MQTT消息,发布到命令topic。OneNET收到App下发的指令后,会把指令推给硬件端;硬件端执行完,再把结果通过命令应答topic传回来。
// 指令下发 function sendCommand(productId, deviceName, cmdId, payload) { const topic = `$sys/${productId}/${deviceName}/cmd/request/${cmdId}` client.publish(topic, JSON.stringify(payload), { qos: 0 }) }我在做继电器控制时,就是按钮点击事件里调用这个函数,把{ switch: 'on' }通过cmd/request/relay001发出去。硬件端收到后执行,再往cmd/response/relay001上报执行结果。整个过程链路清晰,用MQTTX桌面工具模拟硬件端时,还能直接看到App下发的指令报文。
这里有一个细节:命令请求topic的{cmdId}是用作标识的,同一个指令多次发送时cmdId要区分开,否则硬件端可能因为消息去重而忽略后续指令。我在实际测试中,就让cmdId带上时间戳,类似relay001_1690000000,能确保每次指令都是独立的消息。
5. 换端就翻车的三个坑:App和小程序的适配细节
如果你用HBuilderX直接“运行到小程序模拟器”,大概率会在连接阶段卡住。这不是代码问题,是跨端环境的差异。我踩过的坑主要集中在三个地方,提前配好能省一晚上。
5.1 小程序webSocket协议和合法域名
微信小程序里不能随便连WebSocket地址,必须在小程序后台配置socket合法域名,而且域名必须是WSS开头,不能用IP直连。OneNET的MQTT如果走TLS,连接的就是wss协议地址。开发调试时,可以在微信开发者工具里勾选“不校验合法域名”,但真机预览和发布后配置没配上,连接还是失败。
另外一个对应关系的细节:小程序端的连接地址要写成wxs://协议头。这个专门给小程序用的协议头,不是拼写错误,是mqtt.js针对小程序的WebSocket实现。App端用wss://,小程序端改用wxs://,同一套其余代码不用动。
5.2 App后台切换后的断线重连
App切到后台超过一定时间,系统会断开WebSocket连接。回到前台时如果不处理,界面就一直停在“已断开”状态。我的做法是在App.vue的onShow生命周期里检查MQTT连接状态,如果status不是connected,就重新走一遍connectOneNet流程。同时把reconnectPeriod设成5000毫秒,配合mqtt.js自带的断线重连机制,基本能做到无感恢复。
实际测试中发现,Android和iOS对后台WebSocket的回收策略不一样,iOS更激进。所以不要只依赖前台的onShow钩子,最好在全局的onHide时主动记下时间戳,onShow时判断离开超过30秒就强制重连,这样能避免“看似连着但实际收不到消息”的假连接状态。
5.3 下拉刷新和页面滚动的冲突处理
这个问题和MQTT没直接关系,但页面有实时数据看板时几乎必踩。热搜里那句“下拉如何触动滚动屏而不触发页面下拉刷新”说的就是它。我的做法是:页面配置里不要全局开启enablePullDownRefresh,改用scroll-view的refresher-enabled属性单独实现下拉刷新,通过@refresherrefresh事件去重新拉一次数据。这样滚动容器自己滚动,不会触发页面级的下拉刷新,体验稳定很多。
6. 打包上架前的配置检查清单
项目开发完,最后一公里是打包。uniapp用HBuilderX云打包算是最省心的路径,但几个配置不提前做,打包后会莫名其妙出问题。
6.1 HBuilderX云打包和证书
App端打包前,在manifest.json里确认App模块勾选了网络、WebSocket能力。Android上架需要准备签名证书,可以用Android Studio生成keystore,也可以直接用HBuilderX的云端证书功能。iOS第一次打测试包时,流程比较绕,建议先用云打包的“测试证书”模式跑通安装包,再换正式证书。
要注意的是云打包时选择的“打包类型”要和你的实际需求匹配:测试包用自定义基座就够了,正式发布才需要生成正式包。离线打包SDK方案如果要用,务必确认离线SDK版本号和HBuilderX版本号一致,否则会出现编译错误或者原生能力失效。
6.2 小程序和H5端的额外配置
如果你同时发布微信小程序版本,记得在小程序管理后台的“开发设置-服务器域名”里,把socket合法域名配成OneNET的wss接入地址。域名配置生效有几分钟延迟,配完别急着重试。H5端则需要注意跨域限制,浏览器环境里跨域WebSocket一般没问题,但如果是生产环境,最好用HTTPS页面,避免混合内容被浏览器拦截。
跑通整个链路后,我的体会是:uniapp+vue2+OneNET这套组合,每个环节都不是最“前沿”的,但胜在文档全、能搜到的坑多、团队上手快。如果你只是要把设备数据搬到手机上看,再加一点远程控制,这套方案已经足够覆盖大部分需求。最后分享一个调试习惯:所有MQTT问题,先在电脑上用MQTTX这样的桌面工具连一次OneNET,确认参数没问题,再回到uniapp里查代码。这一步能帮你把“平台问题”和“代码问题”快速区分开,省下大量时间。
本文还有配套的精品资源,点击获取