ESP32 Web 服务入门:WebServer 网页控制、mDNS 域名与 REST API

L2 收官:Wi-Fi 第一课让设备「能上网」,本篇让它「被访问」——手机浏览器打开一个页面,点按钮控制 ESP32 上的 LED。这就是 L2 验收项目「手机浏览器控制 LED」的完整答案,也是智能家居面板、桌面潮玩 Web 配置界面的原型。


一、WebServer 三分钟跑通

#include <WiFi.h>
#include <WebServer.h>

WebServer server(80);              // 80 端口

void handleRoot() {
  server.send(200, "text/html", "<h1>ESPDIY</h1><a href='/on'>开灯</a> <a href='/off'>关灯</a>");
}
void handleOn()  { digitalWrite(2, HIGH); server.sendHeader("Location", "/"); server.send(303); }
void handleOff() { digitalWrite(2, LOW);  server.sendHeader("Location", "/"); server.send(303); }

void setup() {
  pinMode(2, OUTPUT);
  WiFi.begin("你的SSID", "你的密码");
  while (WiFi.status() != WL_CONNECTED) delay(300);

  server.on("/", handleRoot);      // 注册路由
  server.on("/on", handleOn);
  server.on("/off", handleOff);
  server.begin();

  Serial.printf("访问: http://%s\n", WiFi.localIP().toString().c_str());
}

void loop() {
  server.handleClient();           // 必须持续调用(非阻塞)
}

浏览器访问串口打印的 IP → 点「开灯」→ LED 亮。核心机制:路由注册 + loop 轮询 handleClient(延续非阻塞范式,一次请求处理几毫秒,不碍事)。


二、mDNS:告别记 IP

IP 地址记不住也容易变。mDNS 让局域网设备通过 espdiy.local 这样的域名互访(手机/电脑原生支持):

#include <ESPmDNS.h>

void setup() {
  // ... Wi-Fi 连接成功后:
  if (MDNS.begin("espdiy")) {          // 初始化,域名 = espdiy.local
    Serial.println("mDNS 已启动: http://espdiy.local");
  }
  MDNS.addService("http", "tcp", 80);  // 广播 HTTP 服务
}

手机浏览器输入 http://espdiy.local 直达。一个名字只对应一台设备——量产产品用序列号后缀(espdiy-a3f2)防撞名。 局限:仅同一局域网内有效;个别安卓浏览器对 .local 支持不完整,保留 IP 访问作为后备。


三、进阶:REST API + 网页 AJAX(现代做法)

上面的整页跳转是「网页 1.0」。物联网标准做法:网页静态托管 + REST JSON 接口 + AJAX 局部刷新:

// REST 接口:GET /api/status 返回状态 JSON
void handleApiStatus() {
  JsonDocument doc;
  doc["led"] = digitalRead(2) == HIGH;
  doc["uptime"] = millis() / 1000;
  String out;
  serializeJson(doc, out);
  server.send(200, "application/json", out);
}

// REST 接口:POST /api/led 切换状态
void handleApiLed() {
  digitalWrite(2, !digitalRead(2));
  handleApiStatus();               // 返回新状态
}

void setup() {
  // ...
  server.on("/api/status", HTTP_GET, handleApiStatus);
  server.on("/api/led", HTTP_POST, handleApiLed);
}

网页端(存为字符串常量或 SPIFFS 文件)用 fetch 调接口:

<button onclick="fetch('/api/led',{method:'POST'}).then(refresh)">切换 LED</button>
<span id="status"></span>
<script>
async function refresh() {
  const s = await (await fetch('/api/status')).json();
  document.getElementById('status').textContent =
    s.led ? 'ON' : 'OFF' + ' · 运行 ' + s.uptime + 's';
}
setInterval(refresh, 2000);   // 每 2 秒刷新
</script>

这个架构 = Home Assistant 面板、WLED 网页的原型——L3 的 ESPAsyncWebServer 是它的性能加强版。


四、L2 验收项目:完整动线

现在可以串起 L2 全部知识自检:

[Wi-Fi] 事件驱动连接 + 自动重连
   ↓
[mDNS] espdiy.local 访问入口
   ↓
[WebServer] 网页 + REST API
   ↓
[PWM/定时器/中断/ADC/I2C] 业务逻辑(呼吸灯、传感器轮询、按键)
   ↓
手机浏览器 → 控制/查看

全链路跑通 = L2 毕业 🎓


五、避坑速查表

症状根因与对策
网页打不开server.handleClient() 没在 loop 调用
mDNS 解析失败手机连了 5GHz 而 ESP32 在 2.4GHz(部分路由器 AP 隔离);换 IP 直连验证
传参丢失POST body 默认不解析,用 server.arg("xx") 须配 server.on(..., HTTP_POST, ...)
页面越长越卡HTML 字符串拼在闪存( PROGMEM / LittleFS );或换 AsyncWebServer
响应时亮时灭loop 里有长 delay 阻塞了 handleClient(回看定时器篇)
espdiy.local 撞名量产用序列号后缀
想要 HTTPS静态站可用 Cloudflare 反代;本地证书较重,L4 话题

六、延伸阅读

  • 🧭 前作:Wi-Fi 第一课——客户端侧联网
  • ⏭️ L3 方向:ESPAsyncWebServer(异步高性能)、MQTT(物联协议)、ESP-NOW(无路由器通信)
  • 🧭 工具串联:WLED 固件源码就是本篇架构的工业级实现,可作参考阅读

版权声明:本文采用 CC BY-SA 4.0 协议授权。转载与二创请注明出处:ESPDIY 创客工坊 - espdiy.com

ESP32 Web Services Guide: WebServer, mDNS and REST APIs

L2 finale: Wi-Fi First Steps made the device reach the network; this guide makes it reachable — open a page in a phone browser, tap a button, control the LED. It’s the complete answer to the L2 acceptance project and the prototype for smart-home dashboards and web config UIs.


1. WebServer in three minutes

#include <WiFi.h>
#include <WebServer.h>

WebServer server(80);              // port 80

void handleRoot() {
  server.send(200, "text/html", "<h1>ESPDIY</h1><a href='/on'>LED ON</a> <a href='/off'>LED OFF</a>");
}
void handleOn()  { digitalWrite(2, HIGH); server.sendHeader("Location", "/"); server.send(303); }
void handleOff() { digitalWrite(2, LOW);  server.sendHeader("Location", "/"); server.send(303); }

void setup() {
  pinMode(2, OUTPUT);
  WiFi.begin("YOUR_SSID", "YOUR_PASSWORD");
  while (WiFi.status() != WL_CONNECTED) delay(300);

  server.on("/", handleRoot);      // register routes
  server.on("/on", handleOn);
  server.on("/off", handleOff);
  server.begin();

  Serial.printf("Visit: http://%s\n", WiFi.localIP().toString().c_str());
}

void loop() {
  server.handleClient();           // must be called continuously (non-blocking)
}

Open the printed IP in a browser → tap “LED ON” → the LED lights. Core mechanism: route registration + handleClient polled in loop (continuing the non-blocking paradigm — each request takes milliseconds and disturbs nothing).


2. mDNS: forget the IP

IPs are forgettable and change. mDNS lets LAN devices address each other as espdiy.local (natively supported by phones and PCs):

#include <ESPmDNS.h>

void setup() {
  // ... after Wi-Fi connects:
  if (MDNS.begin("espdiy")) {          // hostname = espdiy.local
    Serial.println("mDNS started: http://espdiy.local");
  }
  MDNS.addService("http", "tcp", 80);  // advertise the HTTP service
}

Type http://espdiy.local in the phone browser. One name per device — production units use serial-number suffixes (espdiy-a3f2) to avoid collisions. Limits: same LAN only; some Android browsers handle .local poorly — keep the IP as a fallback.


3. Advanced: REST API + AJAX (the modern way)

Full-page links are “web 1.0”. The IoT-standard architecture: static page hosting + REST JSON endpoints + AJAX partial refresh:

// REST: GET /api/status returns the status JSON
void handleApiStatus() {
  JsonDocument doc;
  doc["led"] = digitalRead(2) == HIGH;
  doc["uptime"] = millis() / 1000;
  String out;
  serializeJson(doc, out);
  server.send(200, "application/json", out);
}

// REST: POST /api/led toggles the LED
void handleApiLed() {
  digitalWrite(2, !digitalRead(2));
  handleApiStatus();               // return the new state
}

void setup() {
  // ...
  server.on("/api/status", HTTP_GET, handleApiStatus);
  server.on("/api/led", HTTP_POST, handleApiLed);
}

The page (as a string constant or a SPIFFS file) calls the API with fetch:

<button onclick="fetch('/api/led',{method:'POST'}).then(refresh)">Toggle LED</button>
<span id="status"></span>
<script>
async function refresh() {
  const s = await (await fetch('/api/status')).json();
  document.getElementById('status').textContent =
    (s.led ? 'ON' : 'OFF') + ' · up ' + s.uptime + 's';
}
setInterval(refresh, 2000);   // refresh every 2s
</script>

This architecture = the prototype of Home Assistant dashboards and the WLED web UI — L3’s ESPAsyncWebServer is its high-performance upgrade.


4. The L2 acceptance project: full chain

Now string together all of L2 for the self-check:

[Wi-Fi] event-driven connect + auto-reconnect
   ↓
[mDNS] espdiy.local entry point
   ↓
[WebServer] page + REST API
   ↓
[PWM/timers/interrupts/ADC/I2C] business logic (breathing LED, sensor polling, buttons)
   ↓
Phone browser → control & monitor

Full chain working = L2 graduation 🎓


5. Pitfall cheat sheet

SymptomCause and fix
Page never loadsserver.handleClient() missing from loop
mDNS doesn’t resolvePhone on 5GHz while the ESP32 is on 2.4GHz (AP isolation on some routers); verify via IP
POST body “missing”Body isn’t parsed by default — use server.arg("xx") with the right route method
Page gets sluggishHTML strings eat RAM (PROGMEM / LittleFS); or move to AsyncWebServer
Intermittent responseA long delay in loop blocks handleClient (revisit timers)
espdiy.local collisionUse serial-number suffixes in production
Wanting HTTPSCloudflare reverse proxy for static sites; local certs are an L4 topic

6. Further reading

  • 🧭 Previous: Wi-Fi First Steps — the client side
  • ⏭️ L3 directions: ESPAsyncWebServer (async performance), MQTT (IoT protocol), ESP-NOW (routerless comms)
  • 🧭 Tool tie-in: the WLED firmware is the industrial-grade version of this architecture — excellent reference reading

License: CC BY-SA 4.0. Repost & remix with attribution: ESPDIY Maker Lab - espdiy.com