简介一套基于UniApp与Vue2框架、对接中国移动OneNet平台的物联网跨端应用工程面向从事移动端开发或物联网应用实践、希望用一套代码覆盖Android、iOS及小程序的开发者。压缩包共103个文件、约48.34MB主体包括前端页面与业务逻辑vue/js/scss/json、图标与界面素材png/svg/jpg、H5构建环境html/css以及4个可直接安装的apk同时附带证书及下载地址配置文件目录结构便于按功能模块检索和二次修改。目前已有452人学习。借助该资源可以完整拆解“一次编写、多端发布”的工程组织方式理解Vue2组件化开发在跨端场景中的落地也能掌握OneNet设备接入、数据上报、消息推送等API的调用流程并可直接对照apk运行效果进行验证适合作为课程设计、毕业设计或物联网入门项目的参考资料。 做物联网App最怕什么不是硬件端连不上也不是功能做不出来而是折腾了半天发现自己选的工具链把跨端、消息推送、设备状态同步这些事全搞得特别别扭。上个月我刚用uniappvue2OneNET组合做完一套设备控制与数据监测App从HBuilderX初始化工程到MQTT长连接调通再到打包上架安卓市场整条链路算是完整趟了一遍。标题里这三个词看起来简单实际上每个环节都有不少隐藏坑值得单独拿出来讲讲。先交代一下这个项目的大致背景硬件端用的是ESP8266系列模块通过Wi-Fi接入OneNET云平台上行定时上报温湿度、开关状态等数据App端需要同时支持Android和iOS界面要包含设备列表、实时数据面板、远程控制按钮这几个核心模块。整体架构选型就是标题里这三个词其中OneNET主要承担设备接入和数据转发uniapp负责跨端UI和业务逻辑v2版的Vue语法配合nvue页面处理长列表和实时刷新。下面直接按项目推进的先后顺序把从零搭到能跑的完整过程拆开讲。1. 为什么偏偏是uniappvue2OneNET这个组合先说技术选型。很多人一上来就问“为什么不用原生开发”“为什么不用Vue3”这类问题在真实项目里答案往往很现实团队现有的技术栈就是vue2没有精力同时维护两套原生代码而且项目周期不允许把时间耗在Futter的学习曲线上。uniapp在这类场景下的优势是能一套代码编译到App、H5和小程序HBuilderX里的“运行到手机或模拟器”调试效率很高尤其是真机同步调试时改代码即时生效比传统的原生编译等待体验好太多。OneNET则是中国移动旗下的物联网云平台选它的理由有几个一是免费版就能覆盖中小项目需求设备接入数量和数据存储额度对个人开发者足够友好二是MQTT接入文档写得比较细致社区里能搜到的踩坑案例也多三是平台自带数据流模板和触发器可以在云端做简单的告警逻辑减少App端轮询压力。至于为什么不用EMQX这类自建Broker答案也简单——项目要的是快速上线而不是运维自由云平台在设备管理、数据留存、权限控制上已经帮我们做好了一层封装节省的成本非常可观。注意这里说的“vue2”在uniapp里指的是HBuilderX创建项目时选择Vue 2版本。如果你打开HBuilderX看到的默认模板已经是Vue 3不要慌新建项目时在模板下拉框里可以切到Vue 2版本。很多老项目、旧插件、旧教程都是基于vue2的新手跟教程做的时候尽量保持一致。这条技术链还有一个隐形优势uniapp的uniCloud虽然也能做后端但当你已经有OneNET这种物联网平台做数据中枢时App端不需要再单独搭一个业务后端。App通过MQTT直接订阅设备主题控制指令通过发布消息下行设备状态通过订阅消息上行服务器那一层直接被平台替代了。整个数据链路清爽很多。2. 开发环境与工程初始化HBuilderX里的一次性配置2.1 HBuilderX版本与项目模板选择环境搭建这一步本身不难但有几个细节不注意后面会连续踩坑。我用的HBuilderX是当前最新稳定版创建项目时选择“uni-app”模板框架选Vue 2。项目名建议全小写字母加数字不要用中文和驼峰否则后面打包Android时的包名和路径处理会多出不少麻烦。创建完项目后第一件事是打开manifest.json把基础配置里的AppID换成自己的。如果你没有DCloud账号先去注册一个——这一步省不掉因为无论是自定义调试基座还是云打包都需要有效的AppID。基础配置里的应用名称、版本号这些按实际填就行但版本号格式建议保持三位数字比如1.0.0后面上架应用市场时很多商店对版本号格式有要求。2.2 引入mqtt.js依赖的方式与版本锁定的重要性物联网App的核心依赖就是MQTT客户端库。uniapp的H5端可以直接通过npm安装mqtt包但App端和微信小程序端不走npm这么简单。我在这个项目里的做法是从npm包中把mqtt.min.js提取出来放到项目的common/js/目录下然后在需要的页面里通过require引入。这里有一个非常关键的版本问题mqtt.js在4.x版本以后对微信小程序和App端的适配方式有变化某些版本在App端会直接报navigator is not defined的错。我这边的经验是锁定在4.2.6版本稳定性和兼容性都最好。如果你在项目里发现mqtt连接一直异常但代码逻辑看起来又没问题大概率是版本兼容性导致的直接换版本试试。引入方式可以参考下面这样// 页面中引入mqtt库 import mqtt from /common/js/mqtt.min.js然后就是封装MQTT连接工具。不要在每个页面都写一遍连接逻辑而是单独建一个mqttService.js统一管理连接实例、订阅、发布、断线重连和消息分发。这个封装的好处后面会越来越明显当你有多个页面需要同时监听设备状态时只需要在工具类里维护一个主题回调池避免重复创建连接导致OneNET服务端频繁断开旧连接报错。3. OneNET平台侧配置产品、设备与数据流模板3.1 创建产品时的协议选择与设备注册登录OneNET控制台第一步是创建产品。这里我吃的第一个亏是产品协议选错了。OneNET平台支持多种协议MQTT、HTTP、TCP透传、Modbus等一定要选MQTT因为后面App端的长连接、消息主动推送、设备状态实时感知都依赖MQTT的能力。如果你选了HTTP虽然也能上报数据但设备下行控制和状态推送会变得极其别扭。创建完产品后在产品详情页可以看到产品ID这个ID后面请求token时会用到。接着添加设备设备名称可以自定义设备ID是平台自动生成的。这里要记下三样关键信息产品IDproductId设备IDdeviceIdAPIKey或产品AccessKey用于生成连接凭证OneNET的MQTT接入支持一机一密和一型一密两种方式。个人项目建议直接用设备级别的APIKey简单直接如果是量产设备再考虑一型一密的产品级接入方式。3.2 数据流模板的作用与设备上报数据的映射关系OneNET要求设备端按数据流模板的格式上报数据这个模板可以在控制台手动定义也可以让设备端动态创建数据流。我的做法是在控制台提前定义好temperature、humidity、switch_status这三个数据流分别表示温度、湿度、开关状态。设备端上报数据时用JSON格式组装数据点{ temperature: 26.5, humidity: 60.2, switch_status: 1 }设备端上报后OneNET会自动把最新值存到对应的数据流里。App端通过API查询历史数据或者通过MQTT订阅实时数据都可以拿到这些值。这个数据流模板设计实际上等于帮我们定好了设备与App之间的“通信协议字段”前期把字段定规范后面写解析代码时能省不少事。4. App与OneNET的MQTT对接从token计算到消息收发4.1 MQTT连接参数与token签名规则这一节是整个项目的核心也是最容易出问题的地方。OneNET的MQTT接入地址是mqtts.heclouds.com端口8883TLS加密。但问题来了uniapp打包出来的App在部分Android机型上对TLS端口支持不太稳定我实际调试时用的是1883端口connectTimeout设得长一点。生产环境如果对安全性要求高还是建议用8883只是要在更多真机上做兼容测试。连接MQTT时username不是你的OneNET账号也不是设备ID而是由产品ID和设备ID组合出来的字符串。具体规则是username产品IDclientId设备IDpasswordtoken由产品或设备级APIKey通过签名算法生成token的计算方式比较固定规则是把产品ID、设备ID、APIKey、时间戳等参数按特定顺序拼接用HMAC-SHA1做哈希结果转成十六进制字符串。我贴一下实际项目里的计算代码function getToken(productId, deviceId, apiKey) { const timestamp parseInt(Date.now() / 1000) 3600 const version 2018-10-31 const res products/ productId /devices/ deviceId const et timestamp.toString() const method sha1 const sortParams { et: et, method: method, res: res, timestamp: timestamp, version: version } const keys Object.keys(sortParams).sort() let signStr for (let i 0; i keys.length; i) { signStr keys[i] sortParams[keys[i]] } signStr signStr.substring(0, signStr.length - 1) // 使用crypto-js计算HMAC-SHA1 const CryptoJS require(/common/js/crypto-js.js) const hash CryptoJS.HmacSHA1(signStr, apiKey) return CryptoJS.enc.Base64.stringify(hash) }这段代码建议直接复制保留到项目里因为每次连接MQTT都要重新生成token且token有过期时间。你可以在封装的mqttService里做一个“提前5分钟刷新token”的机制避免设备在长连接中途因为token过期被平台踢下线。4.2 订阅主题与发布指令的格式细节OneNET MQTT的主题格式和标准MQTT的topic有区别不是自己随便定的。设备上行数据订阅和下行命令使用的主题格式如下设备上报数据topic使用$sys/{pid}/{device-id}/dp/post/jsonApp端不需要订阅这个因为平台会自动把数据流转发到数据流里。App订阅设备实时数据实际是订阅平台预先定义好的$sys/{pid}/{device-id}/dp/post/json/accepted或直接查询API获取最新数据点。简单场景下我更推荐App查询API获取数据而不是订阅消息因为订阅消息要处理各种状态码和重连逻辑。下行控制指令App向设备端发控制指令时发布到$sys/{pid}/{device-id}/dp/post/json但是payload格式要按OneNET规定的格式来{ temperature: { value: 26.5 }, switch_status: { value: 0 } }这个格式很容易踩坑。很多人一开始按标准MQTT习惯直接发布{switch_status: 0}这种扁平JSON结果设备端能收到消息但平台不识别数据流里的值也不会更新。OneNET的要求是每个数据点都要包一层{ value: 实际值 }的结构尤其是控制类字段漏掉这层包装整个命令就废了。4.3 连接状态监听与断线重连策略物联网App最怕的不是连接不上而是连上了不知道什么时候断了然后用户看着界面上“设备在线”实际上设备已经掉线半天了。所以我在mqttService里专门做了一个连接状态监听和心跳检查机制。mqtt.js本身支持reconnectPeriod参数但我发现单纯依赖这个参数在OneNET平台上有问题——长时间断线后重连时如果token过期会陷入反复连接失败的循环。我处理的办法是监听close和error事件在断线后先判断当前token是否过期如果过期就先重新生成token再重连如果没过期则按递增间隔重试最多重试5次超过后停止并提示用户检查网络。心跳机制方面OneNET的MQTT broker默认60秒左右会断开没有心跳的连接。我在连接参数里设置了keepalive: 30同时在业务层额外加了一个应用层心跳——每20秒发布一条空消息到设备的$sys/{pid}/{device-id}/dp/post/json确保连接长期存活。这个双保险在弱网环境下特别有用实测能明显减少“假在线”的情况。5. 设备列表与实时数据页面的实现思路5.1 页面结构划分与实时刷新方案的取舍App端我分了三个主页面设备列表页、设备详情/控制页、数据历史记录页。设备列表页采用下拉刷新加触底加载的方式数据来源是OneNET的HTTP API——查看设备列表接口返回设备的基本信息、在线状态、最近上报时间。这里实时刷新方案我纠结过用轮询还是用MQTT订阅。最终选择是列表页用“初次加载下拉刷新定时轮询”轮询间隔设30秒详情页用MQTT订阅实时数据实现秒级刷新。原因很简单列表页如果也用MQTT订阅要同时订阅多个设备的主题移动端网络状态不稳定时连接管理成本很高而详情页只面对一个设备订阅单个主题的压力小得多实时体验也更好。这个折中方案实践证明非常靠谱既保证了关键页面的实时性又不至于让网络请求和长连接过于频繁。5.2 下拉刷新与页面滚动冲突的处理这个项目的页面里有数据图表图表区域需要支持触摸缩放和滚动。这里遇到一个经典问题在uniapp里页面滚动和图表Canvas触摸手势经常互相干扰。特别是安卓端手指上下滑动时经常同时触发页面下拉刷新和图表内部滚动体验非常差。我最后的方案是图表容器使用touchstart和touchmove事件拦截在图表区域内阻止事件冒泡并通过touchmove.stop.prevent禁止页面跟随滚动同时用scroll-view包裹图表将图表滚动操作限制在scroll-view内部。数据面板区域则使用页面的原生滚动。这个写法如果直接看uniapp文档不一定能快速找到但实测能解决大部分冲突问题。另外用renderjs实现canvas图表时还要注意图表数据更新时给renderjs传值的方式避免每次刷新都重建整个图表实例。5.3 历史数据曲线展示的选型与坑历史数据我用的是OneNET的API查询数据点返回JSON数组然后把数据传给页面里的renderjs绘图。图表库方面我在canvas上直接绘制折线图没引大型图表库因为uniapp App端跑复杂图表库容易卡顿而且渲染层逻辑和逻辑层通信频繁会导致数据延迟。自己写一个简易折线图大概不到两百行代码但性能好得多。这里重点要说的是renderjs的坑。renderjs在真机上对Canvas的支持和模拟器上完全不一样模拟器上正常显示的图表真机上可能会白屏、模糊、甚至数据不刷新。经验是在页面onShow时必须重置一下renderjs里的数据源否则切后台再回来时图表数据还是上一次的快照看起来就像“卡死”了一样。另外canvas宽度要按设备dpr做适配不然高分屏上图表边缘是糊的。6. 打包发布从本地自定义基座到安卓应用市场上架6.1 自定义调试基座与云打包的差异开发阶段直接运行标准基座可以快速调试但如果你在App端用了第三方SDK或者原生插件比如高德地图定位、推送标准基座里没有这些模块运行会直接报错。这时候就需要打包自定义调试基座。HBuilderX里的步骤是菜单栏“运行” - “运行到手机或模拟器” - “制作自定义调试基座”制作完成后在运行下拉菜单里勾选“使用自定义调试基座”。云打包相对简单在HBuilderX里选择“发行” - “原生App-云打包”选择Android包名、证书、渠道等。这里的关键是Android证书。很多人第一次打包时随便生成一个证书后面上架应用市场时需要证书指纹信息再来回折腾。建议项目一开始就用命令行keytool生成正式证书并妥善保存好keystore文件和密码后面每次打包都用同一个证书避免应用覆盖安装时签名不一致导致安装失败。6.2 安卓应用市场上架需要的材料与常见驳回原因国内安卓应用市场上架基本上都需要软著软件著作权登记证书、隐私政策、ICP备案域名等材料。如果你想尽快上架试用可以先从酷安、应用宝这类审核相对没那么严格的市场开始但主流市场迟早要补齐材料。审核被驳回最多的原因就是隐私政策第三方SDK目录没写全uniapp打包出来的应用一般会包含DCloud的推送、统计SDK需要在隐私政策里明确声明相关权限和SDK用途。另外还遇到过一个问题App在部分市场审核时被要求“不允许在用户未同意隐私政策前收集设备信息”。uniapp的manifest里默认会勾选一些统计模块如果配置不细致App第一次启动时后台就会上报设备信息。解决方式是把manifest.json里暂时用不到的模块都去掉提交审核时关闭统计分析功能等通过后再开启。这个细节我在上架两个市场时都踩到了写出来提醒一下。6.3 上架后版本升级与崩溃监控的补充如果你准备长期迭代建议版本发布后接入uni统计或者第三方崩溃监控这样线上出问题能快速定位。uniapp项目的js错误可以通过uni.onError全局监听上报到自己的日志服务但原生层的崩溃信息是普通js捕获不到的必须有原生SDK支持。对于纯js逻辑为主的中小型项目前期不上崩溃监控问题不大但至少把全球唯一定位error的日志接口留好后面出问题有抓手。7. 实际项目里最容易忽略的三类隐患这一节聊聊我实际调试过程中遇到的、教科书里基本不会写的问题。第一类是Android设备兼容性差异。同一个uniapp项目在不同品牌手机上的表现可能完全不一样。比如我在一台老旧的华为EMUI设备上测试MQTT长连接发现连接十分钟后必断后来查了日志发现是系统对网络请求进行了省电优化休眠时切断了长连接。解决办法是在App内引导用户开启“允许后台活动”权限或者在前台Service中保活。另一种做法是降低keepalive时间让系统误判为高频活跃应用但这个方案治标不治本。第二类是uniapp中的setInterval在App端和H5端的差异。H5端setInterval切后台再回来可能因为页面被销毁导致定时器失效App端则相对稳定但要注意页面onHide时清理定时器否则多个页面叠加的定时器会互相干扰。我在MQTT工具类里用了一个统一的心跳定时器只有MQTT连接状态为connected时心跳才执行避免无谓的CPU消耗。第三类是OneNET数据流字段的更新延迟。OneNET平台的数据流在设备上报后并不是立刻对API查询可见的有一定的延迟通常几十秒到几分钟不等。如果你在设备详情页通过API查询最新数据点会发现和MQTT实时推送的数据有时间差。这个现象在官方文档里描述得不是很明显但对实时性要求高的项目影响很大。我的处理方式是详情页优先展示MQTT推送的最新值MQTT未连上时才退化为API查询兜底保证用户看到的数据永远是实时值。最后再分享一个小技巧。如果你打算以后在项目里同时支持多个厂商的物联网云平台建议在mqttService层再抽象一层接口把OneNET特有的token生成、topic拼接逻辑都隔离在一个Promise函数里页面层只调用connect(deviceId)、subscribe(deviceId)、publish(deviceId, payload)三个方法。这样哪天要切到其他平台只需要重写底层适配部分页面代码一行都不用动。我在这个项目里就是这样设计的后期给另一个项目接入备用通道时省了不少事。本文还有配套的精品资源点击获取