Browse Source

feat: 添加`MQTT`示例 (#1021)

* feat: add mqtt client demo use mqtt.js and emqx broker
EchoByte 1 year ago
parent
commit
69bc6602d0
7 changed files with 613 additions and 13 deletions
  1. 9 1
      .vscode/settings.json
  2. 1 0
      locales/en.yaml
  3. 1 0
      locales/zh-CN.yaml
  4. 1 0
      package.json
  5. 198 8
      pnpm-lock.yaml
  6. 11 4
      src/router/modules/able.ts
  7. 392 0
      src/views/able/mqtt-client.vue

+ 9 - 1
.vscode/settings.json

@@ -35,5 +35,13 @@
   "i18n-ally.sourceLanguage": "en",
   "i18n-ally.displayLanguage": "zh-CN",
   "i18n-ally.enabledFrameworks": ["vue"],
-  "iconify.excludes": ["el"]
+  "iconify.excludes": ["el"],
+  "vsmqtt.brokerProfiles": [
+    {
+      "name": "broker.emqx.io",
+      "host": "broker.emqx.io/mqtt",
+      "port": 1883,
+      "clientId": "vsmqtt_client_db34"
+    }
+  ]
 }

+ 1 - 0
locales/en.yaml

@@ -93,6 +93,7 @@ menus:
   hsVideoFrame: Video Frame Capture
   hsWavesurfer: Audio Visualization
   hsRipple: Ripple
+  hsMqtt: Mqtt Client
   hsOptimize: Debounce、Throttle、Copy、Longpress Directives
   hsVerify: Captcha
   hsWatermark: Water Mark

+ 1 - 0
locales/zh-CN.yaml

@@ -93,6 +93,7 @@ menus:
   hsVideoFrame: 视频帧截取-wasm版
   hsWavesurfer: 音频可视化
   hsRipple: 波纹(Ripple)
+  hsMqtt: MQTT客户端(mqtt)
   hsOptimize: 防抖、截流、复制、长按指令
   hsVerify: 图形验证码
   hsWatermark: 水印

+ 1 - 0
package.json

@@ -79,6 +79,7 @@
     "localforage": "^1.10.0",
     "mint-filter": "^4.0.3",
     "mitt": "^3.0.1",
+    "mqtt": "^5.5.0",
     "nprogress": "^0.2.0",
     "path": "^0.12.7",
     "pinia": "^2.1.7",

+ 198 - 8
pnpm-lock.yaml

@@ -98,6 +98,9 @@ dependencies:
   mitt:
     specifier: ^3.0.1
     version: 3.0.1
+  mqtt:
+    specifier: ^5.5.0
+    version: 5.5.0
   nprogress:
     specifier: ^0.2.0
     version: 0.2.0
@@ -2347,6 +2350,13 @@ packages:
     resolution: {integrity: sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==}
     dev: true
 
+  /@types/readable-stream@4.0.11:
+    resolution: {integrity: sha512-R3eUMUTTKoIoaz7UpYLxvZCrOmCRPRbAmoDDHKcimTEySltaJhF8hLzj4+EzyDifiX5eK6oDQGSfmNnXjxZzYQ==}
+    dependencies:
+      '@types/node': 20.11.30
+      safe-buffer: 5.1.2
+    dev: false
+
   /@types/semver@7.5.8:
     resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
     dev: true
@@ -2379,6 +2389,12 @@ packages:
     resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
     dev: false
 
+  /@types/ws@8.5.10:
+    resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
+    dependencies:
+      '@types/node': 20.11.30
+    dev: false
+
   /@types/yargs-parser@21.0.3:
     resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
     dev: false
@@ -3127,6 +3143,13 @@ packages:
     dev: false
     optional: true
 
+  /abort-controller@3.0.0:
+    resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
+    engines: {node: '>=6.5'}
+    dependencies:
+      event-target-shim: 5.0.1
+    dev: false
+
   /acorn-globals@6.0.0:
     resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==}
     dependencies:
@@ -3423,10 +3446,23 @@ packages:
     resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
     dev: true
 
+  /base64-js@1.5.1:
+    resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+    dev: false
+
   /binary-extensions@2.3.0:
     resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
     engines: {node: '>=8'}
 
+  /bl@6.0.12:
+    resolution: {integrity: sha512-EnEYHilP93oaOa2MnmNEjAcovPS3JlQZOyzGXi3EyEpPhm9qWvdDp7BmAVEVusGzp8LlwQK56Av+OkDoRjzE0w==}
+    dependencies:
+      '@types/readable-stream': 4.0.11
+      buffer: 6.0.3
+      inherits: 2.0.4
+      readable-stream: 4.5.2
+    dev: false
+
   /boolbase@1.0.0:
     resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
     dev: true
@@ -3487,6 +3523,13 @@ packages:
     resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
     dev: false
 
+  /buffer@6.0.3:
+    resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+    dependencies:
+      base64-js: 1.5.1
+      ieee754: 1.2.1
+    dev: false
+
   /bundle-import@0.0.1:
     resolution: {integrity: sha512-L0z0iPX7t7ff5eZsK7oMEH+Ly2lzJczFKPHwrta6X8SF64a20R3wOrAOYK1MzHZVaWWugg9qlSTVfVwqvQJ2dw==}
     dependencies:
@@ -3786,6 +3829,10 @@ packages:
     engines: {node: '>= 10'}
     dev: true
 
+  /commist@3.2.0:
+    resolution: {integrity: sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==}
+    dev: false
+
   /compare-func@2.0.0:
     resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==}
     dependencies:
@@ -3804,6 +3851,16 @@ packages:
   /concat-map@0.0.1:
     resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
 
+  /concat-stream@2.0.0:
+    resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
+    engines: {'0': node >= 6.0}
+    dependencies:
+      buffer-from: 1.1.2
+      inherits: 2.0.4
+      readable-stream: 3.6.2
+      typedarray: 0.0.6
+    dev: false
+
   /confbox@0.1.3:
     resolution: {integrity: sha512-eH3ZxAihl1PhKfpr4VfEN6/vUd87fmgb6JkldHgg/YR6aEBhW63qUDgzP2Y6WM0UumdsYp5H3kibalXAdHfbgg==}
     requiresBuild: true
@@ -4898,6 +4955,11 @@ packages:
       es5-ext: 0.10.64
     dev: false
 
+  /event-target-shim@5.0.1:
+    resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
+    engines: {node: '>=6'}
+    dev: false
+
   /eventemitter3@4.0.7:
     resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
     dev: false
@@ -4906,6 +4968,11 @@ packages:
     resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
     dev: true
 
+  /events@3.3.0:
+    resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
+    engines: {node: '>=0.8.x'}
+    dev: false
+
   /execa@5.1.1:
     resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
     engines: {node: '>=10'}
@@ -4981,6 +5048,14 @@ packages:
     resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
     dev: true
 
+  /fast-unique-numbers@8.0.13:
+    resolution: {integrity: sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==}
+    engines: {node: '>=16.1.0'}
+    dependencies:
+      '@babel/runtime': 7.24.1
+      tslib: 2.6.2
+    dev: false
+
   /fastest-levenshtein@1.0.16:
     resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
     engines: {node: '>= 4.9.1'}
@@ -5420,6 +5495,10 @@ packages:
     hasBin: true
     dev: true
 
+  /help-me@5.0.0:
+    resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
+    dev: false
+
   /hey-listen@1.0.8:
     resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
     dev: false
@@ -5508,6 +5587,10 @@ packages:
       safer-buffer: 2.1.2
     dev: false
 
+  /ieee754@1.2.1:
+    resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+    dev: false
+
   /ignore@5.3.1:
     resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
     engines: {node: '>= 4'}
@@ -6237,6 +6320,10 @@ packages:
     engines: {node: '>=14'}
     dev: false
 
+  /js-sdsl@4.3.0:
+    resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==}
+    dev: false
+
   /js-tokens@4.0.0:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
 
@@ -6592,7 +6679,6 @@ packages:
   /lru-cache@10.2.0:
     resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
     engines: {node: 14 || >=16.14}
-    dev: true
 
   /lru-cache@5.1.1:
     resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -6730,7 +6816,6 @@ packages:
 
   /minimist@1.2.8:
     resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
-    dev: true
 
   /minipass@3.3.6:
     resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
@@ -6795,6 +6880,43 @@ packages:
     resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==}
     dev: false
 
+  /mqtt-packet@9.0.0:
+    resolution: {integrity: sha512-8v+HkX+fwbodsWAZIZTI074XIoxVBOmPeggQuDFCGg1SqNcC+uoRMWu7J6QlJPqIUIJXmjNYYHxBBLr1Y/Df4w==}
+    dependencies:
+      bl: 6.0.12
+      debug: 4.3.4
+      process-nextick-args: 2.0.1
+    transitivePeerDependencies:
+      - supports-color
+    dev: false
+
+  /mqtt@5.5.0:
+    resolution: {integrity: sha512-8WGQpjEzABcCh/UwLj7XaEoLsSLHJrJK3YLu57fJoLUBdz6zPGFdxWK3i1YYdPqKDu0sU6YHt+tly/qdLsLiyg==}
+    engines: {node: '>=16.0.0'}
+    hasBin: true
+    dependencies:
+      '@types/readable-stream': 4.0.11
+      '@types/ws': 8.5.10
+      commist: 3.2.0
+      concat-stream: 2.0.0
+      debug: 4.3.4
+      help-me: 5.0.0
+      lru-cache: 10.2.0
+      minimist: 1.2.8
+      mqtt-packet: 9.0.0
+      number-allocator: 1.0.14
+      readable-stream: 4.5.2
+      reinterval: 1.1.0
+      rfdc: 1.3.1
+      split2: 4.2.0
+      worker-timers: 7.1.4
+      ws: 8.16.0
+    transitivePeerDependencies:
+      - bufferutil
+      - supports-color
+      - utf-8-validate
+    dev: false
+
   /mri@1.2.0:
     resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
     engines: {node: '>=4'}
@@ -6928,6 +7050,15 @@ packages:
       boolbase: 1.0.0
     dev: true
 
+  /number-allocator@1.0.14:
+    resolution: {integrity: sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==}
+    dependencies:
+      debug: 4.3.4
+      js-sdsl: 4.3.0
+    transitivePeerDependencies:
+      - supports-color
+    dev: false
+
   /nwsapi@2.2.7:
     resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==}
     dev: false
@@ -7990,6 +8121,10 @@ packages:
     engines: {node: '>=6'}
     dev: false
 
+  /process-nextick-args@2.0.1:
+    resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
+    dev: false
+
   /process@0.11.10:
     resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
     engines: {node: '>= 0.6.0'}
@@ -8069,7 +8204,17 @@ packages:
       string_decoder: 1.3.0
       util-deprecate: 1.0.2
     dev: false
-    optional: true
+
+  /readable-stream@4.5.2:
+    resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+    dependencies:
+      abort-controller: 3.0.0
+      buffer: 6.0.3
+      events: 3.3.0
+      process: 0.11.10
+      string_decoder: 1.3.0
+    dev: false
 
   /readdirp@3.6.0:
     resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
@@ -8081,6 +8226,10 @@ packages:
     resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
     dev: false
 
+  /reinterval@1.1.0:
+    resolution: {integrity: sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==}
+    dev: false
+
   /require-directory@2.1.1:
     resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
     engines: {node: '>=0.10.0'}
@@ -8149,7 +8298,6 @@ packages:
 
   /rfdc@1.3.1:
     resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==}
-    dev: true
 
   /rimraf@3.0.2:
     resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
@@ -8229,11 +8377,14 @@ packages:
     dependencies:
       queue-microtask: 1.2.3
 
+  /safe-buffer@5.1.2:
+    resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+    dev: false
+
   /safe-buffer@5.2.1:
     resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
     requiresBuild: true
     dev: false
-    optional: true
 
   /safer-buffer@2.1.2:
     resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@@ -8441,7 +8592,6 @@ packages:
   /split2@4.2.0:
     resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
     engines: {node: '>= 10.x'}
-    dev: true
 
   /sprintf-js@1.0.3:
     resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
@@ -8525,7 +8675,6 @@ packages:
     dependencies:
       safe-buffer: 5.2.1
     dev: false
-    optional: true
 
   /strip-ansi@6.0.1:
     resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
@@ -9051,7 +9200,6 @@ packages:
 
   /tslib@2.6.2:
     resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
-    dev: true
 
   /type-check@0.4.0:
     resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
@@ -9095,6 +9243,10 @@ packages:
       is-typedarray: 1.0.0
     dev: false
 
+  /typedarray@0.0.6:
+    resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
+    dev: false
+
   /typeit@8.8.3:
     resolution: {integrity: sha512-K7nChkj6iyylUi713VBDULUXXLF0pfB6nFPVhNnXTKO2An7NzVz5fjoAHk2FAC3TFLiSnU+QsqhDmap17oBELw==}
     requiresBuild: true
@@ -9663,6 +9815,31 @@ packages:
     engines: {node: '>=0.8'}
     dev: false
 
+  /worker-timers-broker@6.1.4:
+    resolution: {integrity: sha512-y3D+Yfj37lrItEMIlcfCm/IRueYtYKgpLlTG2wgTIZ9PSw0n/K4kweilgk3gTC4ahbQNVGT90lU+Rf7W4M5bsw==}
+    dependencies:
+      '@babel/runtime': 7.24.1
+      fast-unique-numbers: 8.0.13
+      tslib: 2.6.2
+      worker-timers-worker: 7.0.67
+    dev: false
+
+  /worker-timers-worker@7.0.67:
+    resolution: {integrity: sha512-0ZP2+v2fyiiiGaCEdWxMRUk5YxGFwWdRGB12ZfQy13vw8/27Xd+MW3ua56qlcM30nzjpddXXzLuEpHhGW+Pz7w==}
+    dependencies:
+      '@babel/runtime': 7.24.1
+      tslib: 2.6.2
+    dev: false
+
+  /worker-timers@7.1.4:
+    resolution: {integrity: sha512-8PRtiPAyeYukrY+iOUL+0tq4Zn5qyCHrTqFTtHxcESfIxGyulxNwyzQkybrYBKhnMWmx0bku3wxRfE1hts5R6Q==}
+    dependencies:
+      '@babel/runtime': 7.24.1
+      tslib: 2.6.2
+      worker-timers-broker: 6.1.4
+      worker-timers-worker: 7.0.67
+    dev: false
+
   /wrap-ansi@6.2.0:
     resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
     engines: {node: '>=8'}
@@ -9731,6 +9908,19 @@ packages:
         optional: true
     dev: false
 
+  /ws@8.16.0:
+    resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
+    engines: {node: '>=10.0.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: '>=5.0.2'
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+    dev: false
+
   /xe-utils@3.5.22:
     resolution: {integrity: sha512-rhWuM2fVXsdrqUtyjRAsYTDLvVzYSSZqg/uCRyDqcw96AweB1AWsnTJZyl9g68ejnP0xhxfSoNBSrIcOeX1yxA==}
     dev: false

+ 11 - 4
src/router/modules/able.ts

@@ -10,13 +10,21 @@ export default {
     rank: able
   },
   children: [
+    {
+      path: "/components/mqtt-client",
+      name: "MqttClient",
+      component: () => import("@/views/able/mqtt-client.vue"),
+      meta: {
+        title: $t("menus.hsMqtt"),
+        extraIcon: "IF-pure-iconfont-new svg"
+      }
+    },
     {
       path: "/able/verify",
       name: "Verify",
       component: () => import("@/views/able/verify.vue"),
       meta: {
-        title: $t("menus.hsVerify"),
-        extraIcon: "IF-pure-iconfont-new svg"
+        title: $t("menus.hsVerify")
       }
     },
     {
@@ -56,8 +64,7 @@ export default {
       name: "Ripple",
       component: () => import("@/views/able/ripple.vue"),
       meta: {
-        title: $t("menus.hsRipple"),
-        extraIcon: "IF-pure-iconfont-new svg"
+        title: $t("menus.hsRipple")
       }
     },
     {

+ 392 - 0
src/views/able/mqtt-client.vue

@@ -0,0 +1,392 @@
+<script setup lang="ts">
+// vue 3 + vite use MQTT.js refer to https://github.com/mqttjs/MQTT.js/issues/1269
+import mqtt from "mqtt";
+import { reactive, ref, onUnmounted } from "vue";
+
+const protocol = location.protocol === "https:" ? "wss" : "ws";
+const port = protocol === "wss" ? 8084 : 8083;
+
+// https://github.com/mqttjs/MQTT.js#qos
+const qosList = [0, 1, 2];
+
+const connection = reactive({
+  protocol,
+  host: "broker.emqx.io",
+  port,
+  clientId: "emqx_vue3_" + Math.random().toString(16).substring(2, 8),
+  username: "emqx_test",
+  password: "emqx_test",
+  clean: true,
+  connectTimeout: 30 * 1000, // ms
+  reconnectPeriod: 4000 // ms
+  // for more options and details, please refer to https://github.com/mqttjs/MQTT.js#mqttclientstreambuilder-options
+});
+
+// 订阅 topic/mqttx 主题
+const subscription = ref({
+  topic: "topic/mqttx",
+  qos: 0 as any
+});
+
+// 发布 topic/browser 主题
+const publish = ref({
+  topic: "topic/browser",
+  qos: 0 as any,
+  payload: '{ "msg": "Hello, I am browser." }'
+});
+
+let client = ref({
+  connected: false
+} as mqtt.MqttClient);
+
+const receivedMessages = ref("");
+const subscribedSuccess = ref(false);
+const btnLoadingType = ref("");
+const retryTimes = ref(0);
+
+const initData = () => {
+  client.value = {
+    connected: false
+  } as mqtt.MqttClient;
+  retryTimes.value = 0;
+  btnLoadingType.value = "";
+  subscribedSuccess.value = false;
+};
+
+const handleOnReConnect = () => {
+  retryTimes.value += 1;
+  if (retryTimes.value > 5) {
+    try {
+      client.value.end();
+      initData();
+      console.log("connection maxReconnectTimes limit, stop retry");
+    } catch (error) {
+      console.log("handleOnReConnect catch error:", error);
+    }
+  }
+};
+
+const createConnection = () => {
+  try {
+    btnLoadingType.value = "connect";
+    const { protocol, host, port, ...options } = connection;
+    const connectUrl = `${protocol}://${host}:${port}/mqtt`;
+    // 连接MQTT 服务器
+    client.value = mqtt.connect(connectUrl, options);
+
+    if (client.value.on) {
+      // https://github.com/mqttjs/MQTT.js#event-connect
+      client.value.on("connect", () => {
+        btnLoadingType.value = "";
+        console.log("connection successful");
+      });
+
+      // https://github.com/mqttjs/MQTT.js#event-reconnect
+      client.value.on("reconnect", handleOnReConnect);
+
+      // https://github.com/mqttjs/MQTT.js#event-error
+      client.value.on("error", error => {
+        console.log("connection error:", error);
+      });
+
+      // https://github.com/mqttjs/MQTT.js#event-message
+      client.value.on("message", (topic: string, message) => {
+        receivedMessages.value = receivedMessages.value.concat(
+          message.toString()
+        );
+        console.log(`received message: ${message} from topic: ${topic}`);
+      });
+    }
+  } catch (error) {
+    btnLoadingType.value = "";
+    console.log("mqtt.connect error:", error);
+  }
+};
+
+// subscribe topic
+// https://github.com/mqttjs/MQTT.js#mqttclientsubscribetopictopic-arraytopic-object-options-callback
+const doSubscribe = () => {
+  btnLoadingType.value = "subscribe";
+  const { topic, qos } = subscription.value;
+  client.value.subscribe(
+    topic,
+    { qos },
+    (error: Error, granted: mqtt.ISubscriptionGrant[]) => {
+      btnLoadingType.value = "";
+      if (error) {
+        console.log("subscribe error:", error);
+        return;
+      }
+      subscribedSuccess.value = true;
+      console.log("subscribe successfully:", granted);
+    }
+  );
+};
+
+// unsubscribe topic
+// https://github.com/mqttjs/MQTT.js#mqttclientunsubscribetopictopic-array-options-callback
+const doUnSubscribe = () => {
+  btnLoadingType.value = "unsubscribe";
+  const { topic, qos } = subscription.value;
+  client.value.unsubscribe(topic, { qos }, error => {
+    btnLoadingType.value = "";
+    subscribedSuccess.value = false;
+    if (error) {
+      console.log("unsubscribe error:", error);
+      return;
+    }
+    console.log(`unsubscribed topic: ${topic}`);
+  });
+};
+
+// publish message
+// https://github.com/mqttjs/MQTT.js#mqttclientpublishtopic-message-options-callback
+const doPublish = () => {
+  btnLoadingType.value = "publish";
+  const { topic, qos, payload } = publish.value;
+  client.value.publish(topic, payload, { qos }, error => {
+    btnLoadingType.value = "";
+    if (error) {
+      console.log("publish error:", error);
+      return;
+    }
+    console.log(`published message: ${payload}`);
+  });
+};
+
+// disconnect
+// https://github.com/mqttjs/MQTT.js#mqttclientendforce-options-callback
+const destroyConnection = () => {
+  if (client.value.connected) {
+    btnLoadingType.value = "disconnect";
+    try {
+      client.value.end(false, () => {
+        initData();
+        console.log("disconnected successfully");
+      });
+    } catch (error) {
+      btnLoadingType.value = "";
+      console.log("disconnect error:", error);
+    }
+  }
+};
+
+const handleProtocolChange = (value: string) => {
+  connection.port = value === "wss" ? 8084 : 8083;
+};
+
+onUnmounted(() => {
+  try {
+    if (client.value.end) {
+      client.value.end();
+      console.log("disconnected successfully");
+    }
+  } catch (error) {
+    console.log(error);
+  }
+});
+</script>
+
+<template>
+  <el-card shadow="never" :body-style="{ padding: '20px' }">
+    <template #header>
+      基于
+      <el-link
+        type="primary"
+        :underline="false"
+        href="https://github.com/mqttjs/MQTT.js"
+        target="_blank"
+      >
+        MQTT.js
+      </el-link>
+      和 免费的公共MQTT代理
+      <el-link
+        type="primary"
+        :underline="false"
+        href="broker.emqx.io"
+        target="_blank"
+      >
+        EMQX
+      </el-link>
+      实现的一套 MQTT 客户端。
+    </template>
+    <template #default>
+      <el-card shadow="never">
+        <h1>设置</h1>
+        <el-form label-position="top" :model="connection">
+          <el-row :gutter="20">
+            <el-col :span="8">
+              <el-form-item prop="protocol" label="协议">
+                <el-select
+                  v-model="connection.protocol"
+                  @change="handleProtocolChange"
+                >
+                  <el-option label="ws://" value="ws" />
+                  <el-option label="wss://" value="wss" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item prop="host" label="主机">
+                <el-input v-model="connection.host" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item prop="port" label="端口">
+                <el-input
+                  v-model.number="connection.port"
+                  type="number"
+                  placeholder="8083/8084"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item prop="clientId" label="客户端ID">
+                <el-input v-model="connection.clientId" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item prop="username" label="用户名">
+                <el-input v-model="connection.username" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item prop="password" label="密码">
+                <el-input v-model="connection.password" />
+              </el-form-item>
+            </el-col>
+
+            <el-col :span="24">
+              <el-button
+                type="primary"
+                :disabled="client.connected"
+                :loading="btnLoadingType === 'connect'"
+                @click="createConnection"
+              >
+                {{ client.connected ? "已连接" : "连接" }}
+              </el-button>
+
+              <el-button
+                v-if="client.connected"
+                type="danger"
+                :loading="btnLoadingType === 'disconnect'"
+                @click="destroyConnection"
+              >
+                断开连接
+              </el-button>
+            </el-col>
+          </el-row>
+        </el-form>
+      </el-card>
+      <el-card shadow="never" class="mt-4">
+        <h1>订阅</h1>
+        <el-form label-position="top" :model="subscription">
+          <el-row :gutter="20" :align="'middle'">
+            <el-col :span="8">
+              <el-form-item prop="topic" label="主题">
+                <el-input
+                  v-model="subscription.topic"
+                  :disabled="subscribedSuccess"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item prop="qos" label="通信质量">
+                <el-select
+                  v-model="subscription.qos"
+                  :disabled="subscribedSuccess"
+                >
+                  <el-option
+                    v-for="qos in qosList"
+                    :key="qos"
+                    :label="qos"
+                    :value="qos"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row>
+            <el-col>
+              <el-button
+                type="primary"
+                class="sub-btn"
+                :loading="btnLoadingType === 'subscribe'"
+                :disabled="!client.connected || subscribedSuccess"
+                @click="doSubscribe"
+              >
+                {{ subscribedSuccess ? "已订阅" : "订阅" }}
+              </el-button>
+              <el-button
+                v-if="subscribedSuccess"
+                type="primary"
+                class="sub-btn"
+                :loading="btnLoadingType === 'unsubscribe'"
+                :disabled="!client.connected"
+                @click="doUnSubscribe"
+              >
+                取消订阅
+              </el-button>
+            </el-col>
+          </el-row>
+        </el-form>
+      </el-card>
+      <el-card shadow="never" class="mt-4">
+        <h1>发布</h1>
+        <el-form label-position="top" :model="publish">
+          <el-row :gutter="20">
+            <el-col :span="8">
+              <el-form-item prop="topic">
+                <template #label>
+                  <span>主题</span>
+                  <el-text type="info" size="small">
+                    可将订阅主题设置为topic/browser,测试MQTT的自发自收。
+                  </el-text>
+                </template>
+                <el-input v-model="publish.topic" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item prop="payload" label="有效载荷">
+                <el-input v-model="publish.payload" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item prop="qos" label="通信质量">
+                <el-select v-model="publish.qos">
+                  <el-option
+                    v-for="qos in qosList"
+                    :key="qos"
+                    :label="qos"
+                    :value="qos"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+        <el-col :span="24">
+          <el-button
+            type="primary"
+            :loading="btnLoadingType === 'publish'"
+            :disabled="!client.connected"
+            @click="doPublish"
+          >
+            发布
+          </el-button>
+        </el-col>
+      </el-card>
+      <el-card shadow="never" class="mt-4">
+        <h1>接收</h1>
+        <el-col :span="24">
+          <el-input
+            v-model="receivedMessages"
+            type="textarea"
+            :rows="3"
+            readonly
+          />
+        </el-col>
+      </el-card>
+    </template>
+  </el-card>
+</template>