十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

微信小程序MQTT客户端实战:WebSocket连接与设备监控

微信小程序MQTT客户端实战:WebSocket连接与设备监控 简介这是一份面向物联网开发者与微信小程序初学者的轻量级MQTT通信实践项目聚焦于在微信小程序环境中实现基于WebSocket的MQTT协议接入解决物联网设备远程监控与低带宽环境下的实时数据传输难题适用于智能家居、工业传感、远程告警等典型场景。压缩包共18个文件40KB包含5个JSON配置文件如app.json、sitemap.json、4个JS核心逻辑文件含MQTT连接与收发封装、3个WXSS样式文件、2个WXML页面结构文件以及README.md、说明文件.txt和附赠资源.docx等辅助文档结构清晰、模块分离明确。已有157人学习下载读者可直接获取完整可运行的小程序MQTT客户端源码涵盖WebSocket连接建立、主题订阅/发布、消息解析与错误重连等关键实现并附有开发注意事项与协议集成要点说明便于快速理解MQTT在小程序端的适配逻辑与工程落地路径。 上个月调试一批温湿度传感器的时候我又被同一个问题卡住了现场没有电脑只有手机微信想看一眼设备实时上报的数据只能拿另一台手机给同事打电话让他在办公室盯着Web端MQTT客户端念数据。回来之后我花了一周时间把常用的Web端MQTT客户端能力搬进了微信小程序做成一个基于微信小程序框架的轻量级MQTT客户端支持WebSocket连接、订阅发布专门用来做物联网设备监控和调试。这里把整个项目的设计思路、关键代码和踩坑过程整理出来给那些想在小程序里跑MQTT的人一个参考。MQTT全称是Message Queuing Telemetry Transport消息队列遥测传输特点就是轻量、省流量、适合资源受限设备。但微信小程序里没有原生TCP Socket所以真正落地时绕不开WebSocket。无论你是做嵌入式设备端还是写小程序前端下面这套方案都能直接拿去改。1. 为什么在小程序里跑MQTT要选WebSocket这条路线1.1 微信小程序没有开放TCP长连接这是最核心的约束。很多做硬件的朋友习惯MQTT走1883端口觉得这才是标准姿势到小程序这边容易卡住小程序网络API里只有wx.requestHTTP、wx.connectSocketWebSocket、wx.uploadFile、wx.downloadFile没有类似Node.js里net.createConnection的能力。也就是说小程序端连TCP这种传输层连接都建不了更别想直接和1883端口对话。有人会想到用wx.createUDPSocket但UDP和MQTT默认的TCP传输不匹配除非服务端专门做UDP封装否则别自找麻烦。结论很直接要在微信小程序里走MQTT唯一现实的通道就是WebSocket。1.2 MQTT over WebSocket如何工作MQTT协议本身并不关心底层是TCP还是WebSocket它只是把控制报文按照固定格式封装。WebSocket可以承载二进制帧刚好能把MQTT报文塞进去。服务端只要启用MQTT over WebSocket监听端口比如EMQX默认的8083ws和8084wss小程序就能用wss://或wxs://开头连接。在mqtt.js里连接地址写法一般是这样const client mqtt.connect(wxs://iot.example.com:8084/mqtt, { clientId: wx_ Math.random().toString(16).slice(2, 12), username: device_001, password: your_token, reconnectPeriod: 0 });注意这里的关键点是wxs://前缀。mqtt.js新版本已经内置了微信小程序适配内部会调用wx.connectSocket不需要你自己再写WebSocket桥接层。如果你拿到旧版mqtt.min.js不支持wxs://连接时会报WebSocket is not defined那就需要手动在全局补一个WebSocket桥接对象或者升级库版本。1.3 和HTTP轮询、裸WebSocket的对比有人说既然都能用WebSocket那我不用MQTT直接用微信wx.connectSocket写一套自定义协议行不行行但项目后续会很痛苦。我做过对比维度MQTT over WebSocketHTTP轮询裸WebSocket自定义协议连接开销长连接一次握手每次请求都握手长连接一次握手协议语义主题订阅/发布自带QoS无主题需自己设计需全部自己定义离线消息会话可以保留不适用自己实现设备端配套几乎所有物联网平台都支持无标准无标准开发成本低有成熟客户端库低但实时性差高结论是小程序端要想做设备监控和数据传输MQTT over WebSocket是性价比最高的方案。主题订阅发布天生适合一对多设备QoS能处理弱网丢包物联网平台也基本都支持WebSocket接入。2. 项目结构设计与MQTT客户端封装2.1 最小目录与依赖引入我的项目目录没有搞太复杂核心是这样的pages/ index/ index.js index.wxml index.wxss utils/ mqtt.min.js config/ mqtt.jsMQTT库我直接下载的mqtt.js的dist压缩版本放到utils目录然后通过require引入。这里有一个经验不要直接用npm把mqtt.js塞进小程序因为mqtt.js在npm环境下会依赖Node.js的stream、Buffer等模块小程序构建npm不一定处理得好。直接引入现成的mqtt.min.js反而最省事前提是版本支持wx/wxs协议。如果项目用了分包注意MQTT.js如果放在分包里启动主包页面时可能访问不到需要用分包异步化方式加载否则会报找不到模块。2.2 封装一个可复用的MqttClient类我不建议每个页面都直接操作mqtt.js的client实例那样页面写多了会乱。封装一个简单的类统一管理连接、订阅、发布、断开。下面是我在项目里用的精简版class MqttClient { constructor(options) { this.options options; this.client null; this.isConnected false; this.subscriptions []; this.messageHandlers []; } connect() { if (this.client) return; this.client mqtt.connect(this.options.url, { clientId: this.options.clientId, username: this.options.username, password: this.options.password, reconnectPeriod: 0, connectTimeout: 8000 }); this.client.on(connect, () { this.isConnected true; this.resubscribeAll(); this.emitStatus(true); }); this.client.on(reconnect, () { this.emitStatus(reconnecting); }); this.client.on(close, () { this.isConnected false; this.emitStatus(false); }); this.client.on(error, (err) { this.emitError(err); }); this.client.on(message, (topic, payload) { this.messageHandlers.forEach((handler) { handler(topic, payload); }); }); } subscribe(topic, qos 0) { if (!this.client || !this.isConnected) { this.subscriptions.push({ topic, qos }); return; } this.client.subscribe(topic, { qos }, (err) { if (!err) { this.subscriptions.push({ topic, qos }); } }); } publish(topic, message, qos 0) { if (!this.client || !this.isConnected) return false; this.client.publish(topic, message, { qos }); return true; } end() { if (this.client) { this.client.end(true); this.client null; } this.isConnected false; } resubscribeAll() { if (!this.client) return; this.subscriptions.forEach((item) { this.client.subscribe(item.topic, { qos: item.qos }); }); } } module.exports MqttClient;这个类最核心的价值是维护了subscriptions列表。MQTT客户端重连成功后服务端不会自动恢复之前的订阅必须重新订阅所以我统一在connect回调里调resubscribeAll()。2.3 配置管理与多环境切换环境配置单独放文件方便切开发、测试、生产环境// config/mqtt.js module.exports { dev: { url: wxs://iot-dev.example.com:8084/mqtt, username: dev_user, password: dev_pass }, prod: { url: wxs://iot.example.com:8084/mqtt, username: prod_user, password: prod_pass } };在页面里引入时先判断当前环境。微信开发者工具里可以自己设置const env dev也可以用__wxConfig.envVersion区分开发版、体验版、正式版。不要把这些连接信息硬编码在页面里否则后面换环境要全局搜索替换容易漏。3. 核心流程连接、订阅、发布、断线重连3.1 连接参数与生命周期绑定小程序页面有明确的onLoad、onUnload生命周期MQTT客户端必须在页面加载时连接页面销毁时断开。如果不及时断开页面跳转后连接还挂在后台既占资源又容易出现消息回调触达已销毁页面的问题。我在页面里是这样绑定的const mqttClient new MqttClient({ url: wxs://iot.example.com:8084/mqtt, clientId: wx_ Date.now(), username: device_001, password: token }); Page({ onLoad() { mqttClient.messageHandlers.push(this.onMessage.bind(this)); mqttClient.connect(); }, onUnload() { mqttClient.end(); }, onMessage(topic, payload) { const data JSON.parse(payload); console.log(topic, data); } });clientId尽量不要写死。多个微信用户同时打开小程序如果clientId一样MQTT服务端会踢掉前面的连接导致连接反复断开。我一般用时间戳随机数生成保证每个端不同。如果对接的是阿里云、华为云这类物联网平台clientId、username、password构成三元组认证格式一般有特殊要求比如clientId可能是设备名|securemode3,signmethodhmacsha1|这种直接按平台文档拼就行。3.2 订阅与消息回调订阅主题是MQTT的核心用法。在我的项目里主题规划成了这样device/{deviceId}/telemetry // 设备属性上报 device/{deviceId}/event // 设备事件 device/{deviceId}/cmd // 平台下发控制命令 device/{deviceId}/cmd_reply // 设备命令回复设备状态类数据走telemetry控制指令走cmd回复走cmd_reply。消息内容统一用JSON字符串比如温度传感器上报{ temperature: 26.5, humidity: 48.2, timestamp: 1710000000000 }页面订阅时可以直接订阅通配符比如device//telemetry这样所有设备的上报都能收到。在消息回调里通过topic解析出deviceId。一个需要注意的地方是消息回调的执行频率。如果设备每秒上报数据MQTT消息回调也会每秒触发如果每次回调都this.setData全量更新页面小程序会明显卡顿。我的做法是设置一个节流器比如100毫秒合并一次数据再更新UI。3.3 发布指令与消息格式约定发布控制指令时不能只发一个裸字符串至少要有命令类型、目标设备、请求ID、时间戳。请求ID尤其重要它是后续ACK回执对应的凭证。function sendCommand(deviceId, cmd, params) { const requestId req_ Date.now() _ Math.random().toString(36).slice(2, 6); const topic device/${deviceId}/cmd; const payload JSON.stringify({ requestId, cmd, params, timestamp: Date.now() }); const ok mqttClient.publish(topic, payload, 1); if (!ok) { wx.showToast({ title: 连接已断开, icon: none }); } return requestId; }发布时用QoS 1而不是QoS 0这样消息至少送达一次。QoS 0相当于发消息不确认丢了就丢了QoS 1相当于对方收到后回执最多重复QoS 2是严格一次物联网控制场景一般QoS 1就够。要记住QoS说到底是客户端和服务端之间的可靠性不是端到端可靠性设备端最终有没有执行还得靠业务层ACK确认。3.4 心跳保活与客户端异常处理MQTT协议本身有心跳机制客户端在keepalive时间间隔内发送PINGREQ报文服务端回PINGRESP。mqtt.js默认keepalive是60秒我会显式设置成30秒让服务端能更快发现死连接。但这只是协议层心跳微信小程序切后台后定时器可能被挂起所以协议心跳不一定可靠。我做了两层保活第一层设置keepalive: 30。第二层监听小程序生命周期在wx.onAppShow时检查连接状态如果断开了就重连在wx.onAppHide时记录当前时间不立即断开。wx.onAppShow(() { if (!mqttClient.isConnected) { mqttClient.connect(); } });尽量在小程序切入后台时清理一些不必要的定时器等回到前台再恢复这样既能省电也能减少被系统挂起的概率。3.5 断线重连与订阅恢复mqtt.js自带reconnectPeriod参数但我发现直接交给它的默认重连策略在小程序场景里并不理想。它可能立刻重连然后网络还没恢复又失败来回折腾。所以我大多时候把reconnectPeriod设为0自己在业务层控制重连。简单重连策略是固定间隔3秒重试复杂一点用指数退避let retryCount 0; function handleDisconnect() { if (retryCount 10) { wx.showToast({ title: 连接失败请检查网络, icon: none }); return; } const delay Math.min(3000 * Math.pow(2, retryCount), 30000); retryCount; setTimeout(() { mqttClient.connect(); }, delay); }连接成功后重置retryCount为0。不要忘记重连成功不等同于订阅恢复必须重新订阅之前的主题这就是封装类里resubscribeAll()存在的意义。4. 物联网设备监控场景的实战落地4.1 实时数据看板消息到UI的更新策略设备监控页面最典型的需求是实时展示温度、湿度、开关状态。我一开始是这么写的mqttClient.messageHandlers.push((topic, payload) { const data JSON.parse(payload); this.setData({ temperature: data.temperature, humidity: data.humidity }); });设备10Hz上报时页面每秒setData十次iPhone能扛住低端安卓机明显掉帧。后来把setData改成批量合并更新使用一个临时队列每100毫秒取最新一份数据渲染一次。数据更新时间戳没变就不渲染宁可丢中间帧也不能让UI卡死。let pendingData null; function onTelemetry(data) { pendingData data; } setInterval(() { if (pendingData) { this.setData({ temperature: pendingData.temperature, humidity: pendingData.humidity }); pendingData null; } }, 100);高频上报场景下UI看到的最新值比真实数据慢一两百毫秒完全不影响监控但流畅度的提升是质变。4.2 设备控制指令发布与确认机制控制指令必须闭环。只发指令不确认用户点了开关但不知道设备到底执行没有。我的实现是用户点击“打开继电器”按钮。小程序发布device/001/cmd消息cmd为relay_on附带requestId。同时启动一个10秒超时定时器并保存requestId到pendingMap。设备执行后往device/001/cmd_reply发消息requestId和原指令一致code为0表示成功。小程序收到reply清除超时定时器提示“设备已打开”。如果10秒内没有收到reply提示“设备无响应”。这里要注意一个页面可能同时发多条指令所以pendingMap要用requestId做key而不是粗暴地用一个变量保存当前请求。const pendingMap new Map(); function waitAck(requestId, timeout 10000) { return new Promise((resolve, reject) { const timer setTimeout(() { pendingMap.delete(requestId); reject(new Error(ack timeout)); }, timeout); pendingMap.set(requestId, { resolve, reject, timer }); }); }消息回调里判断topic为cmd_reply时解析requestId找到pendingMap里的Promise执行resolve。4.3 多设备/多主题管理当页面里设备数量多起来一个页面会订阅几十个主题。这时候不能对每个主题单独写回调最好通过topic结构统一分发。比如订阅device//telemetry后用正则或split解析出中间那段deviceIdfunction parseDeviceId(topic) { const parts topic.split(/); if (parts.length 2) { return parts[1]; } return null; }拿到deviceId后把数据存入页面的devices对象里对应键用setData更新局部数据this.setData({ [devices.${deviceId}.temperature]: data.temperature });这种动态路径更新setData的方式比每次更新整个devices数组效率高得多。设备列表很长时效果尤其明显。5. 上线前必须处理的坑5.1 socket合法域名与nginx代理配置这是小程序连接MQTT最容易踩的配置坑。开发工具里可以勾选“不校验合法域名”真机一跑就出问题。小程序后台需要配置socket合法域名并且只支持wss://。你如果直接用IP地址加端口比如wss://192.168.1.10:8084/mqtt基本过不了审核和真机校验必须用已经备案的域名并且HTTPS证书有效。如果服务端走nginx反向代理WebSocket要配置协议升级头否则连上之后马上断开或者直接报1006。我的nginx配置长这样location /mqtt { proxy_pass http://127.0.0.1:8083; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; }这条配置的意思是WebSocket连接不能像普通HTTP请求一样转发必须让nginx知道要升级协议并且保持长连接不超时。proxy_read_timeout建议调大否则设备可能长时间没消息nginx主动断开。5.2 小程序生命周期与后台运行小程序一旦切到后台WebSocket连接并不会立刻断开但系统可能随时回收。iOS上锁屏超过一段时间后微信进程的网络连接可能会被系统断开Android上如果用户清理后台小程序进程直接没了连接自然也没了。我遇到最多的问题是用户切后台几分钟再回来界面还是老样子但实际连接已经断了。解决方式是在wx.onAppShow里主动检查并且把重连逻辑做成幂等。所谓幂等就是无论如何调用不会重复创建多个连接。我在MqttClient类的connect方法里加了判断connect() { if (this.client) return; ... }重复点击或重复进入页面时不会建出第二个client。5.3 setData性能与内存问题长连接调试场景里最容易出现的问题是消息回调里频繁setData导致内存暴涨。小程序setData本质上是把数据从逻辑层传到渲染层数据越大、频率越高性能越差。除了节流还要注意不要在消息回调里保存太多历史记录。如果页面需要展示最近N条消息建议只保留最新100条超出就丢弃防止数组无限增长。另外client.end(true)是立即断开如果页面上还有定时器在跑记得在onUnload里一起清除否则定时器回调还会执行报“setData后页面已销毁”之类的警告。5.4 常见报错与排查思路这里整理我在项目中实际遇到过的报错按出现频率排序报错现象可能原因处理办法WebSocket connection failed域名未配置、证书无效、网络不通检查socket合法域名确认wss地址可达连接成功但马上断开错误码1006nginx未配置Upgrade头或服务端主动断开检查代理配置看服务端日志mqtt connect fail: not authorizedusername/password错误或三元组映射不对核对账号、密码、clientId格式topic订阅权限不足服务端配置了ACL设备无权限订阅该主题检查物联网平台主题权限设置Buffer is not definedmqtt.min.js版本与小程序不兼容换支持wxs的MQTT.js版本或补Buffer polyfill我记得有一次调试了整整半天结果发现是nginx没配置proxy_set_header Connection upgrade连接总是握手上去了但服务端立刻认为不是WebSocket请求直接断开。这种问题从客户端看只会看到一个1006把排查重点放在代理层就能快速定位。最后再说一点在这个项目里的实际体会小程序端跑MQTT难点从来不在协议本身。mqtt.js把MQTT报文封装得很好但小程序生命周期、WebSocket域名限制、setData性能这些问题才是真正会消耗时间的地方。我踩过几次坑之后养成了几个习惯所有连接配置集中管理、所有订阅统一维护、所有命令必须带requestId并做ACK超时。这套规范虽然前期多写几行代码但后面接入新设备、新页面时非常省心。如果你也想在微信小程序里做MQTT客户端我建议先把连接、订阅、发布、断开这个最小闭环跑通再慢慢加断线重连、多设备管理、控制确认这些机制。文中这个MqttClient封装可以直接复制到多个页面只需要把订阅主题和消息回调改成按需注册就能复用。整个工程整理成zip之后体积不大导入微信开发者工具就能开始调试跑起来后再去改自己的业务逻辑会顺手很多。本文还有配套的精品资源点击获取
返回列表