mqtt-client.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. <script setup lang="ts">
  2. // vue 3 + vite use MQTT.js refer to https://github.com/mqttjs/MQTT.js/issues/1269
  3. import * as mqtt from "mqtt/dist/mqtt.min";
  4. import { reactive, ref, onUnmounted } from "vue";
  5. const protocol = location.protocol === "https:" ? "wss" : "ws";
  6. const port = protocol === "wss" ? 8084 : 8083;
  7. // https://github.com/mqttjs/MQTT.js#qos
  8. const qosList = [0, 1, 2];
  9. const connection = reactive({
  10. protocol,
  11. host: "broker.emqx.io",
  12. port,
  13. clientId: "emqx_vue3_" + Math.random().toString(16).substring(2, 8),
  14. username: "emqx_test",
  15. password: "emqx_test",
  16. clean: true,
  17. connectTimeout: 30 * 1000, // ms
  18. reconnectPeriod: 4000 // ms
  19. // for more options and details, please refer to https://github.com/mqttjs/MQTT.js#mqttclientstreambuilder-options
  20. });
  21. // 订阅 topic/mqttx 主题
  22. const subscription = ref({
  23. topic: "topic/mqttx",
  24. qos: 0 as any
  25. });
  26. // 发布 topic/browser 主题
  27. const publish = ref({
  28. topic: "topic/browser",
  29. qos: 0 as any,
  30. payload: '{ "msg": "Hello, I am browser." }'
  31. });
  32. let client = ref({
  33. connected: false
  34. } as mqtt.MqttClient);
  35. const receivedMessages = ref("");
  36. const subscribedSuccess = ref(false);
  37. const btnLoadingType = ref("");
  38. const retryTimes = ref(0);
  39. const initData = () => {
  40. client.value = {
  41. connected: false
  42. } as mqtt.MqttClient;
  43. retryTimes.value = 0;
  44. btnLoadingType.value = "";
  45. subscribedSuccess.value = false;
  46. };
  47. const handleOnReConnect = () => {
  48. retryTimes.value += 1;
  49. if (retryTimes.value > 5) {
  50. try {
  51. client.value.end();
  52. initData();
  53. console.log("connection maxReconnectTimes limit, stop retry");
  54. } catch (error) {
  55. console.log("handleOnReConnect catch error:", error);
  56. }
  57. }
  58. };
  59. const createConnection = () => {
  60. try {
  61. btnLoadingType.value = "connect";
  62. const { protocol, host, port, ...options } = connection;
  63. const connectUrl = `${protocol}://${host}:${port}/mqtt`;
  64. // 连接MQTT 服务器
  65. client.value = mqtt.connect(connectUrl, options);
  66. if (client.value.on) {
  67. // https://github.com/mqttjs/MQTT.js#event-connect
  68. client.value.on("connect", () => {
  69. btnLoadingType.value = "";
  70. console.log("connection successful");
  71. });
  72. // https://github.com/mqttjs/MQTT.js#event-reconnect
  73. client.value.on("reconnect", handleOnReConnect);
  74. // https://github.com/mqttjs/MQTT.js#event-error
  75. client.value.on("error", error => {
  76. console.log("connection error:", error);
  77. });
  78. // https://github.com/mqttjs/MQTT.js#event-message
  79. client.value.on("message", (topic: string, message) => {
  80. receivedMessages.value = receivedMessages.value.concat(
  81. message.toString()
  82. );
  83. console.log(`received message: ${message} from topic: ${topic}`);
  84. });
  85. }
  86. } catch (error) {
  87. btnLoadingType.value = "";
  88. console.log("mqtt.connect error:", error);
  89. }
  90. };
  91. // subscribe topic
  92. // https://github.com/mqttjs/MQTT.js#mqttclientsubscribetopictopic-arraytopic-object-options-callback
  93. const doSubscribe = () => {
  94. btnLoadingType.value = "subscribe";
  95. const { topic, qos } = subscription.value;
  96. client.value.subscribe(
  97. topic,
  98. { qos },
  99. (error: Error, granted: mqtt.ISubscriptionGrant[]) => {
  100. btnLoadingType.value = "";
  101. if (error) {
  102. console.log("subscribe error:", error);
  103. return;
  104. }
  105. subscribedSuccess.value = true;
  106. console.log("subscribe successfully:", granted);
  107. }
  108. );
  109. };
  110. // unsubscribe topic
  111. // https://github.com/mqttjs/MQTT.js#mqttclientunsubscribetopictopic-array-options-callback
  112. const doUnSubscribe = () => {
  113. btnLoadingType.value = "unsubscribe";
  114. const { topic, qos } = subscription.value;
  115. client.value.unsubscribe(topic, { qos }, error => {
  116. btnLoadingType.value = "";
  117. subscribedSuccess.value = false;
  118. if (error) {
  119. console.log("unsubscribe error:", error);
  120. return;
  121. }
  122. console.log(`unsubscribed topic: ${topic}`);
  123. });
  124. };
  125. // publish message
  126. // https://github.com/mqttjs/MQTT.js#mqttclientpublishtopic-message-options-callback
  127. const doPublish = () => {
  128. btnLoadingType.value = "publish";
  129. const { topic, qos, payload } = publish.value;
  130. client.value.publish(topic, payload, { qos }, error => {
  131. btnLoadingType.value = "";
  132. if (error) {
  133. console.log("publish error:", error);
  134. return;
  135. }
  136. console.log(`published message: ${payload}`);
  137. });
  138. };
  139. // disconnect
  140. // https://github.com/mqttjs/MQTT.js#mqttclientendforce-options-callback
  141. const destroyConnection = () => {
  142. if (client.value.connected) {
  143. btnLoadingType.value = "disconnect";
  144. try {
  145. client.value.end(false, () => {
  146. initData();
  147. console.log("disconnected successfully");
  148. });
  149. } catch (error) {
  150. btnLoadingType.value = "";
  151. console.log("disconnect error:", error);
  152. }
  153. }
  154. };
  155. const handleProtocolChange = (value: string) => {
  156. connection.port = value === "wss" ? 8084 : 8083;
  157. };
  158. onUnmounted(() => {
  159. try {
  160. if (client.value.end) {
  161. client.value.end();
  162. console.log("disconnected successfully");
  163. }
  164. } catch (error) {
  165. console.log(error);
  166. }
  167. });
  168. </script>
  169. <template>
  170. <el-card shadow="never" :body-style="{ padding: '20px' }">
  171. <template #header>
  172. <div>
  173. 基于
  174. <el-link
  175. type="primary"
  176. underline="never"
  177. href="https://github.com/mqttjs/MQTT.js"
  178. target="_blank"
  179. >
  180. MQTT.js
  181. </el-link>
  182. 和 免费的公共MQTT代理
  183. <el-link
  184. type="primary"
  185. underline="never"
  186. href="broker.emqx.io"
  187. target="_blank"
  188. >
  189. EMQX
  190. </el-link>
  191. 实现的一套 MQTT 客户端
  192. </div>
  193. <el-link
  194. class="mt-2"
  195. href="https://github.com/pure-admin/vue-pure-admin/blob/main/src/views/able/mqtt-client.vue"
  196. target="_blank"
  197. >
  198. 代码位置 src/views/able/mqtt-client.vue
  199. </el-link>
  200. </template>
  201. <el-card shadow="never">
  202. <h1>设置</h1>
  203. <el-form label-position="top" :model="connection">
  204. <el-row :gutter="20">
  205. <el-col :span="8">
  206. <el-form-item prop="protocol" label="协议">
  207. <el-select
  208. v-model="connection.protocol"
  209. @change="handleProtocolChange"
  210. >
  211. <el-option label="ws://" value="ws" />
  212. <el-option label="wss://" value="wss" />
  213. </el-select>
  214. </el-form-item>
  215. </el-col>
  216. <el-col :span="8">
  217. <el-form-item prop="host" label="主机">
  218. <el-input v-model="connection.host" />
  219. </el-form-item>
  220. </el-col>
  221. <el-col :span="8">
  222. <el-form-item prop="port" label="端口">
  223. <el-input
  224. v-model.number="connection.port"
  225. type="number"
  226. placeholder="8083/8084"
  227. />
  228. </el-form-item>
  229. </el-col>
  230. <el-col :span="8">
  231. <el-form-item prop="clientId" label="客户端ID">
  232. <el-input v-model="connection.clientId" />
  233. </el-form-item>
  234. </el-col>
  235. <el-col :span="8">
  236. <el-form-item prop="username" label="用户名">
  237. <el-input v-model="connection.username" />
  238. </el-form-item>
  239. </el-col>
  240. <el-col :span="8">
  241. <el-form-item prop="password" label="密码">
  242. <el-input v-model="connection.password" />
  243. </el-form-item>
  244. </el-col>
  245. <el-col :span="24">
  246. <el-button
  247. type="primary"
  248. :disabled="client.connected"
  249. :loading="btnLoadingType === 'connect'"
  250. @click="createConnection"
  251. >
  252. {{ client.connected ? "已连接" : "连接" }}
  253. </el-button>
  254. <el-button
  255. v-if="client.connected"
  256. type="danger"
  257. :loading="btnLoadingType === 'disconnect'"
  258. @click="destroyConnection"
  259. >
  260. 断开连接
  261. </el-button>
  262. </el-col>
  263. </el-row>
  264. </el-form>
  265. </el-card>
  266. <el-card shadow="never" class="mt-4">
  267. <h1>订阅</h1>
  268. <el-form label-position="top" :model="subscription">
  269. <el-row :gutter="20" :align="'middle'">
  270. <el-col :span="8">
  271. <el-form-item prop="topic" label="主题">
  272. <el-input
  273. v-model="subscription.topic"
  274. :disabled="subscribedSuccess"
  275. />
  276. </el-form-item>
  277. </el-col>
  278. <el-col :span="8">
  279. <el-form-item prop="qos" label="通信质量">
  280. <el-select
  281. v-model="subscription.qos"
  282. :disabled="subscribedSuccess"
  283. >
  284. <el-option
  285. v-for="qos in qosList"
  286. :key="qos"
  287. :label="qos"
  288. :value="qos"
  289. />
  290. </el-select>
  291. </el-form-item>
  292. </el-col>
  293. </el-row>
  294. <el-row>
  295. <el-col>
  296. <el-button
  297. type="primary"
  298. class="sub-btn"
  299. :loading="btnLoadingType === 'subscribe'"
  300. :disabled="!client.connected || subscribedSuccess"
  301. @click="doSubscribe"
  302. >
  303. {{ subscribedSuccess ? "已订阅" : "订阅" }}
  304. </el-button>
  305. <el-button
  306. v-if="subscribedSuccess"
  307. type="primary"
  308. class="sub-btn"
  309. :loading="btnLoadingType === 'unsubscribe'"
  310. :disabled="!client.connected"
  311. @click="doUnSubscribe"
  312. >
  313. 取消订阅
  314. </el-button>
  315. </el-col>
  316. </el-row>
  317. </el-form>
  318. </el-card>
  319. <el-card shadow="never" class="mt-4">
  320. <h1>发布</h1>
  321. <el-form label-position="top" :model="publish">
  322. <el-row :gutter="20">
  323. <el-col :span="8">
  324. <el-form-item prop="topic">
  325. <template #label>
  326. <span>主题</span>
  327. <el-text type="info" size="small">
  328. 可将订阅主题设置为topic/browser,测试MQTT的自发自收。
  329. </el-text>
  330. </template>
  331. <el-input v-model="publish.topic" />
  332. </el-form-item>
  333. </el-col>
  334. <el-col :span="8">
  335. <el-form-item prop="payload" label="有效载荷">
  336. <el-input v-model="publish.payload" />
  337. </el-form-item>
  338. </el-col>
  339. <el-col :span="8">
  340. <el-form-item prop="qos" label="通信质量">
  341. <el-select v-model="publish.qos">
  342. <el-option
  343. v-for="qos in qosList"
  344. :key="qos"
  345. :label="qos"
  346. :value="qos"
  347. />
  348. </el-select>
  349. </el-form-item>
  350. </el-col>
  351. </el-row>
  352. </el-form>
  353. <el-col :span="24">
  354. <el-button
  355. type="primary"
  356. :loading="btnLoadingType === 'publish'"
  357. :disabled="!client.connected"
  358. @click="doPublish"
  359. >
  360. 发布
  361. </el-button>
  362. </el-col>
  363. </el-card>
  364. <el-card shadow="never" class="mt-4">
  365. <h1>接收</h1>
  366. <el-col :span="24">
  367. <el-input
  368. v-model="receivedMessages"
  369. type="textarea"
  370. :rows="3"
  371. readonly
  372. />
  373. </el-col>
  374. </el-card>
  375. </el-card>
  376. </template>