HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

关于uni.showToast被原生界面遮挡,无法穿透

分享插件

问题:

在使用原生模块,或者插件时,想使用Toast弹出信息,但是却被原生界面遮挡,无法看到

解决:

可使用插件市场的 Ba-Toast 原生插件,可完美穿透原生界面

原生Toast弹窗提示(穿透所有界面、穿透原生;自定义颜色、图标)Ba-Toast

Ba-Toast 是一款可穿透所有界面(包括所有原生插件的界面),也可在系统页面显示的原生Toast弹窗提示插件。调用方法参照uniapp自带showToast风格,接入简单,功能强大。


👤 作者介绍

作者: 三杯五岳(q:2579546054)

专注于 UniApp 原生插件、UTS插件开发,包括安卓、苹果、鸿蒙,致力于为开发者提供高质量、易用的原生插件解决方案。

主要方向:

  • UniApp 原生插件开发
  • UniApp UTS插件开发
  • Android、iOS、Harmony原生项目开发
  • 移动端跨平台项目开发
  • 微信小程序
  • PC前端

> 💡 提示: 如需定制开发、技术支持或有其他需求,欢迎通过QQ或留言咨询!


⭐ 如果这些插件对您有帮助,欢迎点赞、收藏、分享!⭐

感谢支持!

继续阅读 »

问题:

在使用原生模块,或者插件时,想使用Toast弹出信息,但是却被原生界面遮挡,无法看到

解决:

可使用插件市场的 Ba-Toast 原生插件,可完美穿透原生界面

原生Toast弹窗提示(穿透所有界面、穿透原生;自定义颜色、图标)Ba-Toast

Ba-Toast 是一款可穿透所有界面(包括所有原生插件的界面),也可在系统页面显示的原生Toast弹窗提示插件。调用方法参照uniapp自带showToast风格,接入简单,功能强大。


👤 作者介绍

作者: 三杯五岳(q:2579546054)

专注于 UniApp 原生插件、UTS插件开发,包括安卓、苹果、鸿蒙,致力于为开发者提供高质量、易用的原生插件解决方案。

主要方向:

  • UniApp 原生插件开发
  • UniApp UTS插件开发
  • Android、iOS、Harmony原生项目开发
  • 移动端跨平台项目开发
  • 微信小程序
  • PC前端

> 💡 提示: 如需定制开发、技术支持或有其他需求,欢迎通过QQ或留言咨询!


⭐ 如果这些插件对您有帮助,欢迎点赞、收藏、分享!⭐

感谢支持!

收起阅读 »

基于vue3.5+vite7+vant4+pinia3实战仿微信界面聊天模板

vite vue3

vite7-vue3-chatroom:基于vue3.5+vite7+pinia3+vant4.x手搓微信app界面聊天实例。包含聊天/通讯录/我的模块,支持图文消息/gif动图、图片/视频预览、红包/朋友圈等功能。

运用技术

  • 技术框架:Vite7.x+Vue3.5+Pinia3+Vue-Router4
  • 组件库:Vant-UI4.x (有赞移动端Vue3组件库)
  • 弹层组件:V3Popup(基于vue3.0自定义弹窗组件)
  • iconfont图标:阿里字体图标库
  • 自定义顶部导航条+底部tabBar

项目框架目录

整个项目使用vite7构建,采用vue3 setup语法糖编码开发。

vite7-wechat聊天室项目已经正式发布到我的原创小店作品集,感谢支持!
Vue3+Vite7+Pinia3+Vant4移动端仿微信聊天室模板

更多详细的项目介绍,可以去看看下面的这篇分享文章。

Vite-Chat聊天室|vue3.5+vite7+pinia3+Vant4移动端仿微信聊天模板

往期推荐

原创uniapp+vue3+deepseek+uv-ui跨端实战仿deepseek/豆包流式ai聊天对话助手。
vue3-webseek网页版AI问答|Vite6+DeepSeek+Arco流式ai聊天打字效果
Electron38-Wechat电脑端聊天|vite7+electron38仿微信桌面端聊天系统
Electron38-Vue3OS客户端OS系统|vite7+electron38+arco桌面os后台管理
electron38-admin桌面端后台|Electron38+Vue3+ElementPlus管理系统
最新版uniapp+vue3+uv-ui跨三端短视频+直播+聊天【H5+小程序+App端】
最新版uni-app+vue3+uv-ui跨三端仿微信app聊天应用【h5+小程序+app端】
uniapp-vue3-os手机oa系统|uni-app+vue3跨三端os后台管理模板
Tauri2.9+Vue3桌面版OS系统|vite7+tauri2+arcoDesign电脑端os后台模板
Tauri2.8+Vue3聊天系统|vite7+tauri2+element-plus客户端仿微信聊天程序
Tauri2-Vite7Admin客户端管理后台|tauri2.9+vue3+element-plus后台系统
Flutter3-MacOS桌面OS系统|flutter3.32+window_manager客户端OS模板
最新研发flutter3.27+bitsdojo_window+getx客户端仿微信聊天Exe应用
最新版Flutter3.32+Dart3.8跨平台仿微信app聊天界面|朋友圈
Electron35-DeepSeek桌面端AI系统|vue3.5+electron+arco客户端ai模板

继续阅读 »

vite7-vue3-chatroom:基于vue3.5+vite7+pinia3+vant4.x手搓微信app界面聊天实例。包含聊天/通讯录/我的模块,支持图文消息/gif动图、图片/视频预览、红包/朋友圈等功能。

运用技术

  • 技术框架:Vite7.x+Vue3.5+Pinia3+Vue-Router4
  • 组件库:Vant-UI4.x (有赞移动端Vue3组件库)
  • 弹层组件:V3Popup(基于vue3.0自定义弹窗组件)
  • iconfont图标:阿里字体图标库
  • 自定义顶部导航条+底部tabBar

项目框架目录

整个项目使用vite7构建,采用vue3 setup语法糖编码开发。

vite7-wechat聊天室项目已经正式发布到我的原创小店作品集,感谢支持!
Vue3+Vite7+Pinia3+Vant4移动端仿微信聊天室模板

更多详细的项目介绍,可以去看看下面的这篇分享文章。

Vite-Chat聊天室|vue3.5+vite7+pinia3+Vant4移动端仿微信聊天模板

往期推荐

原创uniapp+vue3+deepseek+uv-ui跨端实战仿deepseek/豆包流式ai聊天对话助手。
vue3-webseek网页版AI问答|Vite6+DeepSeek+Arco流式ai聊天打字效果
Electron38-Wechat电脑端聊天|vite7+electron38仿微信桌面端聊天系统
Electron38-Vue3OS客户端OS系统|vite7+electron38+arco桌面os后台管理
electron38-admin桌面端后台|Electron38+Vue3+ElementPlus管理系统
最新版uniapp+vue3+uv-ui跨三端短视频+直播+聊天【H5+小程序+App端】
最新版uni-app+vue3+uv-ui跨三端仿微信app聊天应用【h5+小程序+app端】
uniapp-vue3-os手机oa系统|uni-app+vue3跨三端os后台管理模板
Tauri2.9+Vue3桌面版OS系统|vite7+tauri2+arcoDesign电脑端os后台模板
Tauri2.8+Vue3聊天系统|vite7+tauri2+element-plus客户端仿微信聊天程序
Tauri2-Vite7Admin客户端管理后台|tauri2.9+vue3+element-plus后台系统
Flutter3-MacOS桌面OS系统|flutter3.32+window_manager客户端OS模板
最新研发flutter3.27+bitsdojo_window+getx客户端仿微信聊天Exe应用
最新版Flutter3.32+Dart3.8跨平台仿微信app聊天界面|朋友圈
Electron35-DeepSeek桌面端AI系统|vue3.5+electron+arco客户端ai模板

收起阅读 »

网纸xs10250.com新盛公司网上游戏代理号注册网址

新盛线上游戏网址(xs10250.com)微信(fg11667744)或新盛官网 )以获取业务咨询和办理服务。

  1. 主页注册:在网站主页右上角找到“注册”按钮,并创建一个您心仪的账户名。, F3 f; n& P. z4 u: S
  2. 完成注册信息:在弹出的注册页面,输入您的手机号码、验证码以及设置密
继续阅读 »

新盛线上游戏网址(xs10250.com)微信(fg11667744)或新盛官网 )以获取业务咨询和办理服务。

  1. 主页注册:在网站主页右上角找到“注册”按钮,并创建一个您心仪的账户名。, F3 f; n& P. z4 u: S
  2. 完成注册信息:在弹出的注册页面,输入您的手机号码、验证码以及设置密
收起阅读 »

uniappx 的一大遗憾

开发app最核心的是什么?

变现
变现
变现
变现
变现

可以uniappx对支付的支持真的太弱了,比如:两大应用市场的订阅支付 google 和ios的订阅支付,这都是最基本

现在真的窘迫

用uniappx还要花将近500块去插件市场买uts插件,源码授权900起步

希望官网加强对支付能力的封装

继续阅读 »

开发app最核心的是什么?

变现
变现
变现
变现
变现

可以uniappx对支付的支持真的太弱了,比如:两大应用市场的订阅支付 google 和ios的订阅支付,这都是最基本

现在真的窘迫

用uniappx还要花将近500块去插件市场买uts插件,源码授权900起步

希望官网加强对支付能力的封装

收起阅读 »

oppo函数是调用离线消息收不到,个推后台发送没问题

unipush

oppo函数调用离线消息收不到,个推后台发送没问题,不清楚是什么问题,在线消息都没问题

oppo函数调用离线消息收不到,个推后台发送没问题,不清楚是什么问题,在线消息都没问题

网纸xs10250.com腾龙公司注册会员账号注册平台网址

腾龙公司网纸【xs10250.com】【┿(威 fg11667744】 是一款集成了多种棋牌游戏的娱
在我们平台,注册会员将开启一段独特而精彩的旅程,流程如下。
第二步:在登录/注册页面,找到“注册会员”按钮并点击。
第三步:进入注册界面后,填写必要的个人信息,如用户名、手机号码、qq等….
四步:设置安全且易记的登录密码。
五步:根据提示,可能需要同意相关的服务条款和隐私政策。
第六步:点击“注册”或“确认注册”按钮。
第七步:系统进行信息验证和处理,若一切顺利,将提示注册成功。并可以直接登录。
第八步:恭喜您,现在已经成为公司正式会员,登录即可开始享受会员专属的权益和服务

继续阅读 »

腾龙公司网纸【xs10250.com】【┿(威 fg11667744】 是一款集成了多种棋牌游戏的娱
在我们平台,注册会员将开启一段独特而精彩的旅程,流程如下。
第二步:在登录/注册页面,找到“注册会员”按钮并点击。
第三步:进入注册界面后,填写必要的个人信息,如用户名、手机号码、qq等….
四步:设置安全且易记的登录密码。
五步:根据提示,可能需要同意相关的服务条款和隐私政策。
第六步:点击“注册”或“确认注册”按钮。
第七步:系统进行信息验证和处理,若一切顺利,将提示注册成功。并可以直接登录。
第八步:恭喜您,现在已经成为公司正式会员,登录即可开始享受会员专属的权益和服务

收起阅读 »

UniApp 原生插件集合(2026)

分享插件

前言

本文整理了一些比较常用的原生插件,包括扫码、图片选择、文件选择、图片编辑、应用通知、应用未读角标、开机自启、sqlite数据库、保活、快捷方式、图片水印、视频压缩、动态修改应用图标等等,有其他需要可以留言,感谢支持。


📑 目录

作者博客 市场主页 社区主页
1. 扫码(6款) 14. 地图 27. 监听系统广播、自定义广播
2. 文件选择 15. 悬浮窗 28. 监听通知栏消息(支持白黑名单、过滤)
3. 图片选择 16. 画中画 29. 全局置灰、哀悼置灰(可动态、同时支持nvue、vue)
4. 图片编辑 17. 获取设备唯一标识 30. 窗口小工具、桌面小部件、微件
5. 图片压缩 18. WebSocket原生服务(后台) 31. 用其他应用打开、分享
6. 图片水印 19. 安卓快捷方式(桌面长按app图标) 32. 来电显示
7. 视频压缩、剪辑 20. 动态切换应用图标、名称 33. 抖音授权登录、发布、分享
8. 应用消息通知 21. 动态修改状态栏、导航栏 34. 反射方法调用插件
9. 应用未读角标 22. 原生Toast弹窗提示(穿透所有界面) 35. 字母索引列表插件(组件版)
10. 保活 23. PDF阅读 36. 权限申请插件(权限使用说明)
11. 开机自启 24. 声音提示、震动提示、语音播报 37. 桌面应用插件
12. 数据库 25. 短信监听(验证码)
13. 定位 26. 智能安装(自动升级)

👤 作者介绍

作者: 三杯五岳(q:2579546054)

专注于 UniApp 原生插件、UTS插件开发,包括安卓、苹果、鸿蒙,致力于为开发者提供高质量、易用的原生插件解决方案。

主要方向:

  • UniApp 原生插件开发
  • UniApp UTS插件开发
  • Android、iOS、Harmony原生项目开发
  • 移动端跨平台项目开发
  • 微信小程序
  • PC前端

> 💡 提示: 如需定制开发、技术支持或有其他需求,欢迎通过QQ或留言咨询!


⭐ 如果这些插件对您有帮助,欢迎点赞、收藏、分享!⭐

感谢支持!

继续阅读 »

前言

本文整理了一些比较常用的原生插件,包括扫码、图片选择、文件选择、图片编辑、应用通知、应用未读角标、开机自启、sqlite数据库、保活、快捷方式、图片水印、视频压缩、动态修改应用图标等等,有其他需要可以留言,感谢支持。


📑 目录

作者博客 市场主页 社区主页
1. 扫码(6款) 14. 地图 27. 监听系统广播、自定义广播
2. 文件选择 15. 悬浮窗 28. 监听通知栏消息(支持白黑名单、过滤)
3. 图片选择 16. 画中画 29. 全局置灰、哀悼置灰(可动态、同时支持nvue、vue)
4. 图片编辑 17. 获取设备唯一标识 30. 窗口小工具、桌面小部件、微件
5. 图片压缩 18. WebSocket原生服务(后台) 31. 用其他应用打开、分享
6. 图片水印 19. 安卓快捷方式(桌面长按app图标) 32. 来电显示
7. 视频压缩、剪辑 20. 动态切换应用图标、名称 33. 抖音授权登录、发布、分享
8. 应用消息通知 21. 动态修改状态栏、导航栏 34. 反射方法调用插件
9. 应用未读角标 22. 原生Toast弹窗提示(穿透所有界面) 35. 字母索引列表插件(组件版)
10. 保活 23. PDF阅读 36. 权限申请插件(权限使用说明)
11. 开机自启 24. 声音提示、震动提示、语音播报 37. 桌面应用插件
12. 数据库 25. 短信监听(验证码)
13. 定位 26. 智能安装(自动升级)

👤 作者介绍

作者: 三杯五岳(q:2579546054)

专注于 UniApp 原生插件、UTS插件开发,包括安卓、苹果、鸿蒙,致力于为开发者提供高质量、易用的原生插件解决方案。

主要方向:

  • UniApp 原生插件开发
  • UniApp UTS插件开发
  • Android、iOS、Harmony原生项目开发
  • 移动端跨平台项目开发
  • 微信小程序
  • PC前端

> 💡 提示: 如需定制开发、技术支持或有其他需求,欢迎通过QQ或留言咨询!


⭐ 如果这些插件对您有帮助,欢迎点赞、收藏、分享!⭐

感谢支持!

收起阅读 »

扫码插件(7款)

分享插件

扫码原生 - 新版(支持连续扫码模式;支持设置格式;可任意自定义界面)Ba-Scanner

Ba-Scanner 是一款毫秒级扫码插件,同时支持多码、相册、闪光灯、焦距缩放、提示音、震动等等。

核心特性:

  • 新增支持自定义任意界面、任意点击事件,可以让扫码界面和您的应用更加匹配、美观
  • 新增支持连续扫码,可设置时间间隔,亲测持续扫码万次不卡顿
  • 新增支持设置扫码格式
  • 新增支持自定义webview遮罩界面,可直接传html地址,本地、网络都支持

扫码原生 - 基础版(毫秒级、支持多码、自定义界面)Ba-Scanner-G

基于Google MLKit 快速集成二维码扫描,速度比zxing更快,支持同时扫多个二维码、条形码,可配置相册、闪光灯,相机焦距可缩放,可自定义扫描线颜色、文字提示,支持扫描完成提示音、震动等。


扫码原生插件 - 组件版(毫秒级、连续扫码、多码、相册)Ba-ScanView

Ba-ScanView 是一款毫秒级扫码插件,采用component组件模式,可直接在uniapp界面直接引用,高宽可随意设置。


扫码原生插件 - 组件版(毫秒级、连续扫码、多码、相册、新增支持抓拍图片)Ba-ScanView2

Ba-ScanView2 是一款毫秒级扫码插件,采用component组件模式,可直接在uniapp界面直接引用,高宽可随意设置;支持抓拍照片;支持前置、后置摄像头。


扫码插件 - UTS版(毫秒级、连续扫码、多码、拍照)ba-scanview2-u

ba-scanview2-u 是一款毫秒级扫码插件,采用组件模式,可直接在界面引用,高宽可随意设置;支持抓拍照片。

  • 支持连续扫码,可设置时间间隔
  • 支持多码选择
  • 支持设置扫码格式
  • 支持打开、关闭闪光灯
  • 支持相册图片识别
  • 支持关闭和打开扫描
  • 支持多码直接返回
  • 支持前置、后置摄像头
  • 支持拍照
  • 新增支持获取扫码成功的那一帧照片

扫码原生插件 - 组件版(华为、连续扫码、多码、相册、新增支持抓拍图片)Ba-ScanViewH

Ba-ScanViewH 是一款集成华为统一扫码服务的插件,采用component组件模式,可直接在uniapp界面直接引用,高宽可随意设置;支持抓拍照片。


扫码原生插件 - 最经典zxing版本

基于最经典zxing的插件Ba-Scanner-Zxing,操作简单,界面参照uniapp官方界面,支持相册、闪光灯等

继续阅读 »

扫码原生 - 新版(支持连续扫码模式;支持设置格式;可任意自定义界面)Ba-Scanner

Ba-Scanner 是一款毫秒级扫码插件,同时支持多码、相册、闪光灯、焦距缩放、提示音、震动等等。

核心特性:

  • 新增支持自定义任意界面、任意点击事件,可以让扫码界面和您的应用更加匹配、美观
  • 新增支持连续扫码,可设置时间间隔,亲测持续扫码万次不卡顿
  • 新增支持设置扫码格式
  • 新增支持自定义webview遮罩界面,可直接传html地址,本地、网络都支持

扫码原生 - 基础版(毫秒级、支持多码、自定义界面)Ba-Scanner-G

基于Google MLKit 快速集成二维码扫描,速度比zxing更快,支持同时扫多个二维码、条形码,可配置相册、闪光灯,相机焦距可缩放,可自定义扫描线颜色、文字提示,支持扫描完成提示音、震动等。


扫码原生插件 - 组件版(毫秒级、连续扫码、多码、相册)Ba-ScanView

Ba-ScanView 是一款毫秒级扫码插件,采用component组件模式,可直接在uniapp界面直接引用,高宽可随意设置。


扫码原生插件 - 组件版(毫秒级、连续扫码、多码、相册、新增支持抓拍图片)Ba-ScanView2

Ba-ScanView2 是一款毫秒级扫码插件,采用component组件模式,可直接在uniapp界面直接引用,高宽可随意设置;支持抓拍照片;支持前置、后置摄像头。


扫码插件 - UTS版(毫秒级、连续扫码、多码、拍照)ba-scanview2-u

ba-scanview2-u 是一款毫秒级扫码插件,采用组件模式,可直接在界面引用,高宽可随意设置;支持抓拍照片。

  • 支持连续扫码,可设置时间间隔
  • 支持多码选择
  • 支持设置扫码格式
  • 支持打开、关闭闪光灯
  • 支持相册图片识别
  • 支持关闭和打开扫描
  • 支持多码直接返回
  • 支持前置、后置摄像头
  • 支持拍照
  • 新增支持获取扫码成功的那一帧照片

扫码原生插件 - 组件版(华为、连续扫码、多码、相册、新增支持抓拍图片)Ba-ScanViewH

Ba-ScanViewH 是一款集成华为统一扫码服务的插件,采用component组件模式,可直接在uniapp界面直接引用,高宽可随意设置;支持抓拍照片。


扫码原生插件 - 最经典zxing版本

基于最经典zxing的插件Ba-Scanner-Zxing,操作简单,界面参照uniapp官方界面,支持相册、闪光灯等

收起阅读 »

unicloud什么时候可以支持脚本或命令一键部署云函数、云数据库和前端网页托管阿

uni-cloud

这样用 open clawe(龙虾)就可以对话让他不断的的修改,然后远程调试看下是否符合预期了

这样用 open clawe(龙虾)就可以对话让他不断的的修改,然后远程调试看下是否符合预期了

uniapp+vue3调用deepseek api小程序+h5+安卓端ai聊天助手【2026款】

ai vue3 uniapp

一个月爆肝迭代最新版uni-app+vue3+mphtml接入deepseek-v3.2跨三端流式ai应用。提供亮色+暗黑主题、新增深度思考链、katex数学公式、代码复制/高亮、链接/图片预览,支持运行到H5+小程序端+APP端。

主要增加功能

  1. 支持深度思考链(三端)✨
  2. 支持LaTex数学公式(三端)✨
  3. 支持Mermaid图表(H5)✨
  4. 支持代码块滚动粘性、横向滚动、行号、复制代码(三端)✨
  5. 支持表格、链接、图片预览(三端)✨

项目框架目录结构

> #### uniapp-vue3-deepseek跨端ai项目已经发布到我的原创作品小店,欢迎下载使用。
> 2026新版uniapp+deepseek+vue3跨端AI流式输出对话模板

以上就是uniapp接入deepseek跨三端ai项目的一些分享,感谢阅读于支持。

了解更多项目详细介绍,可以看看下面这篇文章。
uniapp+deepseek流式ai助理|uniapp+vue3对接deepseek三端Ai问答模板

热文推荐

Vite7+DeepSeek网页版Ai助手|vue3+arco网页web流式生成ai聊天问答系统
electron39-vue3ai电脑端AI模板|electron39+deepseek+vite7聊天ai应用
vite7+deepseek流式ai模板|vue3.5+deepseek3.2+markdown打字输出ai助手
Electron38-Wechat电脑端聊天|vite7+electron38仿微信桌面端聊天系统
Electron38-Vue3OS客户端OS系统|vite7+electron38+arco桌面os后台管理
electron38-admin桌面端后台|Electron38+Vue3+ElementPlus管理系统
最新版uniapp+vue3+uv-ui跨三端短视频+直播+聊天【H5+小程序+App端】
最新版uni-app+vue3+uv-ui跨三端仿微信app聊天应用【h5+小程序+app端】
Tauri2.9+Vue3桌面版OS系统|vite7+tauri2+arcoDesign电脑端os后台模板
Tauri2.8+Vue3聊天系统|vite7+tauri2+element-plus客户端仿微信聊天程序
Tauri2-Vite7Admin客户端管理后台|tauri2.9+vue3+element-plus后台系统
Flutter3-MacOS桌面OS系统|flutter3.32+window_manager客户端OS模板
最新研发flutter3.27+bitsdojo_window+getx客户端仿微信聊天Exe应用
最新版Flutter3.32+Dart3.8跨平台仿微信app聊天界面|朋友圈
flutter3-deepseek流式AI模板|Flutter3.27+Dio+DeepSeeek聊天ai助手

继续阅读 »

一个月爆肝迭代最新版uni-app+vue3+mphtml接入deepseek-v3.2跨三端流式ai应用。提供亮色+暗黑主题、新增深度思考链、katex数学公式、代码复制/高亮、链接/图片预览,支持运行到H5+小程序端+APP端。

主要增加功能

  1. 支持深度思考链(三端)✨
  2. 支持LaTex数学公式(三端)✨
  3. 支持Mermaid图表(H5)✨
  4. 支持代码块滚动粘性、横向滚动、行号、复制代码(三端)✨
  5. 支持表格、链接、图片预览(三端)✨

项目框架目录结构

> #### uniapp-vue3-deepseek跨端ai项目已经发布到我的原创作品小店,欢迎下载使用。
> 2026新版uniapp+deepseek+vue3跨端AI流式输出对话模板

以上就是uniapp接入deepseek跨三端ai项目的一些分享,感谢阅读于支持。

了解更多项目详细介绍,可以看看下面这篇文章。
uniapp+deepseek流式ai助理|uniapp+vue3对接deepseek三端Ai问答模板

热文推荐

Vite7+DeepSeek网页版Ai助手|vue3+arco网页web流式生成ai聊天问答系统
electron39-vue3ai电脑端AI模板|electron39+deepseek+vite7聊天ai应用
vite7+deepseek流式ai模板|vue3.5+deepseek3.2+markdown打字输出ai助手
Electron38-Wechat电脑端聊天|vite7+electron38仿微信桌面端聊天系统
Electron38-Vue3OS客户端OS系统|vite7+electron38+arco桌面os后台管理
electron38-admin桌面端后台|Electron38+Vue3+ElementPlus管理系统
最新版uniapp+vue3+uv-ui跨三端短视频+直播+聊天【H5+小程序+App端】
最新版uni-app+vue3+uv-ui跨三端仿微信app聊天应用【h5+小程序+app端】
Tauri2.9+Vue3桌面版OS系统|vite7+tauri2+arcoDesign电脑端os后台模板
Tauri2.8+Vue3聊天系统|vite7+tauri2+element-plus客户端仿微信聊天程序
Tauri2-Vite7Admin客户端管理后台|tauri2.9+vue3+element-plus后台系统
Flutter3-MacOS桌面OS系统|flutter3.32+window_manager客户端OS模板
最新研发flutter3.27+bitsdojo_window+getx客户端仿微信聊天Exe应用
最新版Flutter3.32+Dart3.8跨平台仿微信app聊天界面|朋友圈
flutter3-deepseek流式AI模板|Flutter3.27+Dio+DeepSeeek聊天ai助手

收起阅读 »

uniapp基于websocket封装的MQTT客户端,解决使用mqtt.js库在小程序端的兼容问题

websoket

前言

在物联网(IoT)开发中,MQTT协议因其轻量级、低带宽占用和高可靠性,成为了设备通信的首选协议。在Web端或Node.js环境中,开发者通常直接使用成熟的 mqtt.js 库即可轻松实现连接。然而,当我们将目光转向微信小程序或uni-app小程序端时,情况变得复杂起来。
微信小程序的运行环境(Mini Program Environment)有着严格的限制:
不支持TCP Socket:小程序无法直接建立TCP连接,必须通过WebSocket (wss://) 进行通信。
API差异:小程序使用 uni.connectSocket (或 wx.connectSocket) 而非标准的 WebSocket API,且对二进制数据处理(ArrayBuffer)有特定要求。
依赖限制:mqtt.js 默认依赖 Node.js 的 net 模块或浏览器的标准 WebSocket,直接引入往往会导致打包失败或运行时报错(如 process is not defined 或 WebSocket is not a constructor)。
虽然社区存在一些适配方案(如使用 mqtt/dist/mqtt.min.js 并手动注入 WebSocket 实现),但在处理二进制协议解析、心跳保活以及断线重连时,往往显得臃肿且难以调试。
本文将介绍一种轻量级、原生适配的解决方案:基于 uni-app 的 connectSocket API,从零封装一个专为小程序设计的 MQTT 客户端类 WechatMqttClient。它不依赖任何第三方重型库,完美支持 MQTT 3.1.1 协议,解决了二进制数据收发、UTF-8编码兼容及自动重连等核心痛点。

核心难点分析

在封装之前,我们需要明确小程序端实现 MQTT 的几个关键挑战:
协议包构建:MQTT是基于二进制的协议。我们需要手动构建 Fixed Header(固定头部)、Variable Header(可变头部)和 Payload(载荷)。特别是剩余长度(Remaining Length)的变长编码算法,是容易出错的地方。
字符编码:MQTT协议规定主题(Topic)和客户端ID(ClientID)必须使用 UTF-8 编码。JavaScript 字符串内部是 UTF-16,直接转换字节会导致中文乱码或协议解析失败。我们需要手写 UTF-8 编解码器。
心跳机制:小程序网络环境不稳定,且WebSocket连接在无数据传输时可能被运营商或系统切断。必须在应用层实现基于 PINGREQ/PINGRESP 的心跳检测。
数据流处理:uni.connectSocket 返回的是 ArrayBuffer,需要将其转换为 Uint8Array 进行位运算解析。

解决方案:WechatMqttClient 类设计

我们设计了一个名为 WechatMqttClient 的类,它屏蔽了底层的二进制操作,对外提供标准的 connect, publish, subscribe, on 等事件驱动接口。

  1. 核心架构
    该类主要包含以下模块:
    连接管理:封装 uni.connectSocket,处理连接建立、关闭和错误监听。
    协议编解码:实现 MQTT 3.1.1 的 CONNECT, PUBLISH, SUBSCRIBE, UNSUBSCRIBE, PINGREQ 等报文的构建与解析。
    UTF-8 工具集:独立的 encodeString 和 decodeUtf8 方法,确保多语言字符集正确传输。
    心跳与重连:内置指数退避算法的重连机制和定时心跳发送。
    事件总线:简单的发布订阅模式,用于通知上层业务逻辑(如 connect, message, error)。
  2. 关键代码实现解析

A. 建立 WebSocket 连接

不同于标准 WebSocket,我们使用 uni-app 的 API,并指定子协议为 mqtt。

connect() {  
  // ... 验证URL逻辑 ...  

  this.socketTask = uni.connectSocket({  
    url: this.url,  
    protocols: ['mqtt'], // 重要:告知服务器这是MQTT协议  
    success: (res) => console.log('WS创建成功', res),  
    fail: (error) => this.emit('error', error)  
  });  

  this.socketTask.onOpen(() => {  
    // 连接打开后,立即发送 MQTT CONNECT 报文  
    this.sendMqttConnectPacket();  
  });  

  this.socketTask.onMessage((res) => {  
    // 接收二进制数据并解析  
    this.handleMqttPacket(res.data);  
  });  

  // ... 监听 close 和 error 以触发重连 ...  
}

B. 构建 MQTT CONNECT 报文

这是握手的关键。我们需要严格按照 MQTT 3.1.1 规范组装字节流。

sendMqttConnectPacket() {  
  // 1. 准备 Payload (ClientID, Username, Password)  
  let payload = [];  
  const clientId = this.options.clientId || 'client_' + Date.now();  
  payload = payload.concat(this.encodeString(clientId));  

  if (this.options.username) payload = payload.concat(this.encodeString(this.options.username));  
  if (this.options.password) payload = payload.concat(this.encodeString(this.options.password));  

  // 2. 准备 Variable Header (Protocol Name, Level, Flags, KeepAlive)  
  let variableHeader = [];  
  variableHeader = variableHeader.concat(this.encodeString('MQTT'));  
  variableHeader.push(4); // Protocol Level 3.1.1  

  // Connect Flags: CleanSession(0x02), Username(0x80), Password(0x40)  
  let connectFlags = 0x02;   
  if (this.options.username) connectFlags |= 0x80;  
  if (this.options.password) connectFlags |= 0x40;  
  variableHeader.push(connectFlags);  

  // Keep Alive (2 bytes)  
  variableHeader.push((this.keepAlive >> 8) & 0xFF);  
  variableHeader.push(this.keepAlive & 0xFF);  

  // 3. 计算 Remaining Length 并编码  
  const remainingLength = variableHeader.length + payload.length;  
  const remainingLengthBytes = this.encodeRemainingLength(remainingLength);  

  // 4. 组装 Fixed Header (0x10 表示 CONNECT)  
  let mqttPacket = [0x10, ...remainingLengthBytes, ...variableHeader, ...payload];  

  // 5. 发送 ArrayBuffer  
  this.socketTask.send({  
    data: new Uint8Array(mqttPacket).buffer  
  });  
}

C. UTF-8 编码处理

这是很多开源库在小程序端失效的原因。JavaScript 的 charCodeAt 返回的是 UTF-16 代码单元,直接转为字节会破坏多字节字符。我们需要手动实现 UTF-8 转换逻辑(代码中已包含完整的 encodeString 和 decodeUtf8 方法,支持代理对 surrogate pairs)。

D. 消息解析与心跳

在 handleMqttPacket 中,我们读取第一个字节判断报文类型。
0x20 (CONNACK): 检查返回码,若为0则标记 connected = true 并启动心跳。
0x30 (PUBLISH): 解析 Topic 长度,提取 Topic 字符串,剩余部分即为 Payload。
0xD0 (PINGRESP): 收到服务端的心跳响应,确认连接健康。
心跳逻辑通过 setInterval 实现,每隔 keepAlive / 2 秒发送一次 PINGREQ。

完整的mqtt客户端代码

// 封装的MQTT客户端  
class WechatMqttClient {  
  constructor(url, options) {  
    this.url = url;  
    this.options = options;  
    this.socketTask = null;  
    this.connected = false;  
    this.eventHandlers = {};  
    this.messageId = 1;  
    this.reconnectAttempts = 0;  
    this.maxReconnectAttempts = 5;  
    this.reconnectTimer = null;  
    this.shouldReconnect = true;  
    this.keepAlive = options.keepAlive || 60;  
    this.heartbeatTimer = null;  
  }  

  connect() {  
    try {  
      // 重置重连标志  
      this.shouldReconnect = true;  

      // 验证URL格式  
      if (!this.url || (!this.url.startsWith('wss://') && !this.url.startsWith('ws://'))) {  
        const error = new Error('无效的WebSocket URL: ' + this.url + ',必须使用ws://或wss://协议');  
        console.error(error.message);  
        this.emit('error', error);  
        return;  
      }  

      // 先关闭现有连接  
      if (this.socketTask) {  
        this.socketTask.close();  
        this.socketTask = null;  
      }  

      // 使用微信小程序的connectSocket - 简化header  
      this.socketTask = uni.connectSocket({  
        url: this.url,  
        protocols: ['mqtt'],  
        success: (res) => {  
          console.log('WebSocket连接创建成功:', res);  
        },  
        fail: (error) => {  
          console.error('WebSocket连接创建失败:', error);  
          const errorMsg = error.errMsg || JSON.stringify(error);  
          this.emit('error', new Error('WebSocket连接创建失败: ' + errorMsg));  
        }  
      });  

      // 监听连接打开  
      this.socketTask.onOpen((res) => {  
        console.log('WebSocket连接已打开:', res);  
        // 发送MQTT CONNECT协议包  
        this.sendMqttConnectPacket();  
      });  

      // 监听消息接收  
      this.socketTask.onMessage((res) => {  
        console.log('收到WebSocket消息:', res.data);  
        this.handleMqttPacket(res.data);  
      });  

      // 监听连接关闭  
      this.socketTask.onClose((res) => {  
        console.log('WebSocket连接已关闭:', res);  
        this.connected = false;  
        this.emit('close');  
        this.attemptReconnect();  
      });  

      // 监听错误  
      this.socketTask.onError((error) => {  
        console.error('WebSocket错误:', error);  
        this.emit('error', new Error('WebSocket错误: ' + JSON.stringify(error)));  
        this.attemptReconnect();  
      });  

    } catch (error) {  
      console.error('MQTT连接错误:', error);  
      this.emit('error', error);  
    }  
  }  

  // 构建并发送标准的MQTT CONNECT协议包  
  sendMqttConnectPacket() {  
    if (!this.socketTask) return;  

    try {  
      // 构建标准的MQTT 3.1.1 CONNECT协议包  
      const protocolName = 'MQTT';  
      const protocolLevel = 4; // MQTT 3.1.1  
      const cleanSession = true;  

      // 计算可变头部和载荷长度  
      let payload = [];  

      // Client ID  
      const clientId = this.options.clientId || 'client_' + Date.now();  
      payload = payload.concat(this.encodeString(clientId));  

      // Username  
      if (this.options.username) {  
        payload = payload.concat(this.encodeString(this.options.username));  
      }  

      // Password  
      if (this.options.password) {  
        payload = payload.concat(this.encodeString(this.options.password));  
      }  

      // 计算可变头部长度  
      let variableHeader = [];  

      // Protocol Name  
      variableHeader = variableHeader.concat(this.encodeString(protocolName));  

      // Protocol Level  
      variableHeader.push(protocolLevel);  

      // Connect Flags  
      let connectFlags = 0;  
      if (cleanSession) connectFlags |= 0x02;  
      if (this.options.username) connectFlags |= 0x80;  
      if (this.options.password) connectFlags |= 0x40;  
      variableHeader.push(connectFlags);  

      // Keep Alive  
      variableHeader.push((this.keepAlive >> 8) & 0xFF);  
      variableHeader.push(this.keepAlive & 0xFF);  

      // 计算剩余长度  
      const remainingLength = variableHeader.length + payload.length;  
      const remainingLengthBytes = this.encodeRemainingLength(remainingLength);  

      // 构建完整的MQTT包  
      let mqttPacket = [];  

      // Fixed Header: CONNECT (0x10)  
      mqttPacket.push(0x10);  

      // Remaining Length  
      mqttPacket = mqttPacket.concat(remainingLengthBytes);  

      // Variable Header  
      mqttPacket = mqttPacket.concat(variableHeader);  

      // Payload  
      mqttPacket = mqttPacket.concat(payload);  

      // 转换为ArrayBuffer发送  
      const buffer = new Uint8Array(mqttPacket).buffer;  

      this.socketTask.send({  
        data: buffer,  
        success: () => {  
          console.log('MQTT CONNECT协议包发送成功');  
        },  
        fail: (error) => {  
          console.error('MQTT CONNECT协议包发送失败:', error);  
        }  
      });  
    } catch (error) {  
      console.error('发送MQTT CONNECT协议包错误:', error);  
    }  
  }  

  // 编码字符串为MQTT格式 (长度 + UTF-8字节)  
  encodeString(str) {  
    // 使用小程序兼容的UTF-8编码  
    let bytes = [];  
    for (let i = 0; i < str.length; i++) {  
      let charCode = str.charCodeAt(i);  
      if (charCode < 0x80) {  
        bytes.push(charCode);  
      } else if (charCode < 0x800) {  
        bytes.push(0xC0 | (charCode >> 6));  
        bytes.push(0x80 | (charCode & 0x3F));  
      } else if (charCode < 0xD800 || charCode >= 0xE000) {  
        bytes.push(0xE0 | (charCode >> 12));  
        bytes.push(0x80 | ((charCode >> 6) & 0x3F));  
        bytes.push(0x80 | (charCode & 0x3F));  
      } else {  
        // 处理代理对 (surrogate pair)  
        i++;  
        const nextCharCode = str.charCodeAt(i);  
        const codePoint = ((charCode - 0xD800) << 10) | (nextCharCode - 0xDC00) + 0x10000;  
        bytes.push(0xF0 | (codePoint >> 18));  
        bytes.push(0x80 | ((codePoint >> 12) & 0x3F));  
        bytes.push(0x80 | ((codePoint >> 6) & 0x3F));  
        bytes.push(0x80 | (codePoint & 0x3F));  
      }  
    }  

    let result = [];  
    result.push((bytes.length >> 8) & 0xFF);  
    result.push(bytes.length & 0xFF);  
    for (let i = 0; i < bytes.length; i++) {  
      result.push(bytes[i]);  
    }  
    return result;  
  }  

  // 解码UTF-8字节为字符串  
  decodeUtf8(bytes) {  
    let result = '';  
    let i = 0;  

    while (i < bytes.length) {  
      const byte1 = bytes[i];  

      if (byte1 < 0x80) {  
        // 单字节  
        result += String.fromCharCode(byte1);  
        i++;  
      } else if (byte1 < 0xE0) {  
        // 双字节  
        const byte2 = bytes[i + 1];  
        const charCode = ((byte1 & 0x1F) << 6) | (byte2 & 0x3F);  
        result += String.fromCharCode(charCode);  
        i += 2;  
      } else if (byte1 < 0xF0) {  
        // 三字节  
        const byte2 = bytes[i + 1];  
        const byte3 = bytes[i + 2];  
        const charCode = ((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F);  
        result += String.fromCharCode(charCode);  
        i += 3;  
      } else {  
        // 四字节  
        const byte2 = bytes[i + 1];  
        const byte3 = bytes[i + 2];  
        const byte4 = bytes[i + 3];  
        const codePoint = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F);  

        // 处理UTF-16代理对  
        if (codePoint >= 0x10000) {  
          const highSurrogate = ((codePoint - 0x10000) >> 10) + 0xD800;  
          const lowSurrogate = ((codePoint - 0x10000) & 0x3FF) + 0xDC00;  
          result += String.fromCharCode(highSurrogate, lowSurrogate);  
        } else {  
          result += String.fromCharCode(codePoint);  
        }  

        i += 4;  
      }  
    }  

    return result;  
  }  

  // 将字符串转换为UTF-8字节数组  
  stringToBytes(str) {  
    let bytes = [];  
    for (let i = 0; i < str.length; i++) {  
      let charCode = str.charCodeAt(i);  
      if (charCode < 0x80) {  
        bytes.push(charCode);  
      } else if (charCode < 0x800) {  
        bytes.push(0xC0 | (charCode >> 6));  
        bytes.push(0x80 | (charCode & 0x3F));  
      } else if (charCode < 0xD800 || charCode >= 0xE000) {  
        bytes.push(0xE0 | (charCode >> 12));  
        bytes.push(0x80 | ((charCode >> 6) & 0x3F));  
        bytes.push(0x80 | (charCode & 0x3F));  
      } else {  
        // 处理代理对 (surrogate pair)  
        i++;  
        const nextCharCode = str.charCodeAt(i);  
        const codePoint = ((charCode - 0xD800) << 10) | (nextCharCode - 0xDC00) + 0x10000;  
        bytes.push(0xF0 | (codePoint >> 18));  
        bytes.push(0x80 | ((codePoint >> 12) & 0x3F));  
        bytes.push(0x80 | ((codePoint >> 6) & 0x3F));  
        bytes.push(0x80 | (codePoint & 0x3F));  
      }  
    }  
    return bytes;  
  }  

  // 编码MQTT剩余长度  
  encodeRemainingLength(length) {  
    let result = [];  
    let digit;  
    do {  
      digit = length % 128;  
      length = Math.floor(length / 128);  
      if (length > 0) {  
        digit |= 0x80;  
      }  
      result.push(digit);  
    } while (length > 0);  
    return result;  
  }  

  // 处理MQTT协议包  
  handleMqttPacket(data) {  
    try {  
      let buffer;  
      if (data instanceof ArrayBuffer) {  
        buffer = new Uint8Array(data);  
      } else if (typeof data === 'string') {  
        // 如果是字符串,可能是测试数据  
        try {  
          const parsed = JSON.parse(data);  
          if (parsed.topic && parsed.payload) {  
            this.emit('message', parsed.topic, parsed.payload);  
            return;  
          }  
        } catch (e) {  
          // 不是JSON,继续处理  
        }  
        this.emit('message', 'unknown', data);  
        return;  
      } else {  
        this.emit('message', 'unknown', data);  
        return;  
      }  

      if (buffer.length < 2) {  
        console.warn('MQTT包太短');  
        return;  
      }  

      const fixedHeader = buffer[0];  
      const packetType = (fixedHeader >> 4) & 0x0F;  

      // CONNACK (0x20) - 连接确认  
      if (packetType === 2) {  
        if (buffer.length >= 4) {  
          const returnCode = buffer[3];  
          if (returnCode === 0) {  
            console.log('MQTT连接成功');  
            this.connected = true;  
            this.reconnectAttempts = 0;  
            // 启动心跳机制  
            this.startHeartbeat();  
            this.emit('connect');  
          } else {  
            console.error('MQTT连接失败,返回码:', returnCode);  
            this.emit('error', new Error('MQTT连接失败,返回码: ' + returnCode));  
          }  
        }  
        return;  
      }  

      // PUBLISH (0x30) - 消息发布  
      if (packetType === 3) {  
        // 解析剩余长度  
        let remainingLength = 0;  
        let multiplier = 1;  
        let index = 1;  
        let digit;  

        do {  
          digit = buffer[index++];  
          remainingLength += (digit & 0x7F) * multiplier;  
          multiplier *= 128;  
        } while ((digit & 0x80) !== 0);  

        // 提取可变头部和载荷  
        const variableHeaderAndPayload = buffer.subarray(index, index + remainingLength);  

        // 解析主题名称长度  
        const topicLength = (variableHeaderAndPayload[0] << 8) | variableHeaderAndPayload[1];  
        const topicNameBytes = variableHeaderAndPayload.subarray(2, 2 + topicLength);  

        // 解码主题名称  
        const topicName = this.decodeUtf8(topicNameBytes);  

        // 确定QoS级别  
        const qos = (fixedHeader >> 1) & 0x03;  

        // 跳过消息ID(如果QoS > 0)  
        let payloadStart = 2 + topicLength;  
        if (qos > 0) {  
          payloadStart += 2;  
        }  

        // 提取消息载荷  
        const payload = variableHeaderAndPayload.subarray(payloadStart);  

        // 发送消息载荷  
        this.emit('message', topicName, payload);  
        return;  
      }  

      // SUBACK (0x90) - 订阅确认  
      if (packetType === 9) {  
        console.log('收到MQTT SUBACK');  
        return;  
      }  

      // UNSUBACK (0xB0) - 取消订阅确认  
      if (packetType === 11) {  
        console.log('收到MQTT UNSUBACK');  
        return;  
      }  

      // PINGRESP (0xD0) - Ping响应  
      if (packetType === 13) {  
        console.log('收到MQTT PINGRESP');  
        return;  
      }  

      // 其他类型的包  
      console.log('收到其他类型的MQTT包:', packetType);  

    } catch (error) {  
      console.error('处理MQTT协议包错误:', error);  
      this.emit('error', error);  
    }  
  }  

  publish(topic, message, callback) {  
    if (this.connected && this.socketTask) {  
      try {  
        let payloadBytes;  

        // 处理消息载荷  
        if (typeof message === 'string') {  
          // 如果是十六进制字符串,转换为字节  
          if (/^[0-9A-Fa-f\s]+$/.test(message)) {  
            const cleanHex = message.replace(/\s/g, '');  
            payloadBytes = [];  
            for (let i = 0; i < cleanHex.length; i += 2) {  
              payloadBytes.push(parseInt(cleanHex.substr(i, 2), 16));  
            }  
          } else {  
            // 普通字符串  
            payloadBytes = this.stringToBytes(message);  
          }  
        } else if (message instanceof Uint8Array) {  
          payloadBytes = Array.from(message);  
        } else if (message instanceof ArrayBuffer) {  
          payloadBytes = Array.from(new Uint8Array(message));  
        } else {  
          // 其他类型转换为字符串  
          payloadBytes = this.stringToBytes(JSON.stringify(message));  
        }  

        // 构建MQTT PUBLISH协议包  
        let variableHeader = [];  

        // Topic  
        variableHeader = variableHeader.concat(this.encodeString(topic));  

        // QoS 0,没有messageId  

        // 计算剩余长度  
        const remainingLength = variableHeader.length + payloadBytes.length;  
        const remainingLengthBytes = this.encodeRemainingLength(remainingLength);  

        // 构建完整的MQTT包  
        let mqttPacket = [];  

        // Fixed Header: PUBLISH (0x30) - QoS 0, no retain  
        mqttPacket.push(0x30);  

        // Remaining Length  
        mqttPacket = mqttPacket.concat(remainingLengthBytes);  

        // Variable Header  
        mqttPacket = mqttPacket.concat(variableHeader);  

        // Payload  
        mqttPacket = mqttPacket.concat(payloadBytes);  

        // 转换为ArrayBuffer发送  
        const buffer = new Uint8Array(mqttPacket).buffer;  

        this.socketTask.send({  
          data: buffer,  
          success: () => {  
            if (callback) callback();  
          },  
          fail: (error) => {  
            console.error('MQTT PUBLISH协议包发送失败:', error);  
            if (callback) callback(error);  
          }  
        });  
      } catch (error) {  
        console.error('发布消息错误:', error);  
        if (callback) callback(error);  
      }  
    } else {  
      console.warn('WebSocket未连接,无法发布消息');  
      if (callback) callback(new Error('WebSocket未连接'));  
    }  
  }  

  subscribe(topic, callback) {  
    if (this.connected && this.socketTask) {  
      try {  
        // 构建MQTT SUBSCRIBE协议包  
        let variableHeader = [];  

        // Message ID  
        const messageId = this.messageId++;  
        variableHeader.push((messageId >> 8) & 0xFF);  
        variableHeader.push(messageId & 0xFF);  

        // Payload: Topic + QoS  
        let payload = [];  
        payload = payload.concat(this.encodeString(topic));  
        payload.push(0); // QoS 0  

        // 计算剩余长度  
        const remainingLength = variableHeader.length + payload.length;  
        const remainingLengthBytes = this.encodeRemainingLength(remainingLength);  

        // 构建完整的MQTT包  
        let mqttPacket = [];  

        // Fixed Header: SUBSCRIBE (0x82)  
        mqttPacket.push(0x82);  

        // Remaining Length  
        mqttPacket = mqttPacket.concat(remainingLengthBytes);  

        // Variable Header  
        mqttPacket = mqttPacket.concat(variableHeader);  

        // Payload  
        mqttPacket = mqttPacket.concat(payload);  

        // 转换为ArrayBuffer发送  
        const buffer = new Uint8Array(mqttPacket).buffer;  

        this.socketTask.send({  
          data: buffer,  
          success: () => {  
            if (callback) callback();  
          },  
          fail: (error) => {  
            console.error('MQTT SUBSCRIBE协议包发送失败:', error);  
            if (callback) callback(error);  
          }  
        });  
      } catch (error) {  
        console.error('订阅错误:', error);  
        if (callback) callback(error);  
      }  
    } else {  
      console.warn('WebSocket未连接,无法订阅');  
      if (callback) callback(new Error('WebSocket未连接'));  
    }  
  }  

  unsubscribe(topic) {  
    if (this.connected && this.socketTask) {  
      try {  
        // 构建MQTT UNSUBSCRIBE协议包  
        let variableHeader = [];  

        // Message ID  
        const messageId = this.messageId++;  
        variableHeader.push((messageId >> 8) & 0xFF);  
        variableHeader.push(messageId & 0xFF);  

        // Payload: Topic  
        let payload = [];  
        payload = payload.concat(this.encodeString(topic));  

        // 计算剩余长度  
        const remainingLength = variableHeader.length + payload.length;  
        const remainingLengthBytes = this.encodeRemainingLength(remainingLength);  

        // 构建完整的MQTT包  
        let mqttPacket = [];  

        // Fixed Header: UNSUBSCRIBE (0xA2)  
        mqttPacket.push(0xA2);  

        // Remaining Length  
        mqttPacket = mqttPacket.concat(remainingLengthBytes);  

        // Variable Header  
        mqttPacket = mqttPacket.concat(variableHeader);  

        // Payload  
        mqttPacket = mqttPacket.concat(payload);  

        // 转换为ArrayBuffer发送  
        const buffer = new Uint8Array(mqttPacket).buffer;  

        this.socketTask.send({  
          data: buffer,  
          fail: (error) => {  
            console.error('MQTT UNSUBSCRIBE协议包发送失败:', error);  
          }  
        });  
      } catch (error) {  
        console.error('取消订阅错误:', error);  
      }  
    }  
  }  

  sendPingreq() {  
    if (this.connected && this.socketTask) {  
      try {  
        // 构建MQTT PINGREQ协议包  
        const pingreqPacket = [0xC0, 0x00]; // PINGREQ (0xC0) + 剩余长度 0  

        const buffer = new Uint8Array(pingreqPacket).buffer;  

        console.log('发送MQTT PINGREQ');  

        this.socketTask.send({  
          data: buffer,  
          success: () => {  
            console.log('MQTT PINGREQ发送成功');  
          },  
          fail: (error) => {  
            console.error('MQTT PINGREQ发送失败:', error);  
          }  
        });  
      } catch (error) {  
        console.error('发送MQTT PINGREQ错误:', error);  
      }  
    }  
  }  

  startHeartbeat() {  
    // 清除现有的心跳定时器  
    if (this.heartbeatTimer) {  
      clearInterval(this.heartbeatTimer);  
      this.heartbeatTimer = null;  
    }  

    // 每keepAlive/2秒发送一次PINGREQ  
    const heartbeatInterval = (this.keepAlive * 1000) / 2;  

    console.log('启动心跳机制,间隔:', heartbeatInterval, 'ms');  

    this.heartbeatTimer = setInterval(() => {  
      this.sendPingreq();  
    }, heartbeatInterval);  
  }  

  stopHeartbeat() {  
    if (this.heartbeatTimer) {  
      clearInterval(this.heartbeatTimer);  
      this.heartbeatTimer = null;  
      console.log('停止心跳机制');  
    }  
  }  

  end() {  
    // 停止重连  
    this.shouldReconnect = false;  

    // 停止心跳  
    this.stopHeartbeat();  

    // 清除重连定时器  
    if (this.reconnectTimer) {  
      clearTimeout(this.reconnectTimer);  
      this.reconnectTimer = null;  
    }  

    if (this.socketTask) {  
      this.socketTask.close({  
        success: () => {  
          console.log('WebSocket连接已关闭');  
        }  
      });  
      this.connected = false;  
    }  
  }  

  attemptReconnect() {  
    // 检查是否应该重连  
    if (!this.shouldReconnect) {  
      console.log('已停止重连');  
      return;  
    }  

    // 停止当前心跳  
    this.stopHeartbeat();  

    if (this.reconnectAttempts < this.maxReconnectAttempts) {  
      this.reconnectAttempts++;  
      const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);  

      console.log(`尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts}),延迟: ${delay}ms`);  

      this.reconnectTimer = setTimeout(() => {  
        this.connect();  
      }, delay);  
    } else {  
      console.error('达到最大重连次数,停止重连');  
      this.emit('error', new Error('达到最大重连次数'));  
    }  
  }  

  on(event, handler) {  
    if (!this.eventHandlers[event]) {  
      this.eventHandlers[event] = [];  
    }  
    this.eventHandlers[event].push(handler);  
  }  

  emit(event, ...args) {  
    if (this.eventHandlers[event]) {  
      this.eventHandlers[event].forEach(handler => handler(...args));  
    }  
  }  
}  

// 创建MQTT连接函数  
export const createMqttConnection = (url, options) => {  
  return new WechatMqttClient(url, options);  
};

在 uni-app 中使用

下面展示如何在 uni-app (Vue 3 setup 语法糖) 中集成该客户端,实现一个设备状态监控弹窗。

1. 引入与配置

import { createMqttConnection } from '@/utils/miniMqtt.js'; // 引入封装好的类  

// 配置项  
const mqttUrl = 'wss://your-mqtt-broker.com/mqtt';  
const clientId = "emqx_test_" + Math.random().toString(16).substring(2, 8);  
const username = "local_cabinet";  
const password = "hUwRNRvjcaGwsuU3";

2. 初始化连接与事件监听

在组件挂载或用户操作时初始化:

let client = null;  

const initMqtt = () => {  
  if (client) client.end(); // 清理旧连接  

  client = createMqttConnection(mqttUrl, {  
    clientId,  
    username,  
    password,  
    keepAlive: 60  
  });  

  // 监听连接成功  
  client.on('connect', () => {  
    console.log('MQTT连接成功');  
    addDebugMessage("连接成功", "success");  
    // 连接成功后立即订阅主题  
    client.subscribe('wash/hy/device001/pubmsg', (err) => {  
      if(!err) addDebugMessage("订阅成功", "success");  
    });  
  });  

  // 监听错误  
  client.on('error', (err) => {  
    console.error(err);  
    addDebugMessage("发生错误:" + err.message, "error");  
  });  

  // 监听消息  
  client.on('message', (topic, message) => {  
    handleMqttMessage(topic, message);  
  });  

  client.connect();  
};

3. 消息收发处理

发送消息(支持 Hex 字符串):
很多硬件设备通信使用十六进制指令。我们的封装类在 publish 方法中做了特殊处理:如果检测到传入的是纯十六进制字符串,会自动转换为 Uint8Array 发送。

const publishMessage = (topic, message) => {  
  if (!client || !client.connected) return;  

  // 假设 message 是 "01 03 00 00 00 0A C4 0B" 这样的十六进制字符串  
  // 或者普通文本 "Hello"  
  client.publish(topic, message, (err) => {  
    if (err) {  
      uni.showToast({ title: '发送失败', icon: 'none' });  
    } else {  
      addDebugMessage("指令已发送", "primary");  
    }  
  });  
};

接收消息解析:

接收到的 message 通常是 Uint8Array。我们可以将其转换为十六进制字符串以便调试或发送给后端解析。

const handleMqttMessage = (topic, message) => {  
  // 将 Uint8Array 转为 Hex 字符串显示  
  let hexString = "";  
  if (message instanceof Uint8Array) {  
    for (let i = 0; i < message.length; i++) {  
      hexString += message[i].toString(16).toUpperCase().padStart(2, "0") + " ";  
    }  
  }  

  console.log(`收到消息 [${topic}]: ${hexString}`);  
  addDebugMessage(`收到: ${hexString}`, "default");  

  // 此处可调用后端API解析具体的业务含义  
};

4. 完整页面示例结构

<template>  
    <view>  
        <uv-popup ref="popupRef" mode="center" round="10" :closeable="true" @change="change" :safeAreaInsetBottom="false">  
            <view class="w-670 p-40 text-center flex flex-col">  
                <view class="fs-36" :style="{ color:theme.primaryFontColor}">通信状态</view>  
                <scroll-view ref="scrollRef" :scroll-top="scrollTop" scroll-y="true" class="w-full h-420 mt-60">  
                    <view class="scroll-view">  
                         <view v-for="(item,index) in messageInfo" :key="index" class="scroll-item">  
                             <text class="fs-28 text-left" :style="{ color:theme.labelColor}">{{item.time}}</text>  
                             <text class="fs-32 text-left mt-10" :style="{color:item.type}">{{item.message}}</text>  
                         </view>  
                    </view>  
                </scroll-view>  
                <view class="mt-60">  
                    <uv-button   
                        :loading="loading"   
                        text="查询设备状态"   
                        @click="sendMsg"  
                        :customStyle="{  
                            width:'590rpx',  
                            height:'112rpx',  
                            borderRadius:'16rpx',  
                            backgroundColor:theme.primaryColor,  
                            color:theme.whiteColor  
                        }"  
                    ></uv-button>  
                </view>  
            </view>  
        </uv-popup>  
    </view>  
</template>  

<script setup>  
import { ref,nextTick,getCurrentInstance } from 'vue'  
import { getQueryCommandAPI, getQueryCommandTextAPI } from '@/api/device';  
import config from '@/config/config';  
import { createMqttConnection } from '@/utils/miniMqtt.js';  
import { useAppStore } from '@/store/app.js';  

const theme = useAppStore().theme;  
const instance = getCurrentInstance();  

let popupRef = ref(null)  
let scrollRef = ref(null)  
let deviceInfo = ref(null)  
let loading = ref(false)  
let messageInfo = ref([])  
let scrollTop = ref(0)  
let subscribeTopic = "";  
let publishTopic = "";  
let currentPublishTopic = "";  
let currentSubscribeTopic = "";  
let ctimer = null;  
let client = null;  
// MQTT 配置  
const mqttUrl = config.api.wss;  
const clientId = "emqx_test_" + Math.random().toString(16).substring(2, 8);  
const username = "local_cabinet";  
const password = "hUwRNRvjcaGwsuU3";  

let open = ()=>{  
    popupRef.value && popupRef.value.open()  
}  
let openMqtt = (params)=>{  
    deviceInfo.value = params  
     // 生成订阅和发布主题  
    let prefix = params.transmission == 1 ? "wash/hy/" : "wash/ts/";  
    subscribeTopic = prefix + params.device_no + "/pubmsg";  
    publishTopic = prefix + params.device_no + "/submsg";  
    currentPublishTopic = publishTopic;  
    open()  
    initMqtt()  
}  

// 初始化MQTT  
let initMqtt = () => {  
  loading.value = true;  

  if (client) {  
    client.end();  
    console.log("已断开连接");  
  }  

  addDebugMessage("MQTT正在尝试连接...", theme.primaryFontColor);  

  try {  
    // 使用封装的MQTT客户端  
    client = createMqttConnection(mqttUrl, {  
      clientId,  
      username,  
      password,  
      rejectUnauthorized: false,  
    });  

    // 设置事件监听  
    client.on('connect', () => {  
      console.log('MQTT连接成功');  
      addDebugMessage("MQTT连接成功", theme.successColor);  
      loading.value = false;  
      // 订阅主题  
      addSubscribeTopic(subscribeTopic);  
    });  

    client.on('reconnect', () => {  
      console.log("MQTT尝试重连...");  
      addDebugMessage("MQTT正在重连...", theme.primaryFontColor);  
    });  

    client.on('error', (error) => {  
      console.error("MQTT连接错误:", error);  
      addDebugMessage("MQTT连接错误:" + error.message, theme.errorColor);  
      loading.value = false;  
    });  

    client.on('message', (topic, message) => {  
      // 处理接收到的消息  
      handleMqttMessage(topic, message);  
    });  

    // 开始连接  
    client.connect();  

  } catch (error) {  
    console.error("MQTT初始化失败:", error);  
    addDebugMessage("MQTT初始化失败: " + error.message, theme.errorColor);  
    loading.value = false;  
  }  
};  

// 处理MQTT消息  
let handleMqttMessage = (topic, message) => {  
  try {  

    // 转换为16进制显示  
    const hexMessage = messageToHexString(message);  

    // 显示接收消息  
    getQueryCommandTextAPI({  
      command: hexMessage,  
      agreement: deviceInfo.value.agreement  
    }).then(res => {  
      loading.value = false;  
      if (res.data) {  
        let data = res.data;  
        clearTimeout(ctimer);  
        addDebugMessage(`收到消息:${data.run_status_text ? data.run_status_text : ''} ${data.run_fault == "[]" ? "" : data.run_fault}`, theme.primaryFontColor);  
      }  
    }).catch(error => {  
      clearTimeout(ctimer);  
      loading.value = false;  
      addDebugMessage(error.msg, theme.errorColor);  
    });  
  } catch (error) {  
    loading.value = false;  
    addDebugMessage("消息解析错误: " + error.message, theme.errorColor);  
  }  
};  

let sendMsg = () => {  
  loading.value = true;  
  getQueryCommandAPI({  
    deviceNo: deviceInfo.value.deviceNo,  
    agreement: deviceInfo.value.agreement  
  }).then(res => {  
    if (res.data) {  
      let queryCommand = res.data;  
      // 验证是否为有效的十六进制字符串  
      if (isValidHexString(queryCommand)) {  
        publishMessage(currentPublishTopic, queryCommand);  
        uni.$uv.toast("查询命令发送成功");  
      } else {  
        addDebugMessage("警告: 查询命令不是有效的十六进制格式,按文本发送", theme.errorColor);  
        publishMessage(currentPublishTopic, queryCommand);  
        uni.$uv.toast("查询命令发送成功");  
      }  
    } else {  
      uni.$uv.toast("获取查询命令失败或不支持的协议");  
    }  
  });  
  startCoutdown();  
};  

let startCoutdown = () => {  
  clearTimeout(ctimer);  
  ctimer = setTimeout(() => {  
    loading.value = false;  
    addDebugMessage("查询失败", theme.errorColor);  
  }, 5000);  
};  

let scrollToBottom = () => {  
  nextTick(() => {  
         const query = uni.createSelectorQuery().in(instance.proxy);  
            query.select('.scroll-view').boundingClientRect(res => {  
                scrollTop.value = res.height  
        }).exec();  
  });  
};  

// 订阅主题  
let addSubscribeTopic = topic => {  
  if (currentSubscribeTopic && client) {  
    client.unsubscribe(currentSubscribeTopic);  
  }  

  if (topic && client && client.connected) {  
    client.subscribe(topic, (err) => {  
      if (!err) {  
        currentSubscribeTopic = topic;  
        addDebugMessage("订阅成功 ", theme.successColor);  
      } else {  
        addDebugMessage("订阅失败: " + err.message, theme.errorColor);  
      }  
    });  
  }  
};  

// 发布消息  
let publishMessage = (topic, message) => {  
  if (client && client.connected) {  
    let messageToSend;  

    // 检查是否为十六进制字符串  
    if (typeof message === "string" && isValidHexString(message)) {  
      try {  
        // 转换为二进制数据  
        messageToSend = hexStringToBuffer(message);  
      } catch (error) {  
        addDebugMessage("十六进制转换失败: " + error.message, theme.errorColor);  
        return;  
      }  
    } else {  
      // 普通字符串消息  
      messageToSend = message;  
      addDebugMessage("发送文本消息: " + message, theme.primaryFontColor);  
    }  

    client.publish(topic, messageToSend, (err) => {  
      if (err) {  
        addDebugMessage("消息发送失败: " + err.message, theme.errorColor);  
      } else {  
        addDebugMessage("消息发送成功", theme.successColor);  
      }  
    });  
  } else {  
    addDebugMessage("MQTT未连接,无法发送消息", theme.errorColor);  
  }  
};  

// 将十六进制字符串转换为二进制数据  
let hexStringToBuffer = hexString => {  
  // 移除空格和非十六进制字符  
  const cleanHex = hexString.replace(/[^0-9A-Fa-f]/g, "");  

  // 确保是偶数长度  
  const evenLengthHex = cleanHex.length % 2 === 0 ? cleanHex : "0" + cleanHex;  

  // 创建Uint8Array  
  const bytes = new Uint8Array(evenLengthHex.length / 2);  

  for (let i = 0; i < evenLengthHex.length; i += 2) {  
    bytes[i / 2] = parseInt(evenLengthHex.substr(i, 2), 16);  
  }  

  return bytes;  
};  

let addDebugMessage = (message, type) => {  
  messageInfo.value.push({  
    message,  
    type,  
    time: uni.$uv.date(new Date(),'hh:MM:ss')  
  });  
  scrollToBottom();  
};  

// 验证十六进制字符串格式  
let isValidHexString = hexString => {  
  const cleanHex = hexString.replace(/\s+/g, "");  
  return /^[0-9A-Fa-f]+$/.test(cleanHex);  
};  

// 将消息转换为16进制字符串的函数  
let messageToHexString = message => {  
  let hexString = "";  
  if (message instanceof Uint8Array) {  
    // 如果是Uint8Array  
    for (let i = 0; i < message.length; i++) {  
      hexString += message[i].toString(16).toUpperCase().padStart(2, "0") + " ";  
    }  
  } else if (typeof message === "string") {  
    // 如果是字符串,转换每个字符的字节值  
    for (let i = 0; i < message.length; i++) {  
      hexString += message.charCodeAt(i).toString(16).toUpperCase().padStart(2, "0") + " ";  
    }  
  } else if (message && typeof message === "object" && message.constructor && message.constructor.name === "Buffer") {  
    // 如果是Buffer对象(但在浏览器中通常不会遇到)  
    for (let i = 0; i < message.length; i++) {  
      hexString += message[i].toString(16).toUpperCase().padStart(2, "0") + " ";  
    }  
  } else if (message instanceof ArrayBuffer) {  
    // 如果是ArrayBuffer  
    const uint8Array = new Uint8Array(message);  
    for (let i = 0; i < uint8Array.length; i++) {  
      hexString += uint8Array[i].toString(16).toUpperCase().padStart(2, "0") + " ";  
    }  
  } else {  
    // 其他情况,尝试转换为字符串处理  
    const str = message.toString();  
    for (let i = 0; i < str.length; i++) {  
      hexString += str.charCodeAt(i).toString(16).toUpperCase().padStart(2, "0") + " ";  
    }  
  }  

  return hexString.trim();  
};  

let change = (e)=>{  
    if(!e.show){  
         if (client) {  
            // 先取消订阅,再关闭连接  
            if (currentSubscribeTopic) {  
              client.unsubscribe(currentSubscribeTopic);  
            }  
            client.end();  
          }  
          messageInfo.value = [];  
          currentSubscribeTopic = "";  
          currentPublishTopic = "";  
    }  
}  

defineExpose({  
    openMqtt  
})  

</script>  

<style scoped>  
    .scroll-item {  
      display: flex;  
      flex-direction: column;  
      width: 100%;  
      padding: 30rpx 0;  
      border-bottom: 2rpx solid #F5F5F5;  
    }  
</style>
继续阅读 »

前言

在物联网(IoT)开发中,MQTT协议因其轻量级、低带宽占用和高可靠性,成为了设备通信的首选协议。在Web端或Node.js环境中,开发者通常直接使用成熟的 mqtt.js 库即可轻松实现连接。然而,当我们将目光转向微信小程序或uni-app小程序端时,情况变得复杂起来。
微信小程序的运行环境(Mini Program Environment)有着严格的限制:
不支持TCP Socket:小程序无法直接建立TCP连接,必须通过WebSocket (wss://) 进行通信。
API差异:小程序使用 uni.connectSocket (或 wx.connectSocket) 而非标准的 WebSocket API,且对二进制数据处理(ArrayBuffer)有特定要求。
依赖限制:mqtt.js 默认依赖 Node.js 的 net 模块或浏览器的标准 WebSocket,直接引入往往会导致打包失败或运行时报错(如 process is not defined 或 WebSocket is not a constructor)。
虽然社区存在一些适配方案(如使用 mqtt/dist/mqtt.min.js 并手动注入 WebSocket 实现),但在处理二进制协议解析、心跳保活以及断线重连时,往往显得臃肿且难以调试。
本文将介绍一种轻量级、原生适配的解决方案:基于 uni-app 的 connectSocket API,从零封装一个专为小程序设计的 MQTT 客户端类 WechatMqttClient。它不依赖任何第三方重型库,完美支持 MQTT 3.1.1 协议,解决了二进制数据收发、UTF-8编码兼容及自动重连等核心痛点。

核心难点分析

在封装之前,我们需要明确小程序端实现 MQTT 的几个关键挑战:
协议包构建:MQTT是基于二进制的协议。我们需要手动构建 Fixed Header(固定头部)、Variable Header(可变头部)和 Payload(载荷)。特别是剩余长度(Remaining Length)的变长编码算法,是容易出错的地方。
字符编码:MQTT协议规定主题(Topic)和客户端ID(ClientID)必须使用 UTF-8 编码。JavaScript 字符串内部是 UTF-16,直接转换字节会导致中文乱码或协议解析失败。我们需要手写 UTF-8 编解码器。
心跳机制:小程序网络环境不稳定,且WebSocket连接在无数据传输时可能被运营商或系统切断。必须在应用层实现基于 PINGREQ/PINGRESP 的心跳检测。
数据流处理:uni.connectSocket 返回的是 ArrayBuffer,需要将其转换为 Uint8Array 进行位运算解析。

解决方案:WechatMqttClient 类设计

我们设计了一个名为 WechatMqttClient 的类,它屏蔽了底层的二进制操作,对外提供标准的 connect, publish, subscribe, on 等事件驱动接口。

  1. 核心架构
    该类主要包含以下模块:
    连接管理:封装 uni.connectSocket,处理连接建立、关闭和错误监听。
    协议编解码:实现 MQTT 3.1.1 的 CONNECT, PUBLISH, SUBSCRIBE, UNSUBSCRIBE, PINGREQ 等报文的构建与解析。
    UTF-8 工具集:独立的 encodeString 和 decodeUtf8 方法,确保多语言字符集正确传输。
    心跳与重连:内置指数退避算法的重连机制和定时心跳发送。
    事件总线:简单的发布订阅模式,用于通知上层业务逻辑(如 connect, message, error)。
  2. 关键代码实现解析

A. 建立 WebSocket 连接

不同于标准 WebSocket,我们使用 uni-app 的 API,并指定子协议为 mqtt。

connect() {  
  // ... 验证URL逻辑 ...  

  this.socketTask = uni.connectSocket({  
    url: this.url,  
    protocols: ['mqtt'], // 重要:告知服务器这是MQTT协议  
    success: (res) => console.log('WS创建成功', res),  
    fail: (error) => this.emit('error', error)  
  });  

  this.socketTask.onOpen(() => {  
    // 连接打开后,立即发送 MQTT CONNECT 报文  
    this.sendMqttConnectPacket();  
  });  

  this.socketTask.onMessage((res) => {  
    // 接收二进制数据并解析  
    this.handleMqttPacket(res.data);  
  });  

  // ... 监听 close 和 error 以触发重连 ...  
}

B. 构建 MQTT CONNECT 报文

这是握手的关键。我们需要严格按照 MQTT 3.1.1 规范组装字节流。

sendMqttConnectPacket() {  
  // 1. 准备 Payload (ClientID, Username, Password)  
  let payload = [];  
  const clientId = this.options.clientId || 'client_' + Date.now();  
  payload = payload.concat(this.encodeString(clientId));  

  if (this.options.username) payload = payload.concat(this.encodeString(this.options.username));  
  if (this.options.password) payload = payload.concat(this.encodeString(this.options.password));  

  // 2. 准备 Variable Header (Protocol Name, Level, Flags, KeepAlive)  
  let variableHeader = [];  
  variableHeader = variableHeader.concat(this.encodeString('MQTT'));  
  variableHeader.push(4); // Protocol Level 3.1.1  

  // Connect Flags: CleanSession(0x02), Username(0x80), Password(0x40)  
  let connectFlags = 0x02;   
  if (this.options.username) connectFlags |= 0x80;  
  if (this.options.password) connectFlags |= 0x40;  
  variableHeader.push(connectFlags);  

  // Keep Alive (2 bytes)  
  variableHeader.push((this.keepAlive >> 8) & 0xFF);  
  variableHeader.push(this.keepAlive & 0xFF);  

  // 3. 计算 Remaining Length 并编码  
  const remainingLength = variableHeader.length + payload.length;  
  const remainingLengthBytes = this.encodeRemainingLength(remainingLength);  

  // 4. 组装 Fixed Header (0x10 表示 CONNECT)  
  let mqttPacket = [0x10, ...remainingLengthBytes, ...variableHeader, ...payload];  

  // 5. 发送 ArrayBuffer  
  this.socketTask.send({  
    data: new Uint8Array(mqttPacket).buffer  
  });  
}

C. UTF-8 编码处理

这是很多开源库在小程序端失效的原因。JavaScript 的 charCodeAt 返回的是 UTF-16 代码单元,直接转为字节会破坏多字节字符。我们需要手动实现 UTF-8 转换逻辑(代码中已包含完整的 encodeString 和 decodeUtf8 方法,支持代理对 surrogate pairs)。

D. 消息解析与心跳

在 handleMqttPacket 中,我们读取第一个字节判断报文类型。
0x20 (CONNACK): 检查返回码,若为0则标记 connected = true 并启动心跳。
0x30 (PUBLISH): 解析 Topic 长度,提取 Topic 字符串,剩余部分即为 Payload。
0xD0 (PINGRESP): 收到服务端的心跳响应,确认连接健康。
心跳逻辑通过 setInterval 实现,每隔 keepAlive / 2 秒发送一次 PINGREQ。

完整的mqtt客户端代码

// 封装的MQTT客户端  
class WechatMqttClient {  
  constructor(url, options) {  
    this.url = url;  
    this.options = options;  
    this.socketTask = null;  
    this.connected = false;  
    this.eventHandlers = {};  
    this.messageId = 1;  
    this.reconnectAttempts = 0;  
    this.maxReconnectAttempts = 5;  
    this.reconnectTimer = null;  
    this.shouldReconnect = true;  
    this.keepAlive = options.keepAlive || 60;  
    this.heartbeatTimer = null;  
  }  

  connect() {  
    try {  
      // 重置重连标志  
      this.shouldReconnect = true;  

      // 验证URL格式  
      if (!this.url || (!this.url.startsWith('wss://') && !this.url.startsWith('ws://'))) {  
        const error = new Error('无效的WebSocket URL: ' + this.url + ',必须使用ws://或wss://协议');  
        console.error(error.message);  
        this.emit('error', error);  
        return;  
      }  

      // 先关闭现有连接  
      if (this.socketTask) {  
        this.socketTask.close();  
        this.socketTask = null;  
      }  

      // 使用微信小程序的connectSocket - 简化header  
      this.socketTask = uni.connectSocket({  
        url: this.url,  
        protocols: ['mqtt'],  
        success: (res) => {  
          console.log('WebSocket连接创建成功:', res);  
        },  
        fail: (error) => {  
          console.error('WebSocket连接创建失败:', error);  
          const errorMsg = error.errMsg || JSON.stringify(error);  
          this.emit('error', new Error('WebSocket连接创建失败: ' + errorMsg));  
        }  
      });  

      // 监听连接打开  
      this.socketTask.onOpen((res) => {  
        console.log('WebSocket连接已打开:', res);  
        // 发送MQTT CONNECT协议包  
        this.sendMqttConnectPacket();  
      });  

      // 监听消息接收  
      this.socketTask.onMessage((res) => {  
        console.log('收到WebSocket消息:', res.data);  
        this.handleMqttPacket(res.data);  
      });  

      // 监听连接关闭  
      this.socketTask.onClose((res) => {  
        console.log('WebSocket连接已关闭:', res);  
        this.connected = false;  
        this.emit('close');  
        this.attemptReconnect();  
      });  

      // 监听错误  
      this.socketTask.onError((error) => {  
        console.error('WebSocket错误:', error);  
        this.emit('error', new Error('WebSocket错误: ' + JSON.stringify(error)));  
        this.attemptReconnect();  
      });  

    } catch (error) {  
      console.error('MQTT连接错误:', error);  
      this.emit('error', error);  
    }  
  }  

  // 构建并发送标准的MQTT CONNECT协议包  
  sendMqttConnectPacket() {  
    if (!this.socketTask) return;  

    try {  
      // 构建标准的MQTT 3.1.1 CONNECT协议包  
      const protocolName = 'MQTT';  
      const protocolLevel = 4; // MQTT 3.1.1  
      const cleanSession = true;  

      // 计算可变头部和载荷长度  
      let payload = [];  

      // Client ID  
      const clientId = this.options.clientId || 'client_' + Date.now();  
      payload = payload.concat(this.encodeString(clientId));  

      // Username  
      if (this.options.username) {  
        payload = payload.concat(this.encodeString(this.options.username));  
      }  

      // Password  
      if (this.options.password) {  
        payload = payload.concat(this.encodeString(this.options.password));  
      }  

      // 计算可变头部长度  
      let variableHeader = [];  

      // Protocol Name  
      variableHeader = variableHeader.concat(this.encodeString(protocolName));  

      // Protocol Level  
      variableHeader.push(protocolLevel);  

      // Connect Flags  
      let connectFlags = 0;  
      if (cleanSession) connectFlags |= 0x02;  
      if (this.options.username) connectFlags |= 0x80;  
      if (this.options.password) connectFlags |= 0x40;  
      variableHeader.push(connectFlags);  

      // Keep Alive  
      variableHeader.push((this.keepAlive >> 8) & 0xFF);  
      variableHeader.push(this.keepAlive & 0xFF);  

      // 计算剩余长度  
      const remainingLength = variableHeader.length + payload.length;  
      const remainingLengthBytes = this.encodeRemainingLength(remainingLength);  

      // 构建完整的MQTT包  
      let mqttPacket = [];  

      // Fixed Header: CONNECT (0x10)  
      mqttPacket.push(0x10);  

      // Remaining Length  
      mqttPacket = mqttPacket.concat(remainingLengthBytes);  

      // Variable Header  
      mqttPacket = mqttPacket.concat(variableHeader);  

      // Payload  
      mqttPacket = mqttPacket.concat(payload);  

      // 转换为ArrayBuffer发送  
      const buffer = new Uint8Array(mqttPacket).buffer;  

      this.socketTask.send({  
        data: buffer,  
        success: () => {  
          console.log('MQTT CONNECT协议包发送成功');  
        },  
        fail: (error) => {  
          console.error('MQTT CONNECT协议包发送失败:', error);  
        }  
      });  
    } catch (error) {  
      console.error('发送MQTT CONNECT协议包错误:', error);  
    }  
  }  

  // 编码字符串为MQTT格式 (长度 + UTF-8字节)  
  encodeString(str) {  
    // 使用小程序兼容的UTF-8编码  
    let bytes = [];  
    for (let i = 0; i < str.length; i++) {  
      let charCode = str.charCodeAt(i);  
      if (charCode < 0x80) {  
        bytes.push(charCode);  
      } else if (charCode < 0x800) {  
        bytes.push(0xC0 | (charCode >> 6));  
        bytes.push(0x80 | (charCode & 0x3F));  
      } else if (charCode < 0xD800 || charCode >= 0xE000) {  
        bytes.push(0xE0 | (charCode >> 12));  
        bytes.push(0x80 | ((charCode >> 6) & 0x3F));  
        bytes.push(0x80 | (charCode & 0x3F));  
      } else {  
        // 处理代理对 (surrogate pair)  
        i++;  
        const nextCharCode = str.charCodeAt(i);  
        const codePoint = ((charCode - 0xD800) << 10) | (nextCharCode - 0xDC00) + 0x10000;  
        bytes.push(0xF0 | (codePoint >> 18));  
        bytes.push(0x80 | ((codePoint >> 12) & 0x3F));  
        bytes.push(0x80 | ((codePoint >> 6) & 0x3F));  
        bytes.push(0x80 | (codePoint & 0x3F));  
      }  
    }  

    let result = [];  
    result.push((bytes.length >> 8) & 0xFF);  
    result.push(bytes.length & 0xFF);  
    for (let i = 0; i < bytes.length; i++) {  
      result.push(bytes[i]);  
    }  
    return result;  
  }  

  // 解码UTF-8字节为字符串  
  decodeUtf8(bytes) {  
    let result = '';  
    let i = 0;  

    while (i < bytes.length) {  
      const byte1 = bytes[i];  

      if (byte1 < 0x80) {  
        // 单字节  
        result += String.fromCharCode(byte1);  
        i++;  
      } else if (byte1 < 0xE0) {  
        // 双字节  
        const byte2 = bytes[i + 1];  
        const charCode = ((byte1 & 0x1F) << 6) | (byte2 & 0x3F);  
        result += String.fromCharCode(charCode);  
        i += 2;  
      } else if (byte1 < 0xF0) {  
        // 三字节  
        const byte2 = bytes[i + 1];  
        const byte3 = bytes[i + 2];  
        const charCode = ((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F);  
        result += String.fromCharCode(charCode);  
        i += 3;  
      } else {  
        // 四字节  
        const byte2 = bytes[i + 1];  
        const byte3 = bytes[i + 2];  
        const byte4 = bytes[i + 3];  
        const codePoint = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F);  

        // 处理UTF-16代理对  
        if (codePoint >= 0x10000) {  
          const highSurrogate = ((codePoint - 0x10000) >> 10) + 0xD800;  
          const lowSurrogate = ((codePoint - 0x10000) & 0x3FF) + 0xDC00;  
          result += String.fromCharCode(highSurrogate, lowSurrogate);  
        } else {  
          result += String.fromCharCode(codePoint);  
        }  

        i += 4;  
      }  
    }  

    return result;  
  }  

  // 将字符串转换为UTF-8字节数组  
  stringToBytes(str) {  
    let bytes = [];  
    for (let i = 0; i < str.length; i++) {  
      let charCode = str.charCodeAt(i);  
      if (charCode < 0x80) {  
        bytes.push(charCode);  
      } else if (charCode < 0x800) {  
        bytes.push(0xC0 | (charCode >> 6));  
        bytes.push(0x80 | (charCode & 0x3F));  
      } else if (charCode < 0xD800 || charCode >= 0xE000) {  
        bytes.push(0xE0 | (charCode >> 12));  
        bytes.push(0x80 | ((charCode >> 6) & 0x3F));  
        bytes.push(0x80 | (charCode & 0x3F));  
      } else {  
        // 处理代理对 (surrogate pair)  
        i++;  
        const nextCharCode = str.charCodeAt(i);  
        const codePoint = ((charCode - 0xD800) << 10) | (nextCharCode - 0xDC00) + 0x10000;  
        bytes.push(0xF0 | (codePoint >> 18));  
        bytes.push(0x80 | ((codePoint >> 12) & 0x3F));  
        bytes.push(0x80 | ((codePoint >> 6) & 0x3F));  
        bytes.push(0x80 | (codePoint & 0x3F));  
      }  
    }  
    return bytes;  
  }  

  // 编码MQTT剩余长度  
  encodeRemainingLength(length) {  
    let result = [];  
    let digit;  
    do {  
      digit = length % 128;  
      length = Math.floor(length / 128);  
      if (length > 0) {  
        digit |= 0x80;  
      }  
      result.push(digit);  
    } while (length > 0);  
    return result;  
  }  

  // 处理MQTT协议包  
  handleMqttPacket(data) {  
    try {  
      let buffer;  
      if (data instanceof ArrayBuffer) {  
        buffer = new Uint8Array(data);  
      } else if (typeof data === 'string') {  
        // 如果是字符串,可能是测试数据  
        try {  
          const parsed = JSON.parse(data);  
          if (parsed.topic && parsed.payload) {  
            this.emit('message', parsed.topic, parsed.payload);  
            return;  
          }  
        } catch (e) {  
          // 不是JSON,继续处理  
        }  
        this.emit('message', 'unknown', data);  
        return;  
      } else {  
        this.emit('message', 'unknown', data);  
        return;  
      }  

      if (buffer.length < 2) {  
        console.warn('MQTT包太短');  
        return;  
      }  

      const fixedHeader = buffer[0];  
      const packetType = (fixedHeader >> 4) & 0x0F;  

      // CONNACK (0x20) - 连接确认  
      if (packetType === 2) {  
        if (buffer.length >= 4) {  
          const returnCode = buffer[3];  
          if (returnCode === 0) {  
            console.log('MQTT连接成功');  
            this.connected = true;  
            this.reconnectAttempts = 0;  
            // 启动心跳机制  
            this.startHeartbeat();  
            this.emit('connect');  
          } else {  
            console.error('MQTT连接失败,返回码:', returnCode);  
            this.emit('error', new Error('MQTT连接失败,返回码: ' + returnCode));  
          }  
        }  
        return;  
      }  

      // PUBLISH (0x30) - 消息发布  
      if (packetType === 3) {  
        // 解析剩余长度  
        let remainingLength = 0;  
        let multiplier = 1;  
        let index = 1;  
        let digit;  

        do {  
          digit = buffer[index++];  
          remainingLength += (digit & 0x7F) * multiplier;  
          multiplier *= 128;  
        } while ((digit & 0x80) !== 0);  

        // 提取可变头部和载荷  
        const variableHeaderAndPayload = buffer.subarray(index, index + remainingLength);  

        // 解析主题名称长度  
        const topicLength = (variableHeaderAndPayload[0] << 8) | variableHeaderAndPayload[1];  
        const topicNameBytes = variableHeaderAndPayload.subarray(2, 2 + topicLength);  

        // 解码主题名称  
        const topicName = this.decodeUtf8(topicNameBytes);  

        // 确定QoS级别  
        const qos = (fixedHeader >> 1) & 0x03;  

        // 跳过消息ID(如果QoS > 0)  
        let payloadStart = 2 + topicLength;  
        if (qos > 0) {  
          payloadStart += 2;  
        }  

        // 提取消息载荷  
        const payload = variableHeaderAndPayload.subarray(payloadStart);  

        // 发送消息载荷  
        this.emit('message', topicName, payload);  
        return;  
      }  

      // SUBACK (0x90) - 订阅确认  
      if (packetType === 9) {  
        console.log('收到MQTT SUBACK');  
        return;  
      }  

      // UNSUBACK (0xB0) - 取消订阅确认  
      if (packetType === 11) {  
        console.log('收到MQTT UNSUBACK');  
        return;  
      }  

      // PINGRESP (0xD0) - Ping响应  
      if (packetType === 13) {  
        console.log('收到MQTT PINGRESP');  
        return;  
      }  

      // 其他类型的包  
      console.log('收到其他类型的MQTT包:', packetType);  

    } catch (error) {  
      console.error('处理MQTT协议包错误:', error);  
      this.emit('error', error);  
    }  
  }  

  publish(topic, message, callback) {  
    if (this.connected && this.socketTask) {  
      try {  
        let payloadBytes;  

        // 处理消息载荷  
        if (typeof message === 'string') {  
          // 如果是十六进制字符串,转换为字节  
          if (/^[0-9A-Fa-f\s]+$/.test(message)) {  
            const cleanHex = message.replace(/\s/g, '');  
            payloadBytes = [];  
            for (let i = 0; i < cleanHex.length; i += 2) {  
              payloadBytes.push(parseInt(cleanHex.substr(i, 2), 16));  
            }  
          } else {  
            // 普通字符串  
            payloadBytes = this.stringToBytes(message);  
          }  
        } else if (message instanceof Uint8Array) {  
          payloadBytes = Array.from(message);  
        } else if (message instanceof ArrayBuffer) {  
          payloadBytes = Array.from(new Uint8Array(message));  
        } else {  
          // 其他类型转换为字符串  
          payloadBytes = this.stringToBytes(JSON.stringify(message));  
        }  

        // 构建MQTT PUBLISH协议包  
        let variableHeader = [];  

        // Topic  
        variableHeader = variableHeader.concat(this.encodeString(topic));  

        // QoS 0,没有messageId  

        // 计算剩余长度  
        const remainingLength = variableHeader.length + payloadBytes.length;  
        const remainingLengthBytes = this.encodeRemainingLength(remainingLength);  

        // 构建完整的MQTT包  
        let mqttPacket = [];  

        // Fixed Header: PUBLISH (0x30) - QoS 0, no retain  
        mqttPacket.push(0x30);  

        // Remaining Length  
        mqttPacket = mqttPacket.concat(remainingLengthBytes);  

        // Variable Header  
        mqttPacket = mqttPacket.concat(variableHeader);  

        // Payload  
        mqttPacket = mqttPacket.concat(payloadBytes);  

        // 转换为ArrayBuffer发送  
        const buffer = new Uint8Array(mqttPacket).buffer;  

        this.socketTask.send({  
          data: buffer,  
          success: () => {  
            if (callback) callback();  
          },  
          fail: (error) => {  
            console.error('MQTT PUBLISH协议包发送失败:', error);  
            if (callback) callback(error);  
          }  
        });  
      } catch (error) {  
        console.error('发布消息错误:', error);  
        if (callback) callback(error);  
      }  
    } else {  
      console.warn('WebSocket未连接,无法发布消息');  
      if (callback) callback(new Error('WebSocket未连接'));  
    }  
  }  

  subscribe(topic, callback) {  
    if (this.connected && this.socketTask) {  
      try {  
        // 构建MQTT SUBSCRIBE协议包  
        let variableHeader = [];  

        // Message ID  
        const messageId = this.messageId++;  
        variableHeader.push((messageId >> 8) & 0xFF);  
        variableHeader.push(messageId & 0xFF);  

        // Payload: Topic + QoS  
        let payload = [];  
        payload = payload.concat(this.encodeString(topic));  
        payload.push(0); // QoS 0  

        // 计算剩余长度  
        const remainingLength = variableHeader.length + payload.length;  
        const remainingLengthBytes = this.encodeRemainingLength(remainingLength);  

        // 构建完整的MQTT包  
        let mqttPacket = [];  

        // Fixed Header: SUBSCRIBE (0x82)  
        mqttPacket.push(0x82);  

        // Remaining Length  
        mqttPacket = mqttPacket.concat(remainingLengthBytes);  

        // Variable Header  
        mqttPacket = mqttPacket.concat(variableHeader);  

        // Payload  
        mqttPacket = mqttPacket.concat(payload);  

        // 转换为ArrayBuffer发送  
        const buffer = new Uint8Array(mqttPacket).buffer;  

        this.socketTask.send({  
          data: buffer,  
          success: () => {  
            if (callback) callback();  
          },  
          fail: (error) => {  
            console.error('MQTT SUBSCRIBE协议包发送失败:', error);  
            if (callback) callback(error);  
          }  
        });  
      } catch (error) {  
        console.error('订阅错误:', error);  
        if (callback) callback(error);  
      }  
    } else {  
      console.warn('WebSocket未连接,无法订阅');  
      if (callback) callback(new Error('WebSocket未连接'));  
    }  
  }  

  unsubscribe(topic) {  
    if (this.connected && this.socketTask) {  
      try {  
        // 构建MQTT UNSUBSCRIBE协议包  
        let variableHeader = [];  

        // Message ID  
        const messageId = this.messageId++;  
        variableHeader.push((messageId >> 8) & 0xFF);  
        variableHeader.push(messageId & 0xFF);  

        // Payload: Topic  
        let payload = [];  
        payload = payload.concat(this.encodeString(topic));  

        // 计算剩余长度  
        const remainingLength = variableHeader.length + payload.length;  
        const remainingLengthBytes = this.encodeRemainingLength(remainingLength);  

        // 构建完整的MQTT包  
        let mqttPacket = [];  

        // Fixed Header: UNSUBSCRIBE (0xA2)  
        mqttPacket.push(0xA2);  

        // Remaining Length  
        mqttPacket = mqttPacket.concat(remainingLengthBytes);  

        // Variable Header  
        mqttPacket = mqttPacket.concat(variableHeader);  

        // Payload  
        mqttPacket = mqttPacket.concat(payload);  

        // 转换为ArrayBuffer发送  
        const buffer = new Uint8Array(mqttPacket).buffer;  

        this.socketTask.send({  
          data: buffer,  
          fail: (error) => {  
            console.error('MQTT UNSUBSCRIBE协议包发送失败:', error);  
          }  
        });  
      } catch (error) {  
        console.error('取消订阅错误:', error);  
      }  
    }  
  }  

  sendPingreq() {  
    if (this.connected && this.socketTask) {  
      try {  
        // 构建MQTT PINGREQ协议包  
        const pingreqPacket = [0xC0, 0x00]; // PINGREQ (0xC0) + 剩余长度 0  

        const buffer = new Uint8Array(pingreqPacket).buffer;  

        console.log('发送MQTT PINGREQ');  

        this.socketTask.send({  
          data: buffer,  
          success: () => {  
            console.log('MQTT PINGREQ发送成功');  
          },  
          fail: (error) => {  
            console.error('MQTT PINGREQ发送失败:', error);  
          }  
        });  
      } catch (error) {  
        console.error('发送MQTT PINGREQ错误:', error);  
      }  
    }  
  }  

  startHeartbeat() {  
    // 清除现有的心跳定时器  
    if (this.heartbeatTimer) {  
      clearInterval(this.heartbeatTimer);  
      this.heartbeatTimer = null;  
    }  

    // 每keepAlive/2秒发送一次PINGREQ  
    const heartbeatInterval = (this.keepAlive * 1000) / 2;  

    console.log('启动心跳机制,间隔:', heartbeatInterval, 'ms');  

    this.heartbeatTimer = setInterval(() => {  
      this.sendPingreq();  
    }, heartbeatInterval);  
  }  

  stopHeartbeat() {  
    if (this.heartbeatTimer) {  
      clearInterval(this.heartbeatTimer);  
      this.heartbeatTimer = null;  
      console.log('停止心跳机制');  
    }  
  }  

  end() {  
    // 停止重连  
    this.shouldReconnect = false;  

    // 停止心跳  
    this.stopHeartbeat();  

    // 清除重连定时器  
    if (this.reconnectTimer) {  
      clearTimeout(this.reconnectTimer);  
      this.reconnectTimer = null;  
    }  

    if (this.socketTask) {  
      this.socketTask.close({  
        success: () => {  
          console.log('WebSocket连接已关闭');  
        }  
      });  
      this.connected = false;  
    }  
  }  

  attemptReconnect() {  
    // 检查是否应该重连  
    if (!this.shouldReconnect) {  
      console.log('已停止重连');  
      return;  
    }  

    // 停止当前心跳  
    this.stopHeartbeat();  

    if (this.reconnectAttempts < this.maxReconnectAttempts) {  
      this.reconnectAttempts++;  
      const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);  

      console.log(`尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts}),延迟: ${delay}ms`);  

      this.reconnectTimer = setTimeout(() => {  
        this.connect();  
      }, delay);  
    } else {  
      console.error('达到最大重连次数,停止重连');  
      this.emit('error', new Error('达到最大重连次数'));  
    }  
  }  

  on(event, handler) {  
    if (!this.eventHandlers[event]) {  
      this.eventHandlers[event] = [];  
    }  
    this.eventHandlers[event].push(handler);  
  }  

  emit(event, ...args) {  
    if (this.eventHandlers[event]) {  
      this.eventHandlers[event].forEach(handler => handler(...args));  
    }  
  }  
}  

// 创建MQTT连接函数  
export const createMqttConnection = (url, options) => {  
  return new WechatMqttClient(url, options);  
};

在 uni-app 中使用

下面展示如何在 uni-app (Vue 3 setup 语法糖) 中集成该客户端,实现一个设备状态监控弹窗。

1. 引入与配置

import { createMqttConnection } from '@/utils/miniMqtt.js'; // 引入封装好的类  

// 配置项  
const mqttUrl = 'wss://your-mqtt-broker.com/mqtt';  
const clientId = "emqx_test_" + Math.random().toString(16).substring(2, 8);  
const username = "local_cabinet";  
const password = "hUwRNRvjcaGwsuU3";

2. 初始化连接与事件监听

在组件挂载或用户操作时初始化:

let client = null;  

const initMqtt = () => {  
  if (client) client.end(); // 清理旧连接  

  client = createMqttConnection(mqttUrl, {  
    clientId,  
    username,  
    password,  
    keepAlive: 60  
  });  

  // 监听连接成功  
  client.on('connect', () => {  
    console.log('MQTT连接成功');  
    addDebugMessage("连接成功", "success");  
    // 连接成功后立即订阅主题  
    client.subscribe('wash/hy/device001/pubmsg', (err) => {  
      if(!err) addDebugMessage("订阅成功", "success");  
    });  
  });  

  // 监听错误  
  client.on('error', (err) => {  
    console.error(err);  
    addDebugMessage("发生错误:" + err.message, "error");  
  });  

  // 监听消息  
  client.on('message', (topic, message) => {  
    handleMqttMessage(topic, message);  
  });  

  client.connect();  
};

3. 消息收发处理

发送消息(支持 Hex 字符串):
很多硬件设备通信使用十六进制指令。我们的封装类在 publish 方法中做了特殊处理:如果检测到传入的是纯十六进制字符串,会自动转换为 Uint8Array 发送。

const publishMessage = (topic, message) => {  
  if (!client || !client.connected) return;  

  // 假设 message 是 "01 03 00 00 00 0A C4 0B" 这样的十六进制字符串  
  // 或者普通文本 "Hello"  
  client.publish(topic, message, (err) => {  
    if (err) {  
      uni.showToast({ title: '发送失败', icon: 'none' });  
    } else {  
      addDebugMessage("指令已发送", "primary");  
    }  
  });  
};

接收消息解析:

接收到的 message 通常是 Uint8Array。我们可以将其转换为十六进制字符串以便调试或发送给后端解析。

const handleMqttMessage = (topic, message) => {  
  // 将 Uint8Array 转为 Hex 字符串显示  
  let hexString = "";  
  if (message instanceof Uint8Array) {  
    for (let i = 0; i < message.length; i++) {  
      hexString += message[i].toString(16).toUpperCase().padStart(2, "0") + " ";  
    }  
  }  

  console.log(`收到消息 [${topic}]: ${hexString}`);  
  addDebugMessage(`收到: ${hexString}`, "default");  

  // 此处可调用后端API解析具体的业务含义  
};

4. 完整页面示例结构

<template>  
    <view>  
        <uv-popup ref="popupRef" mode="center" round="10" :closeable="true" @change="change" :safeAreaInsetBottom="false">  
            <view class="w-670 p-40 text-center flex flex-col">  
                <view class="fs-36" :style="{ color:theme.primaryFontColor}">通信状态</view>  
                <scroll-view ref="scrollRef" :scroll-top="scrollTop" scroll-y="true" class="w-full h-420 mt-60">  
                    <view class="scroll-view">  
                         <view v-for="(item,index) in messageInfo" :key="index" class="scroll-item">  
                             <text class="fs-28 text-left" :style="{ color:theme.labelColor}">{{item.time}}</text>  
                             <text class="fs-32 text-left mt-10" :style="{color:item.type}">{{item.message}}</text>  
                         </view>  
                    </view>  
                </scroll-view>  
                <view class="mt-60">  
                    <uv-button   
                        :loading="loading"   
                        text="查询设备状态"   
                        @click="sendMsg"  
                        :customStyle="{  
                            width:'590rpx',  
                            height:'112rpx',  
                            borderRadius:'16rpx',  
                            backgroundColor:theme.primaryColor,  
                            color:theme.whiteColor  
                        }"  
                    ></uv-button>  
                </view>  
            </view>  
        </uv-popup>  
    </view>  
</template>  

<script setup>  
import { ref,nextTick,getCurrentInstance } from 'vue'  
import { getQueryCommandAPI, getQueryCommandTextAPI } from '@/api/device';  
import config from '@/config/config';  
import { createMqttConnection } from '@/utils/miniMqtt.js';  
import { useAppStore } from '@/store/app.js';  

const theme = useAppStore().theme;  
const instance = getCurrentInstance();  

let popupRef = ref(null)  
let scrollRef = ref(null)  
let deviceInfo = ref(null)  
let loading = ref(false)  
let messageInfo = ref([])  
let scrollTop = ref(0)  
let subscribeTopic = "";  
let publishTopic = "";  
let currentPublishTopic = "";  
let currentSubscribeTopic = "";  
let ctimer = null;  
let client = null;  
// MQTT 配置  
const mqttUrl = config.api.wss;  
const clientId = "emqx_test_" + Math.random().toString(16).substring(2, 8);  
const username = "local_cabinet";  
const password = "hUwRNRvjcaGwsuU3";  

let open = ()=>{  
    popupRef.value && popupRef.value.open()  
}  
let openMqtt = (params)=>{  
    deviceInfo.value = params  
     // 生成订阅和发布主题  
    let prefix = params.transmission == 1 ? "wash/hy/" : "wash/ts/";  
    subscribeTopic = prefix + params.device_no + "/pubmsg";  
    publishTopic = prefix + params.device_no + "/submsg";  
    currentPublishTopic = publishTopic;  
    open()  
    initMqtt()  
}  

// 初始化MQTT  
let initMqtt = () => {  
  loading.value = true;  

  if (client) {  
    client.end();  
    console.log("已断开连接");  
  }  

  addDebugMessage("MQTT正在尝试连接...", theme.primaryFontColor);  

  try {  
    // 使用封装的MQTT客户端  
    client = createMqttConnection(mqttUrl, {  
      clientId,  
      username,  
      password,  
      rejectUnauthorized: false,  
    });  

    // 设置事件监听  
    client.on('connect', () => {  
      console.log('MQTT连接成功');  
      addDebugMessage("MQTT连接成功", theme.successColor);  
      loading.value = false;  
      // 订阅主题  
      addSubscribeTopic(subscribeTopic);  
    });  

    client.on('reconnect', () => {  
      console.log("MQTT尝试重连...");  
      addDebugMessage("MQTT正在重连...", theme.primaryFontColor);  
    });  

    client.on('error', (error) => {  
      console.error("MQTT连接错误:", error);  
      addDebugMessage("MQTT连接错误:" + error.message, theme.errorColor);  
      loading.value = false;  
    });  

    client.on('message', (topic, message) => {  
      // 处理接收到的消息  
      handleMqttMessage(topic, message);  
    });  

    // 开始连接  
    client.connect();  

  } catch (error) {  
    console.error("MQTT初始化失败:", error);  
    addDebugMessage("MQTT初始化失败: " + error.message, theme.errorColor);  
    loading.value = false;  
  }  
};  

// 处理MQTT消息  
let handleMqttMessage = (topic, message) => {  
  try {  

    // 转换为16进制显示  
    const hexMessage = messageToHexString(message);  

    // 显示接收消息  
    getQueryCommandTextAPI({  
      command: hexMessage,  
      agreement: deviceInfo.value.agreement  
    }).then(res => {  
      loading.value = false;  
      if (res.data) {  
        let data = res.data;  
        clearTimeout(ctimer);  
        addDebugMessage(`收到消息:${data.run_status_text ? data.run_status_text : ''} ${data.run_fault == "[]" ? "" : data.run_fault}`, theme.primaryFontColor);  
      }  
    }).catch(error => {  
      clearTimeout(ctimer);  
      loading.value = false;  
      addDebugMessage(error.msg, theme.errorColor);  
    });  
  } catch (error) {  
    loading.value = false;  
    addDebugMessage("消息解析错误: " + error.message, theme.errorColor);  
  }  
};  

let sendMsg = () => {  
  loading.value = true;  
  getQueryCommandAPI({  
    deviceNo: deviceInfo.value.deviceNo,  
    agreement: deviceInfo.value.agreement  
  }).then(res => {  
    if (res.data) {  
      let queryCommand = res.data;  
      // 验证是否为有效的十六进制字符串  
      if (isValidHexString(queryCommand)) {  
        publishMessage(currentPublishTopic, queryCommand);  
        uni.$uv.toast("查询命令发送成功");  
      } else {  
        addDebugMessage("警告: 查询命令不是有效的十六进制格式,按文本发送", theme.errorColor);  
        publishMessage(currentPublishTopic, queryCommand);  
        uni.$uv.toast("查询命令发送成功");  
      }  
    } else {  
      uni.$uv.toast("获取查询命令失败或不支持的协议");  
    }  
  });  
  startCoutdown();  
};  

let startCoutdown = () => {  
  clearTimeout(ctimer);  
  ctimer = setTimeout(() => {  
    loading.value = false;  
    addDebugMessage("查询失败", theme.errorColor);  
  }, 5000);  
};  

let scrollToBottom = () => {  
  nextTick(() => {  
         const query = uni.createSelectorQuery().in(instance.proxy);  
            query.select('.scroll-view').boundingClientRect(res => {  
                scrollTop.value = res.height  
        }).exec();  
  });  
};  

// 订阅主题  
let addSubscribeTopic = topic => {  
  if (currentSubscribeTopic && client) {  
    client.unsubscribe(currentSubscribeTopic);  
  }  

  if (topic && client && client.connected) {  
    client.subscribe(topic, (err) => {  
      if (!err) {  
        currentSubscribeTopic = topic;  
        addDebugMessage("订阅成功 ", theme.successColor);  
      } else {  
        addDebugMessage("订阅失败: " + err.message, theme.errorColor);  
      }  
    });  
  }  
};  

// 发布消息  
let publishMessage = (topic, message) => {  
  if (client && client.connected) {  
    let messageToSend;  

    // 检查是否为十六进制字符串  
    if (typeof message === "string" && isValidHexString(message)) {  
      try {  
        // 转换为二进制数据  
        messageToSend = hexStringToBuffer(message);  
      } catch (error) {  
        addDebugMessage("十六进制转换失败: " + error.message, theme.errorColor);  
        return;  
      }  
    } else {  
      // 普通字符串消息  
      messageToSend = message;  
      addDebugMessage("发送文本消息: " + message, theme.primaryFontColor);  
    }  

    client.publish(topic, messageToSend, (err) => {  
      if (err) {  
        addDebugMessage("消息发送失败: " + err.message, theme.errorColor);  
      } else {  
        addDebugMessage("消息发送成功", theme.successColor);  
      }  
    });  
  } else {  
    addDebugMessage("MQTT未连接,无法发送消息", theme.errorColor);  
  }  
};  

// 将十六进制字符串转换为二进制数据  
let hexStringToBuffer = hexString => {  
  // 移除空格和非十六进制字符  
  const cleanHex = hexString.replace(/[^0-9A-Fa-f]/g, "");  

  // 确保是偶数长度  
  const evenLengthHex = cleanHex.length % 2 === 0 ? cleanHex : "0" + cleanHex;  

  // 创建Uint8Array  
  const bytes = new Uint8Array(evenLengthHex.length / 2);  

  for (let i = 0; i < evenLengthHex.length; i += 2) {  
    bytes[i / 2] = parseInt(evenLengthHex.substr(i, 2), 16);  
  }  

  return bytes;  
};  

let addDebugMessage = (message, type) => {  
  messageInfo.value.push({  
    message,  
    type,  
    time: uni.$uv.date(new Date(),'hh:MM:ss')  
  });  
  scrollToBottom();  
};  

// 验证十六进制字符串格式  
let isValidHexString = hexString => {  
  const cleanHex = hexString.replace(/\s+/g, "");  
  return /^[0-9A-Fa-f]+$/.test(cleanHex);  
};  

// 将消息转换为16进制字符串的函数  
let messageToHexString = message => {  
  let hexString = "";  
  if (message instanceof Uint8Array) {  
    // 如果是Uint8Array  
    for (let i = 0; i < message.length; i++) {  
      hexString += message[i].toString(16).toUpperCase().padStart(2, "0") + " ";  
    }  
  } else if (typeof message === "string") {  
    // 如果是字符串,转换每个字符的字节值  
    for (let i = 0; i < message.length; i++) {  
      hexString += message.charCodeAt(i).toString(16).toUpperCase().padStart(2, "0") + " ";  
    }  
  } else if (message && typeof message === "object" && message.constructor && message.constructor.name === "Buffer") {  
    // 如果是Buffer对象(但在浏览器中通常不会遇到)  
    for (let i = 0; i < message.length; i++) {  
      hexString += message[i].toString(16).toUpperCase().padStart(2, "0") + " ";  
    }  
  } else if (message instanceof ArrayBuffer) {  
    // 如果是ArrayBuffer  
    const uint8Array = new Uint8Array(message);  
    for (let i = 0; i < uint8Array.length; i++) {  
      hexString += uint8Array[i].toString(16).toUpperCase().padStart(2, "0") + " ";  
    }  
  } else {  
    // 其他情况,尝试转换为字符串处理  
    const str = message.toString();  
    for (let i = 0; i < str.length; i++) {  
      hexString += str.charCodeAt(i).toString(16).toUpperCase().padStart(2, "0") + " ";  
    }  
  }  

  return hexString.trim();  
};  

let change = (e)=>{  
    if(!e.show){  
         if (client) {  
            // 先取消订阅,再关闭连接  
            if (currentSubscribeTopic) {  
              client.unsubscribe(currentSubscribeTopic);  
            }  
            client.end();  
          }  
          messageInfo.value = [];  
          currentSubscribeTopic = "";  
          currentPublishTopic = "";  
    }  
}  

defineExpose({  
    openMqtt  
})  

</script>  

<style scoped>  
    .scroll-item {  
      display: flex;  
      flex-direction: column;  
      width: 100%;  
      padding: 30rpx 0;  
      border-bottom: 2rpx solid #F5F5F5;  
    }  
</style>
收起阅读 »

UniApp 原生插件集合(2026)

前言

本文整理了一些比较常用的原生插件,包括扫码、图片选择、文件选择、图片编辑、应用通知、应用未读角标、开机自启、sqlite数据库、保活、快捷方式、图片水印、视频压缩、动态修改应用图标等等,有其他需要可以留言,感谢支持。


📑 目录

第一列 第二列 第三列
1. 扫码(6款) 14. 地图 27. 监听系统广播、自定义广播
2. 文件选择 15. 悬浮窗 28. 监听通知栏消息(支持白黑名单、过滤)
3. 图片选择 16. 画中画 29. 全局置灰、哀悼置灰(可动态、同时支持nvue、vue)
4. 图片编辑 17. 获取设备唯一标识 30. 窗口小工具、桌面小部件、微件
5. 图片压缩 18. WebSocket原生服务(后台) 31. 用其他应用打开、分享
6. 图片水印 19. 安卓快捷方式(桌面长按app图标) 32. 来电显示
7. 视频压缩、剪辑 20. 动态切换应用图标、名称 33. 抖音授权登录、发布、分享
8. 应用消息通知 21. 动态修改状态栏、导航栏 34. 反射方法调用插件
9. 应用未读角标 22. 原生Toast弹窗提示(穿透所有界面) 35. 字母索引列表插件(组件版)
10. 保活 23. PDF阅读 36. 权限申请插件(权限使用说明)
11. 开机自启 24. 声音提示、震动提示、语音播报 37. 桌面应用插件
12. 数据库 25. 短信监听(验证码)
13. 定位 26. 智能安装(自动升级)

⭐ 如果这些插件对您有帮助,欢迎点赞、收藏、分享!⭐

感谢支持!

继续阅读 »

前言

本文整理了一些比较常用的原生插件,包括扫码、图片选择、文件选择、图片编辑、应用通知、应用未读角标、开机自启、sqlite数据库、保活、快捷方式、图片水印、视频压缩、动态修改应用图标等等,有其他需要可以留言,感谢支持。


📑 目录

第一列 第二列 第三列
1. 扫码(6款) 14. 地图 27. 监听系统广播、自定义广播
2. 文件选择 15. 悬浮窗 28. 监听通知栏消息(支持白黑名单、过滤)
3. 图片选择 16. 画中画 29. 全局置灰、哀悼置灰(可动态、同时支持nvue、vue)
4. 图片编辑 17. 获取设备唯一标识 30. 窗口小工具、桌面小部件、微件
5. 图片压缩 18. WebSocket原生服务(后台) 31. 用其他应用打开、分享
6. 图片水印 19. 安卓快捷方式(桌面长按app图标) 32. 来电显示
7. 视频压缩、剪辑 20. 动态切换应用图标、名称 33. 抖音授权登录、发布、分享
8. 应用消息通知 21. 动态修改状态栏、导航栏 34. 反射方法调用插件
9. 应用未读角标 22. 原生Toast弹窗提示(穿透所有界面) 35. 字母索引列表插件(组件版)
10. 保活 23. PDF阅读 36. 权限申请插件(权限使用说明)
11. 开机自启 24. 声音提示、震动提示、语音播报 37. 桌面应用插件
12. 数据库 25. 短信监听(验证码)
13. 定位 26. 智能安装(自动升级)

⭐ 如果这些插件对您有帮助,欢迎点赞、收藏、分享!⭐

感谢支持!

收起阅读 »