4. Web 客户端开发:从零搭一个控制面板
好,到了这一章,我们终于要跟浏览器打交道了。前面几章我们把 ESP32 的 WebSocket 服务端跑起来了,灯也焊好了,但总得有个界面去点吧?这一章我就带你手写一个 HTML 页面,配上 JavaScript 的 WebSocket 客户端,把那个 LED 灯真正「点」亮。
我个人习惯把 Web 客户端分成三层来看:结构层(HTML)、行为层(JavaScript)、交互层(UI 控件)。这三层拆开讲,你后面改起来也顺手。
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里,方便追踪。
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 是浏览器原生支持的,本地文件也能用。
测试步骤很简单:
- 确保 ESP32 已上电,并且 WebSocket 服务端在运行(上一章的内容)。
- 打开 HTML 页面,看状态是否显示「已连接 ✅」。
- 点击「开灯」,看日志区出现「发送: ON」,同时 LED 亮起。
- 点击「关灯」,LED 熄灭,日志出现「发送: OFF」。
4.5 知识结构图:Web 客户端三层架构
下面这张图帮你理清 HTML、JavaScript、UI 控件之间的关系。我画图喜欢用这种分层结构,一眼就能看出数据怎么流动。
说白了,整个流程就是:你点按钮 → 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)看有没有报错。调试嘛,就是个排雷的过程,我当年也是这么过来的。