4. Web 客户端开发:从零搭一个控制面板

好,到了这一章,我们终于要跟浏览器打交道了。前面几章我们把 ESP32 的 WebSocket 服务端跑起来了,灯也焊好了,但总得有个界面去点吧?这一章我就带你手写一个 HTML 页面,配上 JavaScript 的 WebSocket 客户端,把那个 LED 灯真正「点」亮。

我个人习惯把 Web 客户端分成三层来看:结构层(HTML)、行为层(JavaScript)、交互层(UI 控件)。这三层拆开讲,你后面改起来也顺手。

本章核心目标: 一个 HTML 页面 + 一段 JS 代码,通过 WebSocket 向 ESP32 发送 "ON" 和 "OFF" 指令,控制板载 LED 亮灭。

4.1 HTML 页面结构设计

先搭骨架。我不喜欢把页面搞得太花哨,嵌入式调试界面嘛,清晰、稳定、响应快才是王道。下面这个结构我用了很多项目,基本没大改过。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP32 LED 控制面板</title>
    <style>
        /* 样式稍后统一讲 */
    </style>
</head>
<body>
    <div id="container">
        <h1>💡 LED 控制器</h1>
        <div id="status">未连接</div>
        <button id="btnOn">开灯</button>
        <button id="btnOff">关灯</button>
        <div id="log"></div>
    </div>
    <script>
        // JavaScript 代码稍后写
    </script>
</body>
</html>

这里有几个关键点:

  • #status:显示连接状态,方便调试。我在项目里吃过亏,没状态指示,连没连上都不知道,傻等半天。
  • #btnOn / #btnOff:两个按钮,一个开一个关。简单粗暴,但够用。
  • #log:日志区,显示发送和接收的消息。这个太重要了,尤其是初期联调的时候。
小建议: 移动端调试时,别忘了加 viewport 标签。不然 iPhone 上按钮会变得很小,点起来很痛苦。

4.2 JavaScript WebSocket 客户端实现

接下来是重头戏。JavaScript 的 WebSocket API 其实很简单,就那么几个事件:onopen、onmessage、onerror、onclose。但怎么组织代码,让它在掉线后能自动重连,这才是经验活。

我曾经在一个智能家居项目里,就因为没做重连机制,用户路由器一重启,面板就永远灰掉了。后来被客户追着骂……嗯,从那以后我的 WebSocket 客户端必带重连。

// WebSocket 客户端完整代码
let socket = null;
let reconnectTimer = null;

function connectWebSocket() {
    const url = 'ws://192.168.1.100:81';  // 替换成你的 ESP32 IP
    socket = new WebSocket(url);

    socket.onopen = function() {
        document.getElementById('status').textContent = '已连接 ✅';
        document.getElementById('status').style.color = 'green';
        clearTimeout(reconnectTimer);
        console.log('WebSocket 连接成功');
    };

    socket.onmessage = function(event) {
        const msg = event.data;
        document.getElementById('log').innerHTML += '<p>收到: ' + msg + '</p>';
        // 如果 ESP32 返回状态,可以在这里更新 UI
    };

    socket.onerror = function(error) {
        console.error('WebSocket 错误:', error);
        document.getElementById('status').textContent = '连接错误 ❌';
    };

    socket.onclose = function() {
        document.getElementById('status').textContent = '已断开,正在重连...';
        document.getElementById('status').style.color = 'orange';
        // 自动重连,间隔 3 秒
        reconnectTimer = setTimeout(connectWebSocket, 3000);
    };
}

// 页面加载时启动连接
window.onload = function() {
    connectWebSocket();
};

这段代码有几个设计细节:

  • 重连机制:onclose 里启动一个 3 秒定时器,重新调用 connectWebSocket()。注意要先 clearTimeout 避免堆积。
  • 状态反馈:每次状态变化都更新 #status 元素,用户一眼就知道当前连接状况。
  • 日志记录:收到消息就追加到 #log 里,方便追踪。
注意: WebSocket 的 URL 是 ws:// 开头,不是 http://。如果你用了 HTTPS 的网页,浏览器会阻止非加密的 WebSocket 连接,这时候得用 wss://。不过本地调试一般用 HTTP,问题不大。

4.3 UI 控件与事件绑定

按钮有了,连接也有了,就差把「点击」和「发送」串起来。事件绑定我推荐用 addEventListener,而不是在 HTML 里写 onclick。为什么?因为 addEventListener 可以绑多个函数,而且代码和结构分离,维护起来清爽。

// 发送指令函数
function sendCommand(cmd) {
    if (socket && socket.readyState === WebSocket.OPEN) {
        socket.send(cmd);
        document.getElementById('log').innerHTML += '<p>发送: ' + cmd + '</p>';
    } else {
        alert('WebSocket 未连接,请等待重连...');
    }
}

// 绑定按钮事件
document.getElementById('btnOn').addEventListener('click', function() {
    sendCommand('ON');
});

document.getElementById('btnOff').addEventListener('click', function() {
    sendCommand('OFF');
});

这里我加了一个 readyState 检查。你想想看,如果用户狂点按钮,但 WebSocket 还没连上,那指令就全丢了。加个判断,没连上就弹个提醒,用户体验好很多。

另外,sendCommand 函数里我顺便把发送的内容也记到日志区。这个习惯是我从一次线上故障里学来的——当时用户说灯没反应,我远程一看日志,发现指令根本没发出去,原来是 IP 地址写错了。

4.4 完整页面整合与测试

把上面三块拼起来,就是一个完整的控制页面。我建议你直接在浏览器里打开这个 HTML 文件(双击就行),不需要任何服务器。WebSocket 是浏览器原生支持的,本地文件也能用。

测试步骤很简单:

  1. 确保 ESP32 已上电,并且 WebSocket 服务端在运行(上一章的内容)。
  2. 打开 HTML 页面,看状态是否显示「已连接 ✅」。
  3. 点击「开灯」,看日志区出现「发送: ON」,同时 LED 亮起。
  4. 点击「关灯」,LED 熄灭,日志出现「发送: OFF」。
常见问题: 如果连接不上,先 ping 一下 ESP32 的 IP,确认网络通。再检查防火墙有没有封 81 端口。我遇到过最奇葩的一次,是公司网络把 WebSocket 的 Upgrade 请求给拦截了……

4.5 知识结构图:Web 客户端三层架构

下面这张图帮你理清 HTML、JavaScript、UI 控件之间的关系。我画图喜欢用这种分层结构,一眼就能看出数据怎么流动。

Web 客户端三层架构 结构层 (HTML) #container 布局容器 #status 状态显示 | #btnOn/#btnOff 按钮 | #log 日志区 行为层 (JavaScript) connectWebSocket() 建立连接 sendCommand() 发送指令 | onmessage 接收响应 交互层 (UI 控件 + 事件) addEventListener 绑定点击事件 点击按钮 → 调用 sendCommand → WebSocket 发送

说白了,整个流程就是:你点按钮 → JS 捕获点击 → 通过 WebSocket 发指令 → ESP32 收到后控制 LED。反过来,ESP32 也可以主动发消息给页面,比如上报当前 LED 状态,然后 JS 更新 UI。这就是全双工通信的魅力。

扩展思路: 如果你觉得两个按钮太单调,可以换成滑块(<input type="range">)来控制 LED 亮度,或者加一个颜色选择器来控制 RGB 灯带。原理一样,只是把发送的指令从 "ON"/"OFF" 改成数值或颜色值。

4.6 本章小结

这一章我们干了三件事:

  • 用 HTML 搭了一个控制面板的骨架,包含状态区、按钮区和日志区。
  • 用 JavaScript 实现了 WebSocket 客户端,带自动重连和状态反馈。
  • 用 addEventListener 把按钮点击和指令发送绑定起来。

你现在应该能在浏览器里打开这个页面,成功控制 ESP32 上的 LED 了。如果还没成功,别急,先检查 IP 地址和端口,再打开浏览器控制台(F12)看有没有报错。调试嘛,就是个排雷的过程,我当年也是这么过来的。