fastbee/yack-mini/miniprogram/services/mqtt-client.js

347 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* MQTT 客户端服务
*/
const mqtt = require('../libs/mqtt-wx.min.js');
const envConfig = require('../env.config.js');
// MQTT 状态常量
const MQTT_STATE = {
DISCONNECTED: 'disconnected',
CONNECTING: 'connecting',
CONNECTED: 'connected',
};
class MqttClient {
constructor() {
this.client = null;
this.state = MQTT_STATE.DISCONNECTED;
this.subscriptions = new Map();
this.onMessageCallback = null;
this.onStateChangeCallback = null;
this.reconnectAttempts = 0;
// 当前连接的 clientId(用于检测连接复用)
this.currentClientId = null;
}
/**
* 设置消息回调
* @param {Function} callback - (topic, message) => void
*/
onMessage(callback) {
this.onMessageCallback = callback;
}
/**
* 设置状态变化回调
* @param {Function} callback - (state) => void
*/
onStateChange(callback) {
this.onStateChangeCallback = callback;
}
/**
* 更新状态并通知
* @param {string} state
*/
_setState(state) {
this.state = state;
if (this.onStateChangeCallback) {
this.onStateChangeCallback(state);
}
console.log('[MQTT] 状态变更:', state);
}
/**
* 连接 MQTT Broker
* @param {Object} options - 连接配置
* @param {string} options.clientId - 客户端 ID
* @param {string} options.username - 用户名
* @param {string} options.password - 密码
* @param {string} options.server - 服务器地址(可选,默认使用 envConfig)
* @returns {Promise}
*/
connect(options) {
return new Promise((resolve, reject) => {
const newClientId = options.clientId;
// 检查网络连接
wx.getNetworkType({
success: (res) => {
if (res.networkType === 'none') {
reject(new Error('网络未连接,请检查网络设置'));
return;
}
console.log('[MQTT] 网络类型:', res.networkType);
},
fail: () => {
console.warn('[MQTT] 无法获取网络状态');
}
});
// 检查是否需要重建连接(clientId 变化或未连接)
if (this.client && this.client.connected) {
if (this.currentClientId === newClientId) {
// 同一设备,复用连接
console.log('[MQTT] 已连接, 复用连接, clientId:', newClientId);
resolve();
return;
}
// 不同设备,先断开旧连接,防止数据串号
console.log('[MQTT] clientId 变化,断开旧连接:', this.currentClientId, '->', newClientId);
this.client.end(true); // 强制关闭,停止重连
this.client = null;
this.subscriptions.clear();
}
this._setState(MQTT_STATE.CONNECTING);
const server = options.server || envConfig.mqttServer;
const connectOptions = {
clientId: options.clientId,
username: options.username,
password: options.password,
clean: true,
keepalive: 30,
connectTimeout: 30000,
reconnectPeriod: 5000,
// protocol 从 URL 自动解析,无需手动设置
};
console.log('[MQTT] 正在连接:', server, 'clientId:', options.clientId);
// 检查服务器地址协议
if (server.startsWith('ws://') && !__wxConfig) {
console.warn('[MQTT] 真机环境建议使用 wss:// 或 wxs:// 协议');
}
try {
this.client = mqtt.connect(server, connectOptions);
this.currentClientId = options.clientId;
// 连接成功
this.client.on('connect', () => {
console.log('[MQTT] 连接成功');
this._setState(MQTT_STATE.CONNECTED);
this.reconnectAttempts = 0;
// 重新订阅之前的主题
this._resubscribe();
resolve();
});
// 连接错误
this.client.on('error', (err) => {
console.error('[MQTT] 连接错误:', err);
const errMsg = err.message || '未知错误';
// 提供更友好的错误提示
let userMsg = 'MQTT 连接失败';
if (errMsg.includes('ECONNREFUSED')) {
userMsg = 'MQTT 服务器拒绝连接,请检查服务器配置';
} else if (errMsg.includes('ENOTFOUND')) {
userMsg = 'MQTT 服务器地址解析失败,请检查地址';
} else if (errMsg.includes('ETIMEDOUT') || errMsg.includes('timeout')) {
userMsg = 'MQTT 连接超时,请检查网络或服务器状态';
} else if (errMsg.includes('WebSocket')) {
userMsg = 'WebSocket 连接失败,请检查协议(wss:// 或 wxs://)';
}
// 仅首次连接失败时关闭并交由调用方(gateway)重试;
// lib 自动重连期间(reconnecting=true)的 error 不拆客户端,让 reconnectPeriod 自愈
if (this.client && !this.client.reconnecting) {
this.client.end(true);
this.client = null;
}
this._setState(MQTT_STATE.DISCONNECTED);
reject(new Error(userMsg + ': ' + errMsg));
});
// 断开连接
this.client.on('close', () => {
console.log('[MQTT] 连接关闭');
this._setState(MQTT_STATE.DISCONNECTED);
});
// 重连(底层 mqtt lib 按 reconnectPeriod 自连,网关语义永不放弃;
// 首次连接失败后的主动重试由 gateway 负责)
this.client.on('reconnect', () => {
this.reconnectAttempts++;
console.log('[MQTT] 正在重连, 尝试次数:', this.reconnectAttempts);
this._setState(MQTT_STATE.CONNECTING);
});
// 收到消息
this.client.on('message', (topic, message) => {
const msgStr = message.toString();
console.log('[MQTT] 收到消息, topic:', topic, 'message:', msgStr);
if (this.onMessageCallback) {
this.onMessageCallback(topic, msgStr);
}
// 触发订阅回调
if (this.subscriptions.has(topic)) {
const callback = this.subscriptions.get(topic);
callback(topic, msgStr);
}
});
// 离线
this.client.on('offline', () => {
console.log('[MQTT] 离线');
this._setState(MQTT_STATE.DISCONNECTED);
});
} catch (err) {
console.error('[MQTT] 创建客户端失败:', err);
this._setState(MQTT_STATE.DISCONNECTED);
reject(new Error('创建 MQTT 客户端失败: ' + (err.message || '未知错误')));
}
});
}
/**
* 发布消息
* @param {string} topic - 主题
* @param {string} message - 消息内容
* @param {Object} options - 发布选项 {qos: 0|1|2, retain: boolean}
* @returns {Promise}
*/
publish(topic, message, options = { qos: 1 }) {
return new Promise((resolve, reject) => {
if (!this.client || !this.client.connected) {
reject(new Error('MQTT 未连接'));
return;
}
this.client.publish(topic, message, options, (err) => {
if (err) {
console.error('[MQTT] 发布失败:', err);
reject(new Error('发布失败'));
} else {
console.log('[MQTT] 发布成功, topic:', topic, 'message:', message);
resolve();
}
});
});
}
/**
* 订阅主题
* @param {string} topic - 主题
* @param {Function} callback - 消息回调(可选)
* @param {Object} options - 订阅选项 {qos: 0|1|2}
* @returns {Promise}
*/
subscribe(topic, callback = null, options = { qos: 1 }) {
return new Promise((resolve, reject) => {
if (!this.client || !this.client.connected) {
reject(new Error('MQTT 未连接'));
return;
}
this.client.subscribe(topic, options, (err) => {
if (err) {
console.error('[MQTT] 订阅失败:', err);
reject(new Error('订阅失败'));
} else {
console.log('[MQTT] 订阅成功:', topic);
if (callback) {
this.subscriptions.set(topic, callback);
}
resolve();
}
});
});
}
/**
* 取消订阅
* @param {string} topic - 主题
* @returns {Promise}
*/
unsubscribe(topic) {
return new Promise((resolve, reject) => {
if (!this.client || !this.client.connected) {
resolve();
return;
}
this.client.unsubscribe(topic, (err) => {
if (err) {
console.error('[MQTT] 取消订阅失败:', err);
reject(new Error('取消订阅失败'));
} else {
console.log('[MQTT] 取消订阅成功:', topic);
this.subscriptions.delete(topic);
resolve();
}
});
});
}
/**
* 重新订阅所有主题
*/
_resubscribe() {
if (this.subscriptions.size === 0) return;
this.subscriptions.forEach((callback, topic) => {
this.subscribe(topic, callback).catch(err => {
console.error('[MQTT] 重订阅失败:', topic, err);
});
});
}
/**
* 断开连接
*/
disconnect() {
return new Promise((resolve) => {
if (!this.client) {
resolve();
return;
}
this.client.end(false, () => {
console.log('[MQTT] 已断开连接');
this._setState(MQTT_STATE.DISCONNECTED);
this.client = null;
this.currentClientId = null;
this.subscriptions.clear();
resolve();
});
});
}
/**
* 获取当前状态
* @returns {string}
*/
getState() {
return this.state;
}
/**
* 是否已连接
* @returns {boolean}
*/
isConnected() {
return this.client && this.client.connected;
}
/**
* 清除消息回调(页面卸载时调用)
*/
clearCallbacks() {
this.onMessageCallback = null;
this.onStateChangeCallback = null;
console.log('[MQTT] 已清除回调');
}
}
// 导出单例
const mqttClient = new MqttClient();
module.exports = {
MQTT_STATE,
MqttClient,
mqttClient,
};