HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

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

unipush

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

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

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

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

继续阅读 »

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

收起阅读 »

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>
收起阅读 »

小程序端解决分包的uni_modules打包后产物进入主包中的问题

体积优化 微信小程序 uni-app-x uni-app

配置

分包优化

需要在 mainfest.json 指定小程序节点下添加如下配置,例如:

{  
  "mp-weixin": {  
         "optimization": {  
            "subPackages": true  
          },  
        "usingComponents": true  
  }  
}

主包分包的 uni_modules

首先,主包的 uni_moudles 要放在主包的根目录下,分包的 uni_moudles 要放在分包的根目录下,参考附件的第一个图片。

然后,在 pages.json 中配置组件 easycom 引入规则,这一步是为了避免同一个组件库被主包分包都使用,出现识别错误的问题,例如,我在 uniappx 项目中使用了 rice-ui 组件库,可以这样配置

{  
  "easycom": {  
        "autoscan": true,  
        "custom": {  
            "^rice-(.*)": "uni_modules/rice-ui/components/rice-$1/rice-$1.uvue",  
            "^sub-rice-(.*)": "sub/uni_modules/rice-ui/components/rice-$1/rice-$1.uvue"  
        }  
    }  
}

这样,分包用组件就写 sub-rice-avatar,主包就是 rice-button

效果参考附件的第二张图片

示例项目

参考附件三

继续阅读 »

配置

分包优化

需要在 mainfest.json 指定小程序节点下添加如下配置,例如:

{  
  "mp-weixin": {  
         "optimization": {  
            "subPackages": true  
          },  
        "usingComponents": true  
  }  
}

主包分包的 uni_modules

首先,主包的 uni_moudles 要放在主包的根目录下,分包的 uni_moudles 要放在分包的根目录下,参考附件的第一个图片。

然后,在 pages.json 中配置组件 easycom 引入规则,这一步是为了避免同一个组件库被主包分包都使用,出现识别错误的问题,例如,我在 uniappx 项目中使用了 rice-ui 组件库,可以这样配置

{  
  "easycom": {  
        "autoscan": true,  
        "custom": {  
            "^rice-(.*)": "uni_modules/rice-ui/components/rice-$1/rice-$1.uvue",  
            "^sub-rice-(.*)": "sub/uni_modules/rice-ui/components/rice-$1/rice-$1.uvue"  
        }  
    }  
}

这样,分包用组件就写 sub-rice-avatar,主包就是 rice-button

效果参考附件的第二张图片

示例项目

参考附件三

收起阅读 »

删帖

苹果内购 应用内支付

帖子不删掉改空帖

帖子不删掉改空帖

uni-app仿deepseek跨三端流式ai实例|uniapp聊天ai

sse OpenAI vue3 uni_ai uni_app

经过一个多月迭代升级,2026最新款uniapp+vue3+deepseek搭建小程序.安卓.H5端流式打字ai对话模板。支持深度思考、复制代码、latex数学公式、链接、图片预览等功能。

三端效果如下:

运用技术:

  • 编辑器:HbuilderX 4.87
  • 技术框架:uni-app+vue3+pinia2+vite5
  • 大模型框架:DeepSeek-V3.2
  • 组件库:uni-ui+uv-ui
  • 高亮插件:highlight.js
  • markdown渲染:ua-markdown+mp-html
  • 本地缓存:pinia-plugin-unistorage

项目结构目录

> #### uniapp-deepseek跨三端流式ai模板已经同步到我的原创作品集。
> 2026版uniapp+deepseek+vue3跨端AI流式输出对话模板

新增支持小程序端复制代码、latex数学公式、表格、链接复制、预览图片等功能。

h5端支持mermaid图表渲染,支持运行到pc端以750px宽度显示页面。

作者:xiaoyan2017
链接: https://www.cnblogs.com/xiaoyan2017/p/19599014
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

继续阅读 »

经过一个多月迭代升级,2026最新款uniapp+vue3+deepseek搭建小程序.安卓.H5端流式打字ai对话模板。支持深度思考、复制代码、latex数学公式、链接、图片预览等功能。

三端效果如下:

运用技术:

  • 编辑器:HbuilderX 4.87
  • 技术框架:uni-app+vue3+pinia2+vite5
  • 大模型框架:DeepSeek-V3.2
  • 组件库:uni-ui+uv-ui
  • 高亮插件:highlight.js
  • markdown渲染:ua-markdown+mp-html
  • 本地缓存:pinia-plugin-unistorage

项目结构目录

> #### uniapp-deepseek跨三端流式ai模板已经同步到我的原创作品集。
> 2026版uniapp+deepseek+vue3跨端AI流式输出对话模板

新增支持小程序端复制代码、latex数学公式、表格、链接复制、预览图片等功能。

h5端支持mermaid图表渲染,支持运行到pc端以750px宽度显示页面。

作者:xiaoyan2017
链接: https://www.cnblogs.com/xiaoyan2017/p/19599014
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

安卓启动图变形问题解决方案,.9.png启动图生成

.9.png

使用Android Studio工具去生成,效果嘎嘎好

有不会的联系我,或者发送邮件:it2003wei@163.com,请我喝杯奶茶帮你搞定嘻嘻

使用Android Studio工具去生成,效果嘎嘎好

有不会的联系我,或者发送邮件:it2003wei@163.com,请我喝杯奶茶帮你搞定嘻嘻

h5端尺寸转换逻辑

uni_app项目 h5

vue3 + vite

vue3 下 , h5使用rpx 时, 会 编译成 rem

注册 postcss 的 plugin

在/node_modules/@dcloudio/uni-h5-vite/dist/plugin/config.js,
代码是, 用的 是config 这个钩子

 css: {  
    postcss: {  
        plugins: (0, uni_cli_shared_1.initPostcssPlugin)({  
            uniApp: (0, uni_cli_shared_1.parseRpx2UnitOnce)(inputDir, process.env.UNI_PLATFORM),  
        }),  
    },  
},

parseRpx2UnitOnce , 选定使用的单位

exports.parseRpx2UnitOnce = (0, uni_shared_1.once)((inputDir, platform = 'h5') => {  
    // 如果是  h5/app/鸿蒙 会使用 defaultRpx2Unit  
    const rpx2unit = platform === 'h5' || platform === 'app' || platform === 'app-harmony'  
        ? uni_shared_1.defaultRpx2Unit  
        : uni_shared_1.defaultMiniProgramRpx2Unit;  
    const manifestJson = (0, exports.parseManifestJsonOnce)(inputDir);  
    let platformOptions = getPlatformManifestJson(manifestJson, platform);  
    if (platformOptions && platformOptions.rpx) {  
        return (0, shared_1.extend)({}, rpx2unit, platformOptions);  
    }  
    return (0, shared_1.extend)({}, rpx2unit);  
});

都有啥呢 ?

const defaultRpx2Unit = {  
    unit: 'rem',  
    unitRatio: 10 / 320,  
    unitPrecision: 5,  
};  
const defaultMiniProgramRpx2Unit = {  
    unit: 'rpx',  
    unitRatio: 1,  
    unitPrecision: 1,  
};

但是 注意这里 let platformOptions = getPlatformManifestJson(manifestJson, platform);
还是 会从 mainfest中 获取 配置的, 如果有 , 就用 mainfest 中的,
也就是 你可以 在 manifest.json的 h5节点下 , 覆盖 一些 配置 , 例如

    "h5": {  
        "router": {  
            "mode": "history"  
        },  
        "unit": "px",  
        "unitRatio": 0.5,  
        "unitPrecision": 2,  
    }

但是 文档里 , 我没有 找到

哪里 修改的 rpx 2 rem 呢?

在这里 /node_modules/@dcloudio/uni-cli-shared/dist/postcss/plugins/uniapp.js

const uniapp = (opts) => {  
    const platform = process.env.UNI_PLATFORM;  
    const { unit, unitRatio, unitPrecision } = (0, shared_1.extend)({}, defaultUniAppCssProcessorOptions, opts);  
    const rpx2unit = (0, uni_shared_1.createRpx2Unit)(unit, unitRatio, unitPrecision);  
    return {  
        postcssPlugin: 'uni-app',  
        prepare() {  
            return {  
                OnceExit(root) {  
                    root.walkDecls(walkDecls(rpx2unit));  
                    const rewriteTag = transforms[platform];  
                    filterPrefersColorScheme(root);  
                    if (rewriteTag) {  
                        root.walkRules(walkRules({  
                            rewriteTag,  
                        }));  
                    }  
                },  
            };  
        },  
    };  
};

就是 遍历 然后 替换

function walkDecls(rpx2unit) {  
    return (decl) => {  
        const { value } = decl;  
        if (value.indexOf('rpx') === -1 && value.indexOf('upx') === -1) {  
            return;  
        }  
        // 如果有 rpx 或者upx, 就调用rpx2unit  
        decl.value = rpx2unit(decl.value);  
    };  
}
const unitRE = new RegExp(`"[^"]+"|'[^']+'|url\\([^)]+\\)|(\\d*\\.?\\d+)[r|u]px`, 'g');  

// 这里的  参数 就是  defaultRpx2Unit的三个参数  
function createRpx2Unit(unit, unitRatio, unitPrecision) {  
    // ignore: rpxCalcIncludeWidth  
    return (val) => val.replace(unitRE, (m, $1) => {  
        if (!$1) {  
            return m;  
        }  
        if (unitRatio === 1) {  
            return `${$1}${unit}`;  
        }  
        // 替换 为 rem  
        // 就是  * unitRatio, 然后 保留unitPrecision 位的小数  
        const value = toFixed(parseFloat($1) * unitRatio, unitPrecision);  
        return value === 0 ? '0' : `${value}${unit}`;  
    });  
}

我是 分割线----------------------------------------------------------------------------------------------------------------------------------------------------------------

vue2 + webpack

再 说一下 vue2的 处理, vue2 是 依托于 vue-cli-service

查看打包结果

写一个 最简单的 项目,
查看 打包后的 结果

 ___CSS_LOADER_EXPORT___.push([  
            module.id,  
            '@charset "UTF-8";\n/**\n  */.ggdxd[data-v-4551d3b2]{height:%?600?%;background-color:pink}',  
            "",  
        ]);

是 内联css ,
而 可以 修改么? 可以 在 vue.config.js中 设置css.extract么?

不可以

查看代码, uni 是 使用 自定义 的 vue-cli-service 插件,
/node_modules/@dcloudio/vue-cli-plugin-uni/index.js

h5 强制要 内联css...

if (process.env.UNI_PLATFORM === 'h5' || process.env.UNI_USING_V3) {  
    options.css.extract = false  
} else {  
    options.css.extract = true  
}

height:%?600?%

在 cli 项目中, 根目录下会有postcss.config.js
实际上 使用了 uni的 一个自定义 插件require('@dcloudio/vue-cli-plugin-uni/packages/postcss')
在 parseWord方法中

if (process.env.UNI_PLATFORM === 'h5') {  
    if (u === 'upx' || u === 'rpx') {  
          // 这里 变成了很奇怪的东西  
      node.value = `%?${num}?%`  
    }  
}

也就是 在编译的产物中, rpx/upx 都转成了 %?${num}?%的 格式

啥时候 转成 px 哦?

是 运行时!

运行时的 转换代码 是哪里来的?

是 通过 webpack的 loader 处理的

我写了一个 webpack 插件, 获取了 resolve 之后的 样式使用的loader

[  
  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/vue-loader/lib/loaders/pitcher.js",  

  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue-style-loader/index.js",  

  "/node_modules/@vue/cli-service/node_modules/css-loader/dist/cjs.js",  
  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/webpack-preprocess-loader/index.js",  
  "/node_modules/postcss-loader/dist/cjs.js",  
  "/node_modules/postcss-loader/dist/cjs.js",  
  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/sass-loader/dist/cjs.js",  
  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/webpack-preprocess-loader/index.js",  
  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/vue-loader/lib/index.js",  
  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/webpack-scoped-loader/index.js",  
  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/wrap-loader/index.js",  
]

为什么会使用这个 h5-vue-style-loader?

用了 alias

/node_modules/@dcloudio/vue-cli-plugin-uni/lib/h5/index.js文件中配置了webpack

resolveLoader: {  
    alias: {  
      'vue-style-loader': resolve('packages/h5-vue-style-loader')  
    }  
  }

在 h5-vue-style-loader中 处理 css模块时,
编译源码是

var code = [  
  '// add the styles to the DOM',  
  'var add = require(' + addStylesClientPath + ').default',  
  'var update = add(' + id + ', content, ' + isProduction + ', ' + JSON.stringify(options) + ');'  
]

动态 插入了 add/update 方法
执行后的返回示例:

// style-loader: Adds some css to the DOM by adding a <style> tag  

// load the styles  
var content = require("........!../node_modules/@dcloudio/vue-cli-plugin-uni/packages/wrap-loader/index.js??clonedRuleSet-45[0].rules[0].use!./App.vue?vue&type=style&index=0&lang=scss&");  

if(content.__esModule) content = content.default;  

if(typeof content === 'string') content = [[module.id, content, '']];  
if(content.locals) module.exports = content.locals;  

// add the styles to the DOM  
var add = require("!../node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue-style-loader/lib/addStylesClient.js").default  
var update = add("43b7a677", content, true, {"sourceMap":false,"shadowMode":false});

也就是 这个 css 模块 变成了 上面的样子

运行时呢?

css模块 代码 注入了
var add = require("!../node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue-style-loader/lib/addStylesClient.js").default

源码是:

var UPX_RE = /%\?([+-]?\d+(\.\d+)?)\?%/g;  
function processCss(css) {  
    var page = getPage();  
    if (typeof uni !== "undefined" && !uni.canIUse("css.var")) {  
        //不支持 css 变量  
        var offset = getWindowOffset();  
        css = css  
            .replace(VAR_STATUS_BAR_HEIGHT, "0px")  
            .replace(VAR_WINDOW_TOP, offset.top + "px")  
            .replace(VAR_WINDOW_BOTTOM, offset.bottom + "px")  
            .replace(VAR_WINDOW_LEFT, "0px")  
            .replace(VAR_WINDOW_RIGHT, "0px");  
    }  
    return css  
        .replace(BODY_SCOPED_RE, page)  
        .replace(BODY_RE, "")  
        .replace(PAGE_SCOPED_RE, "body." + page + " uni-page-body")  
        .replace(/\{[\s\S]+?\}|@media.+?\{/g, function (css) {  
            if (typeof uni === "undefined") {  
                return css;  
            }  
            // 这里 把  %?数值?% 格式的  数字样式 使用 upx2px 计算成px  
            return css.replace(UPX_RE, function (a, b) {  
                return uni.upx2px(b) + "px";  
            });  
        });  
}
继续阅读 »

vue3 + vite

vue3 下 , h5使用rpx 时, 会 编译成 rem

注册 postcss 的 plugin

在/node_modules/@dcloudio/uni-h5-vite/dist/plugin/config.js,
代码是, 用的 是config 这个钩子

 css: {  
    postcss: {  
        plugins: (0, uni_cli_shared_1.initPostcssPlugin)({  
            uniApp: (0, uni_cli_shared_1.parseRpx2UnitOnce)(inputDir, process.env.UNI_PLATFORM),  
        }),  
    },  
},

parseRpx2UnitOnce , 选定使用的单位

exports.parseRpx2UnitOnce = (0, uni_shared_1.once)((inputDir, platform = 'h5') => {  
    // 如果是  h5/app/鸿蒙 会使用 defaultRpx2Unit  
    const rpx2unit = platform === 'h5' || platform === 'app' || platform === 'app-harmony'  
        ? uni_shared_1.defaultRpx2Unit  
        : uni_shared_1.defaultMiniProgramRpx2Unit;  
    const manifestJson = (0, exports.parseManifestJsonOnce)(inputDir);  
    let platformOptions = getPlatformManifestJson(manifestJson, platform);  
    if (platformOptions && platformOptions.rpx) {  
        return (0, shared_1.extend)({}, rpx2unit, platformOptions);  
    }  
    return (0, shared_1.extend)({}, rpx2unit);  
});

都有啥呢 ?

const defaultRpx2Unit = {  
    unit: 'rem',  
    unitRatio: 10 / 320,  
    unitPrecision: 5,  
};  
const defaultMiniProgramRpx2Unit = {  
    unit: 'rpx',  
    unitRatio: 1,  
    unitPrecision: 1,  
};

但是 注意这里 let platformOptions = getPlatformManifestJson(manifestJson, platform);
还是 会从 mainfest中 获取 配置的, 如果有 , 就用 mainfest 中的,
也就是 你可以 在 manifest.json的 h5节点下 , 覆盖 一些 配置 , 例如

    "h5": {  
        "router": {  
            "mode": "history"  
        },  
        "unit": "px",  
        "unitRatio": 0.5,  
        "unitPrecision": 2,  
    }

但是 文档里 , 我没有 找到

哪里 修改的 rpx 2 rem 呢?

在这里 /node_modules/@dcloudio/uni-cli-shared/dist/postcss/plugins/uniapp.js

const uniapp = (opts) => {  
    const platform = process.env.UNI_PLATFORM;  
    const { unit, unitRatio, unitPrecision } = (0, shared_1.extend)({}, defaultUniAppCssProcessorOptions, opts);  
    const rpx2unit = (0, uni_shared_1.createRpx2Unit)(unit, unitRatio, unitPrecision);  
    return {  
        postcssPlugin: 'uni-app',  
        prepare() {  
            return {  
                OnceExit(root) {  
                    root.walkDecls(walkDecls(rpx2unit));  
                    const rewriteTag = transforms[platform];  
                    filterPrefersColorScheme(root);  
                    if (rewriteTag) {  
                        root.walkRules(walkRules({  
                            rewriteTag,  
                        }));  
                    }  
                },  
            };  
        },  
    };  
};

就是 遍历 然后 替换

function walkDecls(rpx2unit) {  
    return (decl) => {  
        const { value } = decl;  
        if (value.indexOf('rpx') === -1 && value.indexOf('upx') === -1) {  
            return;  
        }  
        // 如果有 rpx 或者upx, 就调用rpx2unit  
        decl.value = rpx2unit(decl.value);  
    };  
}
const unitRE = new RegExp(`"[^"]+"|'[^']+'|url\\([^)]+\\)|(\\d*\\.?\\d+)[r|u]px`, 'g');  

// 这里的  参数 就是  defaultRpx2Unit的三个参数  
function createRpx2Unit(unit, unitRatio, unitPrecision) {  
    // ignore: rpxCalcIncludeWidth  
    return (val) => val.replace(unitRE, (m, $1) => {  
        if (!$1) {  
            return m;  
        }  
        if (unitRatio === 1) {  
            return `${$1}${unit}`;  
        }  
        // 替换 为 rem  
        // 就是  * unitRatio, 然后 保留unitPrecision 位的小数  
        const value = toFixed(parseFloat($1) * unitRatio, unitPrecision);  
        return value === 0 ? '0' : `${value}${unit}`;  
    });  
}

我是 分割线----------------------------------------------------------------------------------------------------------------------------------------------------------------

vue2 + webpack

再 说一下 vue2的 处理, vue2 是 依托于 vue-cli-service

查看打包结果

写一个 最简单的 项目,
查看 打包后的 结果

 ___CSS_LOADER_EXPORT___.push([  
            module.id,  
            '@charset "UTF-8";\n/**\n  */.ggdxd[data-v-4551d3b2]{height:%?600?%;background-color:pink}',  
            "",  
        ]);

是 内联css ,
而 可以 修改么? 可以 在 vue.config.js中 设置css.extract么?

不可以

查看代码, uni 是 使用 自定义 的 vue-cli-service 插件,
/node_modules/@dcloudio/vue-cli-plugin-uni/index.js

h5 强制要 内联css...

if (process.env.UNI_PLATFORM === 'h5' || process.env.UNI_USING_V3) {  
    options.css.extract = false  
} else {  
    options.css.extract = true  
}

height:%?600?%

在 cli 项目中, 根目录下会有postcss.config.js
实际上 使用了 uni的 一个自定义 插件require('@dcloudio/vue-cli-plugin-uni/packages/postcss')
在 parseWord方法中

if (process.env.UNI_PLATFORM === 'h5') {  
    if (u === 'upx' || u === 'rpx') {  
          // 这里 变成了很奇怪的东西  
      node.value = `%?${num}?%`  
    }  
}

也就是 在编译的产物中, rpx/upx 都转成了 %?${num}?%的 格式

啥时候 转成 px 哦?

是 运行时!

运行时的 转换代码 是哪里来的?

是 通过 webpack的 loader 处理的

我写了一个 webpack 插件, 获取了 resolve 之后的 样式使用的loader

[  
  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/vue-loader/lib/loaders/pitcher.js",  

  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue-style-loader/index.js",  

  "/node_modules/@vue/cli-service/node_modules/css-loader/dist/cjs.js",  
  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/webpack-preprocess-loader/index.js",  
  "/node_modules/postcss-loader/dist/cjs.js",  
  "/node_modules/postcss-loader/dist/cjs.js",  
  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/sass-loader/dist/cjs.js",  
  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/webpack-preprocess-loader/index.js",  
  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/vue-loader/lib/index.js",  
  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/webpack-scoped-loader/index.js",  
  "/node_modules/@dcloudio/vue-cli-plugin-uni/packages/wrap-loader/index.js",  
]

为什么会使用这个 h5-vue-style-loader?

用了 alias

/node_modules/@dcloudio/vue-cli-plugin-uni/lib/h5/index.js文件中配置了webpack

resolveLoader: {  
    alias: {  
      'vue-style-loader': resolve('packages/h5-vue-style-loader')  
    }  
  }

在 h5-vue-style-loader中 处理 css模块时,
编译源码是

var code = [  
  '// add the styles to the DOM',  
  'var add = require(' + addStylesClientPath + ').default',  
  'var update = add(' + id + ', content, ' + isProduction + ', ' + JSON.stringify(options) + ');'  
]

动态 插入了 add/update 方法
执行后的返回示例:

// style-loader: Adds some css to the DOM by adding a <style> tag  

// load the styles  
var content = require("........!../node_modules/@dcloudio/vue-cli-plugin-uni/packages/wrap-loader/index.js??clonedRuleSet-45[0].rules[0].use!./App.vue?vue&type=style&index=0&lang=scss&");  

if(content.__esModule) content = content.default;  

if(typeof content === 'string') content = [[module.id, content, '']];  
if(content.locals) module.exports = content.locals;  

// add the styles to the DOM  
var add = require("!../node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue-style-loader/lib/addStylesClient.js").default  
var update = add("43b7a677", content, true, {"sourceMap":false,"shadowMode":false});

也就是 这个 css 模块 变成了 上面的样子

运行时呢?

css模块 代码 注入了
var add = require("!../node_modules/@dcloudio/vue-cli-plugin-uni/packages/h5-vue-style-loader/lib/addStylesClient.js").default

源码是:

var UPX_RE = /%\?([+-]?\d+(\.\d+)?)\?%/g;  
function processCss(css) {  
    var page = getPage();  
    if (typeof uni !== "undefined" && !uni.canIUse("css.var")) {  
        //不支持 css 变量  
        var offset = getWindowOffset();  
        css = css  
            .replace(VAR_STATUS_BAR_HEIGHT, "0px")  
            .replace(VAR_WINDOW_TOP, offset.top + "px")  
            .replace(VAR_WINDOW_BOTTOM, offset.bottom + "px")  
            .replace(VAR_WINDOW_LEFT, "0px")  
            .replace(VAR_WINDOW_RIGHT, "0px");  
    }  
    return css  
        .replace(BODY_SCOPED_RE, page)  
        .replace(BODY_RE, "")  
        .replace(PAGE_SCOPED_RE, "body." + page + " uni-page-body")  
        .replace(/\{[\s\S]+?\}|@media.+?\{/g, function (css) {  
            if (typeof uni === "undefined") {  
                return css;  
            }  
            // 这里 把  %?数值?% 格式的  数字样式 使用 upx2px 计算成px  
            return css.replace(UPX_RE, function (a, b) {  
                return uni.upx2px(b) + "px";  
            });  
        });  
}
收起阅读 »

uni.setStorage/getStorage PC浏览器调试没问题 apk安装后加载不了

离线本地存储

使用uni.setStorage/getStorage存读数据,PC调试的时候都没问题。封装apk,安装后,读不出来了。不管是同步还是异步。已经打算自己写xml了。不知有无大佬遇到类似问题。

使用uni.setStorage/getStorage存读数据,PC调试的时候都没问题。封装apk,安装后,读不出来了。不管是同步还是异步。已经打算自己写xml了。不知有无大佬遇到类似问题。

video组件报错,DOMException: The element has no supported sources.

video

在内置浏览器上会报错,运行在真机上就可以

在内置浏览器上会报错,运行在真机上就可以

鸿蒙如何在本地测试 wgt 更新

wgt升级 uni-app鸿蒙开发实践 升级中心

鸿蒙端使用升级中心,发布测试版本后,直接运行到手机上测试 wgt 升级时可能会发现升级不成功,应用重启后还是旧的内容;

这是因为运行到鸿蒙端调试时,升级中心释放 wgt 的逻辑被调试时的热更新覆盖掉了,但是可以使用以下方法测试 wgt 升级:

  1. 使用本地打包,生成安装包

  1. 将打包出来的内容(unpackage/dist/build/app-harmony)直接拖到 DevEco 中运行到升级测试
继续阅读 »

鸿蒙端使用升级中心,发布测试版本后,直接运行到手机上测试 wgt 升级时可能会发现升级不成功,应用重启后还是旧的内容;

这是因为运行到鸿蒙端调试时,升级中心释放 wgt 的逻辑被调试时的热更新覆盖掉了,但是可以使用以下方法测试 wgt 升级:

  1. 使用本地打包,生成安装包

  1. 将打包出来的内容(unpackage/dist/build/app-harmony)直接拖到 DevEco 中运行到升级测试
收起阅读 »