# 使用 Electron 连接到部署

本文主要介绍如何在 Electron 项目中使用 MQTT (opens new window),完成一个简单的 MQTT 桌面客户端并实现客户端与 MQTT 服务器的连接、订阅、取消订阅、收发消息等功能。

Electron (opens new window) 是由 GitHub 开发的一个开源框架。它允许使用 Node.js(作为后端)和 Chromium (opens new window)(作为前端)完成桌面 GUI 应用程序的开发。Electron 现已被多个开源 Web 应用程序应用于跨平台的桌面端软件开发,著名项目包括 GitHub 的 Atom,微软的 Visual Studio Code,Slack 的桌面应用等。^1 (opens new window)

一个基础的 Electron 包含三个文件:package.json(元数据)、main.js(代码)和 index.html(图形用户界面)。框架由 Electron 可执行文件(Windows 中为 electron.exe、macOS 中为 electron.app、Linux 中为 electron)提供。开发者可以自行添加标志、自定义图标、重命名或编辑 Electron 可执行文件。

# 前提条件

  1. 已经创建了部署,在 部署概览 下可以查看到连接相关的信息,请确保部署状态为运行中。同时你可以使用 WebSocket 测试连接到 MQTT 服务器。
  2. 认证鉴权 > 认证 中设置用户名和密码,用于连接验证。

# 新建项目

新建项目的方式有很多种,以下简单列举几种:

本文为方便快速搭建示例项目,将使用官方提供的 electron quick start 项目模板进行项目初始化构建。

# 安装依赖

通过命令行安装

npm install mqtt --save
1

安装依赖完成后,如需打开控制台进行调试,需要在 main.js 文件中修改代码,将 win.webContents.openDevTools() 取消注释。

// Open the DevTools.
mainWindow.webContents.openDevTools();
1
2

如此时未使用前端构建工具对前端页面进行打包构建的话,无法直接在 renderer.js 中加载到本地已经安装的 MQTT.js 模块。除使用构建工具方法外,还提供另外两种解决方法:

  1. 可以在 webPreferences 中设置 nodeIntegration 为 true,当有此属性时, webview 中将具有 Node 集成, 并且可以使用像 requireprocess 这样的 node APIs 去访问低层系统资源。 Node 集成默认是禁用的。

    const mainWindow = new BrowserWindow({
      width: 800,
      height: 600,
      webPreferences: {
        nodeIntegration: true,
        preload: path.join(__dirname, "preload.js"),
      },
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
  2. 可以在 preload.js 中进行引入 MQTT.js 模块操作。当没有 node integration 时,这个脚本仍然有能力去访问所有的 Node APIs, 但是当这个脚本执行执行完成之后,通过 Node 注入的全局对象(global objects)将会被删除。

# 连接

请在控制台的 部署概览 找到相关的地址以及端口信息,需要注意如果是基础版,端口不是 1883 或 8883 端口,请确认好端口。

# 连接设置

本文将使用 EMQX 提供的 免费公共 MQTT 服务器 (opens new window),该服务基于 EMQX 的 MQTT 物联网云平台 (opens new window) 创建。服务器接入信息如下:

  • Broker: broker.emqx.io(国内可以使用 broker-cn.emqx.io)
  • TCP Port: 1883
  • WebSocket Port: 8083

为更直观表达,示例的关键连接代码将在 renderer.js 文件中编写,并考虑到安全问题,将使用上文中如何引入 MQTT.js 里的方法 2,在 preload.js 文件中通过 Node.js API 的 require 方法加载已安装的 MQTT 模块,并挂载到全局的 window 对象中,这样在 renderer.js 中,便可以直接访问已加载的模块:

  • 引入 MQTT 模块
// preload.js
const mqtt = require("mqtt");
window.mqtt = mqtt;
1
2
3
  • 配置测试 MQTT 模块
// renderer.js
const clientId = "mqttjs_" + Math.random().toString(16).substr(2, 8);

const host = "mqtt://broker.emqx.io:1883";

const options = {
  keepalive: 30,
  clientId: clientId,
  protocolId: "MQTT",
  protocolVersion: 4,
  clean: true,
  reconnectPeriod: 1000,
  connectTimeout: 30 * 1000,
  will: {
    topic: "WillMsg",
    payload: "Connection Closed abnormally..!",
    qos: 0,
    retain: false,
  },
  rejectUnauthorized: false,
};

// 可查看到 mqtt 模块的信息
console.log(mqtt);

console.log("connecting mqtt client");
const client = mqtt.connect(host, options);

client.on("error", (err) => {
  console.log("Connection error: ", err);
  client.end();
});

client.on("reconnect", () => {
  console.log("Reconnecting...");
});

client.on("connect", () => {
  console.log("Client connected:" + clientId);
  client.subscribe("testtopic/electron", {
    qos: 0,
  });
  client.publish("testtopic/electron", "Electron connection demo...!", {
    qos: 0,
    retain: false,
  });
});

client.on("message", (topic, message, packet) => {
  console.log(
    "Received Message: " + message.toString() + "\nOn topic: " + topic
  );
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

可以看到,在编写完以上代码后并且运行该项目后可以在控制台看到以下内容输出:

electronconsole.png

MQTT 模块运行正常。在设置好模块后,我们就可以编写一个简单的 UI 界面来手动输入 MQTT 连接时所需要的配置等,并在点击连接按钮后可以连接到 MQTT 服务器,此外还可以断开连接,订阅主题,收发消息等。

# 应用程序界面

electronui.png

项目完整代码请见:https://github.com/emqx/MQTT-Client-Examples/tree/master/mqtt-client-Electron (opens new window)

# 连接关键代码

let client = null;

const options = {
  keepalive: 30,
  protocolId: "MQTT",
  protocolVersion: 4,
  clean: true,
  reconnectPeriod: 1000,
  connectTimeout: 30 * 1000,
  will: {
    topic: "WillMsg",
    payload: "Connection Closed abnormally..!",
    qos: 0,
    retain: false,
  },
};

function onConnect() {
  const { host, port, clientId, username, password } = connection;
  const connectUrl = `mqtt://${host.value}:${port.value}`;
  options.clientId =
    clientId.value || `mqttjs_${Math.random().toString(16).substr(2, 8)}`;
  options.username = username.value;
  options.password = password.value;
  console.log("connecting mqtt client");
  client = mqtt.connect(connectUrl, options);
  client.on("error", (err) => {
    console.error("Connection error: ", err);
    client.end();
  });
  client.on("reconnect", () => {
    console.log("Reconnecting...");
  });
  client.on("connect", () => {
    console.log("Client connected:" + options.clientId);
    connectBtn.innerText = "Connected";
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 订阅主题

function onSub() {
  if (client.connected) {
    const { topic, qos } = subscriber;
    client.subscribe(
      topic.value,
      { qos: parseInt(qos.value, 10) },
      (error, res) => {
        if (error) {
          console.error("Subscribe error: ", error);
        } else {
          console.log("Subscribed: ", res);
        }
      }
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 取消订阅

function onUnsub() {
  if (client.connected) {
    const { topic } = subscriber;
    client.unsubscribe(topic.value, (error) => {
      if (error) {
        console.error("Unsubscribe error: ", error);
      } else {
        console.log("Unsubscribed: ", topic.value);
      }
    });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 消息发布

function onSend() {
  if (client.connected) {
    const { topic, qos, payload } = publisher;
    client.publish(topic.value, payload.value, {
      qos: parseInt(qos.value, 10),
      retain: false,
    });
  }
}
1
2
3
4
5
6
7
8
9

# 接收消息

// 在 onConnect 函数中
client.on("message", (topic, message) => {
  const msg = document.createElement("div");
  msg.className = "message-body";
  msg.setAttribute("class", "message-body");
  msg.innerText = `${message.toString()}\nOn topic: ${topic}`;
  document.getElementById("article").appendChild(msg);
});
1
2
3
4
5
6
7
8

# 断开连接

function onDisconnect() {
  if (client.connected) {
    client.end();
    client.on("close", () => {
      connectBtn.innerText = "Connect";
      console.log(options.clientId + " disconnected");
    });
  }
}
1
2
3
4
5
6
7
8
9

# 测试验证

此时我们配合一款同样使用 Electron 编写的 MQTT 5.0 客户端工具 - MQTT X (opens new window) 进行消息的收发测试。

使用 MQTT X 向客户端发送一条消息时,可以看到能正常接收到消息:

electronmessage.png

使用自己编写的客户端向 MQTT X 发送一条消息,此时可以看到 MQTT X 也能正常接收到消息:

mqttx.png

# 更多内容

至此, 我们就完成了使用 Electron 创建一个简单的 MQTT 桌面客户端的过程,并模拟了客户端与 MQTT 服务器进行订阅、收发消息、取消订阅以及断开连接的场景。还值得一提的是,因为 Electron 项目同时包含了浏览器环境和 Node.js 环境,所以除 MQTT/TCP 连接外,还可以利用浏览器的 WebSocket API,同时实现 MQTT over WebSocket 的连接,只需修改上述代码中的连接协议和端口即可。具体如何使用 WebSocket 连接 MQTT 服务,可参考我们的博客 使用 WebSocket 连接 MQTT 服务器 (opens new window)。可以在 这里 (opens new window) 下载到示例的源码,同时也可以在 GitHub (opens new window) 上找到更多其他语言的 Demo 示例。