news 2026/9/30 1:16:28

ESP8266+Arduino IDE+巴法云:零基础物联网远程控制实战教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP8266+Arduino IDE+巴法云:零基础物联网远程控制实战教程

这段时间后台收到最多的问题就是:新手想学物联网,第一块板子怎么选?第一个云平台用哪个?我给的答案基本没变过——板子选ESP8266,云平台选巴法云,开发环境用Arduino IDE。这套组合是真正能让你在半小时内看到数据的方案。ESP8266自带WiFi,成本十几块,Arduino IDE不用学复杂的SDK和命令行,巴法云免费开放、MQTT协议通用,三者搭在一起,从零到点灯再到远程控制,全程没有多余的坑。这篇文章我就把自己实际跑通的全过程拆开讲清楚,包括环境搭建、平台配置、代码实现,以及我踩过的那些报错和坑。想少走弯路的朋友,建议从头看到尾,尤其是最后面的调试部分,很多问题不是你代码写错了,而是配置细节没对齐。

1. 为什么选这套组合:ESP8266 + Arduino IDE + 巴法云

1.1 ESP8266在物联网入门中的地位

我逛社区时经常看到有新手问“ESP8266模块能连接SPI接口芯片吗”,答案是能,而且不止能接SPI,UART、I2C、PWM、ADC这些外设接口它全都有。很多人把ESP8266当成单纯的“WiFi转串口模块”,其实它本身就是一个完整的MCU,内部有CPU、Flash、SRAM,可以独立跑逻辑代码,同时兼顾WiFi通信。也就是说,你可以用一片ESP8266完成数据采集、逻辑处理、网络上传一整套流程,对入门项目来说完全够用。

不过这颗芯片有个核心特点必须记住:它的引脚是3.3V逻辑电平,不是5V。新手最常见的翻车点就是直接拿Arduino Uno的5V引脚去给ESP8266供电,或者用5V的传感器信号去怼GPIO,轻则乱码、重启,重则直接烧掉。所以选ESP8266做项目,先养成一个习惯:凡是接外部模块,先确认电平,再确认供电电流。WiFi模块启动瞬间电流能到300mA以上,用USB转TTL的3.3V输出一般带不动,最好单独用AMS1117-3.3稳压模块供电。

1.2 为什么不用其他开发环境

乐鑫官方其实提供了完整的ESP8266 SDK,也可以用PlatformIO、VS Code搭配插件开发,但我个人还是建议新手从Arduino IDE入手。原因是Arduino IDE把底层的WiFi联网、TCP/IP协议栈、硬件驱动全部封装成了简单易用的库函数,比如WiFi.begin()、client.publish(),你不需要关心TCP握手和AT指令的细节,先把业务逻辑跑通,培养起“程序能跑通”的信心,后面再回头研究底层也不会迟。

相比PlatformIO那一套复杂的工程配置和依赖管理,Arduino IDE只需要“开发板管理器”里装一次ESP8266支持包,之后选对开发板型号就能编译上传。它唯一的缺点是代码提示比较弱,工程大了管理起来吃力,但对教学、快速原型验证、个人DIY场景来说,这个缺点可以忽略。你甚至可以把它理解成“带轮子的脚手架”,先把东西立起来,比什么都重要。

1.3 巴法云能做什么

巴法云是一个物联网消息中转平台,核心能力就是MQTT消息的发布和订阅。设备端连接巴法云的MQTT服务器,往某个“主题”发消息,另一端订阅同一个“主题”,就能收到这条消息。这个模型非常像微信群:主题就是群名,订阅者就是群成员,发布消息就像在群里发言,所有在群里的人都能看到。

选择巴法云而不是自己搭服务器,最直接的原因是省事。个人搭一个MQTT Broker不难,难的是公网IP、域名备案、服务器维护、TLS证书这一整套事情,折腾下来比写设备端代码还费时间。巴法云把这些全部托管了,你在网页上注册账号、创建主题、复制私钥,就能直接用。它对个人开发者免费,控制台里还能直接发测试消息,或者查看设备上行的数据,对学习和产品原型验证都非常方便。

2. 动手前的准备工作

2.1 Arduino IDE安装与ESP8266开发板包配置

第一步是安装Arduino IDE。这里有个容易弄混的点:Arduino IDE有好几个版本,老旧的1.8.x和新版2.x都可以用,但建议用2.x以上的新版本,界面现代化,自带的串口监视器更好用,编译速度也快。安装包直接去Arduino官网下载,选对应操作系统的版本,一路默认安装就行,不需要额外配置什么环境变量。

装好IDE之后,要让它认识ESP8266这颗芯片。打开Arduino IDE,找到“文件 -> 首选项”,在弹出的对话框里找到“附加开发板管理器网址”,填入下面这个地址:

http://arduino.esp8266.com/stable/package_esp8266com_index.json

填好后点“确定”,然后打开“工具 -> 开发板 -> 开发板管理器”,在搜索框里输入ESP8266,看到“esp8266 by ESP8266 Community”这个条目,点安装。这个包有几十MB,国内网络可能需要等一会儿,耐心等它装完。装完后在“工具 -> 开发板”菜单里往下拉,就能看到一大堆ESP8266系列的板卡型号了。

选板子的时候注意别选错。如果你用的是NodeMCU,就选“NodeMCU 1.0 (ESP-12E Module)”,如果你用的是ESP-01裸模块,选“Generic ESP8266 Module”。板子选错会导致GPIO引脚定义对不上,程序能编译但运行逻辑完全不对。我第一次用ESP-01就栽在这上面,默认选了NodeMCU,结果IO0引脚控制继电器,实际操作的却是另一个引脚,排查了半天。

2.2 巴法云账号注册与主题创建

巴法云的使用很简单,但很多新手会忽略一个重要步骤:先注册账号拿私钥,私钥就是设备的“身份证”。打开巴法云官网,注册登录后进入控制台,找到“私钥”这一栏,复制保存好。这个私钥就是一长串字符,后面代码里MQTT连接的clientId要填它,填错就连不上服务器。

接着在控制台里创建主题。巴法云的主题命名规则建议用“英文+数字”的组合,比如LED001、DHT001,别用中文和特殊符号。主题就相当于消息通道的名称,你可以为每个设备或功能建一个主题,比如控制灯的LED001、上报温度的DHT001。创建好之后,控制台的主题列表里就能看到,旁边一般会有“发布消息”的测试入口,方便你直接在网页上往主题里塞一条消息,观察设备有没有反应。

有个细节需要强调:巴法云的主题有两种使用方式,一种走MQTT协议,端口9501;另一种走TCP协议,端口也是9501,但报文格式更原始。我们这次用MQTT方式,所以只需要关心MQTT相关参数:服务器地址填bemfa.com,端口9501,clientId填你的私钥,用户名和密码一般保持默认,或者参考巴法云官网当前的鉴权说明填。MQTT协议本身支持用户名密码,但巴法云简化了这一点,具体以官网文档为准。

2.3 硬件清单与接线注意事项

做最简单的连接测试,你需要的硬件非常少:一块ESP8266开发板,推荐NodeMCU或者Wemos D1 mini,这两种都带USB转串口芯片,插上电脑就能下载程序;一根MicroUSB数据线,注意要选能传数据的那种,很多线只能充电不能传数据;一个LED灯珠加一个220欧限流电阻,如果用开发板自带的LED则可以省掉接线。

接线时务必确认供电和电平。NodeMCU的vin脚可以直接接5V电源,板载稳压电路会转成3.3V供给芯片;但如果是从面包板给ESP8266裸片供电,就必须接3.3V,不能接5V。另外,ESP8266的GPIO口输出电流有限,驱动LED没问题,驱动继电器、电机这类大功率器件必须通过三极管或光耦隔离,别想当然直接焊上去。

如果你想把ESP8266和STM32配合使用,那多半是STM32作为主控,ESP8266作为WiFi透传模块。这时候ESP8266需要刷成AT固件,STM32的串口TX接ESP8266的RX,STM32的RX接ESP8266的TX,两个设备要共地。网上有很多“esp8266与stm32连接原理图”,核心就是这一句话:交叉接串口、共地、电平匹配。ESP8266是3.3V,STM32如果是5V供电,串口引脚也大概率是5V逻辑,这时就需要加电平转换芯片或者串电阻分压,否则通信不稳定,甚至烧坏ESP8266。

3. 编程实现:从点灯到WS2812灯带

3.1 安装PubSubClient和NeoPixel库

代码部分我分两层来讲,第一层是连接巴法云必备的MQTT客户端库,第二层是后面扩展灯带效果要用到的WS2812控制库。

MQTT客户端我推荐PubSubClient,这是Arduino生态里最常用的轻量级MQTT库,RAM占用小,API简单。打开“工具 -> 管理库”,搜索PubSubClient,找到作者为Nick O'Leary的版本,直接安装。另一个可选库是ArduinoJson,它用来解析JSON格式消息,如果你只是收发简单字符串,可以不装,但做复杂协议时很有用。

如果要做灯带控制,还需要装Adafruit NeoPixel库。在库管理器里搜索NeoPixel,找到Adafruit NeoPixel,安装。这个库负责生成WS2812灯带需要的时序波形,你只需要告诉它“第几个灯珠、什么颜色、多少亮度”,它就能把时序处理好。我用这个库做过很多灯带项目,渐变、海浪、滚动这些效果本质都是“按时间顺序改变灯珠的颜色值”,并不神秘。

库装好之后,记得不要同时装一堆名字相似的库。比如你搜NeoPixel可能会出来好几个,如果装的不是Adafruit官方版,编译时会出现大量奇怪的报错。判断方法很简单,看库的作者和版本号,Adafruit NeoPixel by Adafruit,版本号1.x以上,基本没问题。

3.2 连接巴法云的核心代码框架

我先给出一份最小可用的连接代码。这份代码只做两件事:连接WiFi、连接巴法云的MQTT服务器。先跑通这个,再往里面加业务逻辑。

#include <ESP8266WiFi.h> #include <PubSubClient.h> const char* ssid = "你的WiFi名称"; const char* password = "你的WiFi密码"; const char* mqtt_server = "bemfa.com"; const int mqtt_port = 9501; const char* clientId = "你的巴法云私钥"; const char* topic = "LED001"; WiFiClient espClient; PubSubClient client(espClient); void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("WiFi connected"); client.setServer(mqtt_server, mqtt_port); client.setCallback(callback); } void callback(char* topic, byte* payload, unsigned int length) { String message; for (int i = 0; i < length; i++) { message += (char)payload[i]; } Serial.print("收到消息: "); Serial.println(message); } void reconnect() { while (!client.connected()) { Serial.print("尝试连接巴法云..."); if (client.connect(clientId)) { Serial.println("已连接"); client.subscribe(topic); } else { Serial.print("失败, rc="); Serial.print(client.state()); Serial.println(" 5秒后重试"); delay(5000); } } } void loop() { if (!client.connected()) { reconnect(); } client.loop(); }

这段代码里有几个关键点需要拆开讲。

第一,clientId必须填巴法云私钥,这是验证设备身份的凭证。很多新手把private key当成用户名,把clientId留空,结果一直报连接失败。PubSubClient的connect函数第一个参数就是clientId,你要把私钥填进去。

第二,setCallback这个函数是收到主题消息后的回调入口。只要设备订阅了某个主题,巴法云往这个主题发消息,你的callback函数就会被触发。回调函数里的payload就是消息内容,一般按字符串处理。

第三,loop里必须调用client.loop(),这会处理底层的MQTT收发任务。很多人把client.loop()漏掉或者放在某个if分支里,结果消息要么收不到,要么收几次就卡死。这个函数应该无条件放在主循环开头。

3.3 发布与订阅:做出一个能被远程控制的灯

跑通了基础连接,接下来做一个“远程点灯”的完整例子。我在巴法云控制台创建了一个主题LED001,然后在代码里订阅它,当收到“on”就点亮板载LED,收到“off”就熄灭。

#include <ESP8266WiFi.h> #include <PubSubClient.h> const char* ssid = "你的WiFi名称"; const char* password = "你的WiFi密码"; const char* mqtt_server = "bemfa.com"; const int mqtt_port = 9501; const char* clientId = "你的巴法云私钥"; const char* topic = "LED001"; const int ledPin = LED_BUILTIN; WiFiClient espClient; PubSubClient client(espClient); void callback(char* topic, byte* payload, unsigned int length) { String message; for (int i = 0; i < length; i++) { message += (char)payload[i]; } Serial.print("收到命令: "); Serial.println(message); if (message == "on") { digitalWrite(ledPin, LOW); // 开发板LED低电平点亮 } else if (message == "off") { digitalWrite(ledPin, HIGH); // 高电平熄灭 } } void setup() { Serial.begin(115200); pinMode(ledPin, OUTPUT); digitalWrite(ledPin, HIGH); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("WiFi connected"); client.setServer(mqtt_server, mqtt_port); client.setCallback(callback); if (client.connect(clientId)) { client.subscribe(topic); } } void loop() { if (!client.connected()) { if (client.connect(clientId)) { client.subscribe(topic); } delay(5000); } client.loop(); }

编译上传后,打开巴法云控制台,找到LED001这个主题,在发布消息输入框里输入“on”,点发送,开发板上的LED应该熄灭;输入“off”,LED恢复点亮。整个过程不需要接触板子,只要有网,随时随地能控制,这就是物联网最基础的“遥控器”。

这里要提醒一下:NodeMCU板载LED是低电平点亮,所以on对应digitalWrite(LOW)。如果你的板子不一样,逻辑要反过来,或者外接LED用高电平点亮。别死记代码,要看原理图。

3.4 升级实战:ESP8266无线控制WS2812灯带

热词里经常看到“ESP8266无线控制WS2812灯带源码包,含渐变/海浪/滚动等10+灯光效果”,这个方向我非常推荐。WS2812灯带是数字RGB灯珠,每一颗灯珠内部有驱动IC,只需要一根数据线就能串联控制几十上百颗灯珠。很多桌面氛围灯、电视背光、橱柜灯都是这么做的。

接线很简单:WS2812的DIN接ESP8266的一个GPIO口,我习惯用D4(板载LED旁边那个引脚),VCC接5V,GND接GND。注意WS2812的数据电平是5V,而ESP8266输出3.3V,短距离(1米以内)一般能正常工作,但灯带长了以后会出现颜色错乱、闪烁,这时最好加一个电平转换模块,或者把3.3V数据线通过一个1K电阻接到5V上做上拉,亲测能改善。

控制WS2812的核心库就是前面提到的Adafruit NeoPixel。先定义一个灯带对象,指定数据引脚、灯珠数量和灯珠类型:

#include <Adafruit_NeoPixel.h> #define PIN D4 #define NUMPIXELS 30 Adafruit_NeoPixel strip(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

然后写几个灯效函数,本质就是“给不同位置的灯珠填不同颜色,调用strip.show()刷新”。我把常用的渐变效果写出来:

void colorWipe(uint32_t color, int wait) { for (int i = 0; i < strip.numPixels(); i++) { strip.setPixelColor(i, color); strip.show(); delay(wait); } } void rainbowCycle(int wait) { for (long j = 0; j < 256; j++) { for (int i = 0; i < strip.numPixels(); i++) { strip.setPixelColor(i, wheel((i + j) & 255)); } strip.show(); delay(wait); } } uint32_t wheel(byte pos) { if (pos < 85) { return strip.Color(pos * 3, 255 - pos * 3, 0); } else if (pos < 170) { pos -= 85; return strip.Color(255 - pos * 3, 0, pos * 3); } else { pos -= 170; return strip.Color(0, pos * 3, 255 - pos * 3); } }

然后在MQTT回调里根据消息切换效果:

if (message == "rainbow") { rainbowCycle(20); } else if (message == "wipe") { colorWipe(strip.Color(255, 0, 0), 30); }

把接收指令和处理灯效放在一起,你就能在巴法云控制台发送“rainbow”、“wipe”这样的字符串,让灯带实时切换效果。实际项目里我一般把消息分成两类:一类是即时指令,比如“切换模式”;另一类是参数配置,比如“亮度50”“颜色#FF0000”,这类用JSON格式解析比较省事,ArduinoJson库这时候就派上用场了。

4. 调试现场:常见问题与排查技巧

4.1 连接失败速查表

我把这段时间帮人排查过的问题列成一张表,大家直接对着找原因:

现象大概率原因排查方法
串口监视器显示WiFi连接超时SSID或密码不对,或者WiFi是5G频段确认2.4G频段,打印WiFi.status()定位
MQTT连接返回rc=-2服务器地址或端口没配对检查bemfa.com和9501
MQTT连接返回rc=-4clientId认证失败确认私钥完整复制,不能带空格
能连上但收不到消息订阅的主题名和控制台不一致逐字核对主题名称
收到乱码编码不匹配或不支持中文主题改用英文主题,消息内容用UTF-8
板子频繁重启供电不足或电流不够更换5V/2A电源,检查线材压降
一段时间后掉线且不回连没有实现自动重连逻辑参考3.2的reconnect()

有个很隐蔽的问题:PubSubClient库的MQTT连接回调状态码,不同版本含义略有不同。实际调试时,与其猜状态码,不如在reconnect()里把client.state()打出来,然后对照PubSubClient源码里的枚举定义去查,这样最准确。新手遇到rc=-2别慌,多半是网络层面根本连不上服务器,先看看ESP8266有没有成功连上路由器,再用手机热点试一下,排除局域网防火墙问题。

4.2 掉线重连与心跳保活

物联网设备跑在路由器环境下,WiFi信号抖动、路由器重启、运营商IP变化都可能导致设备断线。断线本身不可怕,可怕的是断线后不会自动恢复。很多人的代码只在setup()里连一次,一旦中途掉线,loop()里没有重连逻辑,设备就“失联”了。

稳妥的做法是loop()里检查client.connected(),连接断开就重连,重连成功再订阅主题。我习惯把这段逻辑放在loop()最前面,同时加一个轻量的运行状态打印:

unsigned long lastReconnectAttempt = 0; void loop() { if (!client.connected()) { long now = millis(); if (now - lastReconnectAttempt > 5000) { lastReconnectAttempt = now; if (client.connect(clientId)) { client.subscribe(topic); } } } client.loop(); }

这样即使设备掉线,也会每隔5秒尝试重新建立连接,不会狂刷重连请求把服务器打爆。MQTT本身有保活心跳机制,PubSubClient会在后台定时发PINGREQ包保持连接,你只需要保证client.loop()被频繁调用,不要在主循环里写delay(10000)这种长时间阻塞的代码。一旦主循环被阻塞,心跳发不出去,服务器判定设备离线,又会导致另一边掉线。

如果需要更精细的掉线原因,可以在巴法云控制台看设备上下线记录,或者自己维护一个“最近一次收到消息时间”的变量,超过几分钟没消息就主动重连,这个技巧用在长连接场景里非常有效。

4.3 给新手的几个避坑建议

最后分享几个我实际踩过、帮别人排查过的经验,每一个都是真实教训。

第一,不要用手机热点长期测试。手机热点会休眠、会切换频段、有连接数量限制,设备表现出的问题你根本没法判断是代码问题还是网络问题。在家用路由器环境测试时,建议关闭路由器的“AP隔离”和“5G优先”功能,确保ESP8266能稳定连上2.4G频段。

第二,串口监视器不是万能的。如果你在loop()里加了Serial.println(),但串口监视器波特率和你代码不一致,看到的就是一堆乱码。默认情况下,我建议代码用115200,串口监视器也选115200。另外,NodeMCU的D3、D4等引脚在启动时有特殊功能,如果在这两个引脚上接了外部设备,可能导致模块无法正常启动,新手最容易忽略这个。

第三,私钥和WiFi密码千万不要硬编码发布到公开仓库。很多人在GitHub上传ESP8266例程时,忘了把私钥改成占位符,结果等于把自家设备控制权交出去了。建议把敏感信息单独放在一个头文件里,比如config.h,然后在.gitignore里忽略它。

第四,关于刷固件。如果你之前的ESP8266是买来当AT模块用的,现在想在Arduino IDE里写代码,理论上不需要刷额外固件,Arduino代码本身就是固件,直接编译上传就会覆盖原固件。但如果哪天你想恢复AT功能,就得用乐鑫的Flash Download Tool重新烧写原厂AT固件,这个操作和“esp8266下载固件”那些教程是对应的。需要留意的是,AT固件和Arduino程序共用同一个Flash区域,只能二选一,不存在“同时保留”的说法。

第五,如果想把巴法云接入STM32,我有两种推荐方案:一种是你现在学的这套,让ESP8266运行MQTT程序连接巴法云,STM32通过串口和ESP8266交换数据;另一种是给STM32配一个ESP-01模块刷AT固件,STM32用AT指令控制ESP8266联网。前者灵活、可控性强,适合数据量大的场景;后者代码简单,但AT指令的解析比较繁琐。两者都可以,但别混着来,否则排查问题时会很痛苦。

以上这些内容是我把个人项目从零到能用的完整记录,代码可以在这个基础上继续扩展。比如把DHT11温湿度传感器接上来,用巴法云的主题周期性上报温度湿度;或者把灯带的效果参数通过巴法云下发,做成一个可远程定制的氛围灯。再往后,还可以用巴法云提供的API接口,做一个简单的网页或小程序控制端,不用装App,打开浏览器就能控制家里的设备。我个人在实际操作中的体会是:ESP8266这套方案的容错率其实很高,只要你把连接流程中每一步都验证清楚,剩下的就是在上面做各种有趣的事情了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 1:16:23

泰勒公式展开全攻略:常见函数泰勒级数推导与实战应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:16:05

uni-app小程序接入百度云人脸识别:从录入到1:N搜索全流程实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:16:05

ZYNQ传统方式移植Linux:从FSBL到根文件系统的完整启动链

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:15:28

CentOS 7 repo源管理:默认源排错、镜像选型与归档源配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:15:27

从 0 到 1000 Star:开源项目的里程碑与下一站

从 0 到 1000 Star&#xff1a;开源项目的里程碑与下一站昨晚深夜&#xff0c;终端 AI CLI 工具的代码仓库右上角数字跳过了 1000。 从最初在本地工作目录随手写下的一个几十行 Bash 包装脚本&#xff0c;到如今拥有多平台预编译二进制、活跃的 issue 讨论区和来自全球数十位贡…

作者头像 李华
网站建设 2026/9/30 1:15:09

支付功能测试七层穿透模型与实战Checklist

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华