项目需要给小程序加一个“消息通知”功能,后端给了MQTT接入方式。一开始我觉得简单,用WebSocket直接连不就行了,结果真上手的时候,发现小程序和H5不一样,不只是url换个ws://那么简单。中间踩了一堆坑,从连接失败到收发消息乱掉,再到退出页面连接还在后台偷偷跑。今天把整个接入过程写出来,省得大家再绕弯。
一、选型:为什么不用原生WebSocket
后端用的是EMQX,标准MQTT协议。如果用原生WebSocket,需要自己处理MQTT协议中的PUBLISH、SUBSCRIBE、心跳等细节,非常繁琐。所以直接用mqtt.js这个库,它已经封装好了协议。而且它在小程序环境也能跑,但要改一点东西。
网上很多教程让你去npm安装mqtt,然后直接import,但在小程序里直接跑会报错,因为mqtt.js默认依赖Node.js的很多模块(比如process, global)。所以要么用webpack配置alias,要么用在线构建的版本。这里我选择使用npm包,然后通过uniapp的vue.config.js去解决。
二、安装和基础配置
我项目用的是vue3+vite,因此按照vite的方式处理。
npm install mqtt
然后在vue.config.js里做如下配置(vite就用vite.config.js对应的地方):
// vue.config.js 如果你的项目是vue2
const webpack = require('webpack');
module.exports = {
configureWebpack: {
plugins: [
new webpack.ProvidePlugin({
process: 'process/browser',
Buffer: ['buffer', 'Buffer']
})
],
resolve: {
fallback: {
"stream": require.resolve("stream-browserify"),
"crypto": require.resolve("crypto-browserify"),
"assert": require.resolve("assert/"),
"http": require.resolve("stream-http"),
"https": require.resolve("https-browserify"),
"os": require.resolve("os-browserify/browser"),
"url": require.resolve("url/"),
"path": require.resolve("path-browserify")
}
}
}
}
如果你用的是vite,需要在vite.config.js里配置define和resolve.alias等,特别麻烦。我的建议是:如果小程序为主,直接用HBuilderX的“npm支持”编译,它内置的webpack处理方案成熟一点。我就是用HBuilderX运行的。
三、第一个坑:mqtt.js在小程序里连不上
我在H5测试一切正常,在小程序模拟器里连不上,报错WebSocket connection to 'wss://xxxx/mqtt' failed。后来发现是因为小程序要求域名必须是合法配置的wss,而且不能带端口路径。其次mqtt.js默认使用WebSocket的全局对象,小程序里没有原生WebSocket?其实有,但接口有点不一样。最后我找到社区方案:单独适配mqtt.js的websocket入口。
npm包提供mqtt/dist/mqtt.min.js这个UMD版本,里面通过wx.connectSocket实现。不过在uniapp里,最好的方式是用mqtt.min.js并修改它的连接代码。
import mqtt from 'mqtt/dist/mqtt.min.js';
但直接用这个路径,uniapp打包时可能还会遇到global问题。为了省事,我直接在项目里放了一份mqtt.js文件(从unpkg下载的es5版本),然后在页面里用require('../../utils/mqtt.js')引入,确保不经过vite的转译。这个看似笨办法,但确实稳定。
四、连接参数的正确写法
连接时不能和H5一样直接写ws://。小程序里必须用wxs://或者wss://?其实协议前缀还是wss://,但是不区分大小写?我的理解是:后端如果是wss,那路径就是wss://xxxx/mqtt。但需要注意的是,在小程序后台把socket合法域名配置好,不是https那个,是socket合法域名。例如wss://mqtt.example.com,不能带端口。
我的连接参数如下:
const client = mqtt.connect('wss://mqtt.example.com/mqtt', {
clientId: 'uni_' + Date.now() + '_' + Math.random().toString(16).substr(2),
username: 'user001',
password: 'pass123',
clean: true,
reconnectPeriod: 4000,
connectTimeout: 5000,
protocolVersion: 4
});
这里的clientId必须唯一,不能写死。username和password由后端提供。如果连接不上,八成是clientId冲突或者域名没配上。
五、消息的收发与界面更新
连接成功之后,订阅主题就可以收到消息。在uniapp页面里,需要注意消息回调是在子线程还是主线程?mqtt.js回调默认在同一线程,但在小程序中网络回调需要确保UI更新在Page里,直接在回调里使用this可能不对,可以在外面把this存一下。
onLoad() {
const that = this;
this.client.on('connect', () => {
console.log('连接成功');
that.client.subscribe('user/123/messages', (err) => {
if (!err) {
console.log('订阅成功');
}
});
});
this.client.on('message', function(topic, message) {
const data = JSON.parse(message.toString());
// 注意:这里的this不是页面实例
that.messageList.push(data);
console.log('收到消息', data);
});
}
但是如果你在页面onLoad里创建client,那么每次进入页面都会新建一个连接,离开页面时如果不关闭,连接会一直存在,造成资源浪费。所以通常把mqtt连接放在一个独立的模块里,做成单例。我只在一个不需要全局的页面里测试,所以直接在onLoad创建,onUnload关闭。
六、页面关闭后连接还在后台的坑
一开始我没在onUnload里调用client.end(true),结果退出页面后,控制台还在打印收到消息,而且再次进入页面时会创建新连接,旧连接依然存在,导致重复消息。正确做法是:
onUnload() {
if (this.client) {
this.client.end(true); // 表示强制断开
}
}
但这样也有一个问题:如果小程序切到后台,连接会被系统销毁,那么回来时连不上。需要通过监听onShow和onHide来重建连接。更完善的做法是做成全局单例,用App.vue里创建连接,页面中只订阅topic。我这里确实就偷懒了,直接页面管理。
七、掉线重连会导致订阅丢失
mqtt.js自带reconnectPeriod,断线后会自动重连,但重连成功后需要重新订阅之前的所有主题。所以必须在connect回调里再执行subscribe。此外,如果用户网络状态变化,也会触发close或offline事件。我增加一个handler:
this.client.on('reconnect', () => {
console.log('正在重连...');
});
this.client.on('close', () = > {
console.log('连接关闭');
});
this.client.on('offline', () => {
console.log('已离线');
});
其中end()会触发close,所以不要在close里处理重连逻辑,否则会重复。
八、收到的消息比预期多或乱序
MQTT的QoS级别会影响消息可达性。我后端发送时用了QoS1,但我订阅时没设置QoS,默认可能是0,导致消息乱序重复。建议订阅时统一用{ qos: 1 }:
client.subscribe('user/123/messages', { qos: 1 }, (err) => {
// 回调
});
同时发送消息时,如果客户端要发布,推荐使用client.publish(topic, payload, { qos: 1 }),避免消息丢失。
九、完整示例代码(页面内直接可用)
<template>
<view class="container">
<input v-model="msg" placeholder="输入消息" />
<button @click="sendMessage">发送</button>
<view v-for="(item, index) in messages" :key="index">{{ item }}</view>
</view>
</template>
<script>
import mqtt from '../../utils/mqtt.min.js';
export default {
data() {
return {
client: null,
msg: '',
messages: []
};
},
onLoad() {
this.connectMqtt();
},
onUnload() {
if (this.client) {
this.client.end(true);
}
},
methods: {
connectMqtt() {
this.client = mqtt.connect('wss://mqtt.example.com/mqtt', {
clientId: 'uni_' + Date.now(),
username: 'test',
password: 'secret',
clean: true,
reconnectPeriod: 5000,
connectTimeout: 5000,
protocolVersion: 4
});
this.client.on('connect', () => {
console.log('连接成功');
this.client.subscribe('order/notify', { qos: 1 }, (err) => {
if (err) {
console.error('订阅失败', err);
}
});
});
this.client.on('message', (topic, payload) => {
const text = payload.toString();
this.messages.push(topic + ': ' + text);
});
this.client.on('reconnect', () => {
console.log('重连中');
});
this.client.on('error', (err) => {
console.error('MQTT错误', err);
});
},
sendMessage() {
if (!this.msg) return;
this.client.publish('order/notify', this.msg, { qos: 1 });
this.messages.push('我发送: ' + this.msg);
this.msg = '';
}
}
};
</script>
十、一些真正的坑总结
- HBuilderX内置webpack可以解决mqtt.js的全局问题,但最好不要用npm安装的,直接用本地min.js。
- 小程序后台域名必须配置socket合法域名,而且必须是https/wss,不能用ip加端口。
- 连接成功回调里订阅,不然重连后不会自动订阅。
- 离开页面要end(true),否则连接泄漏。
- 如果要跨页面共享消息,需要把mqtt连接挂在Vuex或全局,页面只负责subscribe和onMessage分发。
- 消息到达后要判断页面是否处于前台,如果退到后台,不要弹UI,可以改用全局变量或本地存储。
- 在部分安卓手机上,长时间连接会被系统回收,所以最好监听App的onShow/onHide手动重连。
十一、能不能用uniapp提供的uni.connectSocket去封装?
可以,但那样就相当于自己实现MQTT协议了,太麻烦。除非你只需要极简功能,否则还是用mqtt.js成熟。
十二、最终建议
如果你的业务只需要服务端消息推送,不需要客户端上报,可以考虑使用uni.subscribeMessage或微信公众号模板消息,小程序原生支持,还不需要个人服务器。但如果要做IM双向聊天,那MQTT就是很好的选择。我在这个项目里只用了订阅和发布,已经够了。
上面这些坑是我分别在小程序开发工具、真机调试和H5端测试总结出来的,希望能帮你省点时间。

