技术深挖(二):Wails 3 跨端通信——前端如何优雅地“等待”扫码成功?

在完成了 Go 后端的硬核逻辑后,压力来到了前端。Mole 的前端并没有复杂的全家桶,而是回归本质,将精力集中在与 Wails 3 的运行时(Runtime)交互上。 本文将重点拆解:前端如何调用后端服务,以及如何处理“激励视频”这种跨端异步逻辑。 一、 桥梁:引入运行时与绑定 在 Wails 3 中,前端不再需要通过 HTTP 接口访问后端,而是通过自动生成的 Bindings。在 main.js 中,这几行代码是整个应用的灵魂: import { Events, Browser } from "@wailsio/runtime"; // 由 Wails 3 自动生成的绑定代码 import { HandleStopFrp, HandleStartFrp, GetDomainURL, } from "../bindings/mole/MoleService"; Events: 核心组件,用于监听后端的主动推送到前端的消息(如日志、状态变更)。 HandleStartFrp 等: 它们看起来是 JS 函数,但执行时会直接触发 Go 后端对应 Service 的方法。 二、 核心实战:扫码激励的异步闭环 Mole 的核心业务是“看广告换带宽”。这就带来一个挑战:用户点击“连接”后,由于没看广告,流程会中断并弹出小程序码。前端如何知道用户什么时候看完了广告? 拦截与判断 我们不能在前端写 if(adWatched),因为前端代码是透明的。逻辑必须在 Go 后端:如果后端判断该用户需要看广告,HandleStartFrp 会返回特定的状态码(如 2)。 状态机:从“连接中”到“等待验证” 看看这段核心的 connect 逻辑: async function connect() { const hint = document.getElementById("hint"); try { const res = await HandleStartFrp(); // 发起 Go 调用 if (res.code === 1) { // 情况 A: 验证通过,直接分配域名并连接 document.getElementById("subdomain-url").innerText = res.content; setUIConnected(true); addLog("成功连接到服务器", "system"); } else if (res.code === 2) { // 情况 B: 触发激励逻辑,弹出小程序码 showAdModal(res.content); // res.content 包含小程序码 Base64 或 URL addLog("请扫码完成验证后继续", "info"); // 【关键】注册一次性监听事件,等待后端“发令枪” const unsubscribe = Events.On("ad-status", (data) => { closeAdModal(); // 关闭弹窗 unsubscribe(); // 销毁监听,防止重复触发 if (data.status === "done") { addLog("验证成功,正在连接...", "system"); connect(); // 再次发起连接请求,此时后端将通过验证 } else { // 处理异常(如超时、未看完) hint.innerText = data.message; hint.classList.add("error-shake"); addLog(`验证未完成: ${data.message}`, "error"); setUIConnected(false); } }); } } catch (err) { setUIConnected(false); } } 三、 为什么这样设计?(经验总结) 安全性(Security First): 连接逻辑的“入场券”完全由 Go 后端控制。前端只是一个 UI 展示层,即便用户强行修改 JS 调用 connect(),如果后端没有收到广告系统的回调信号,依然不会分配 frp 配置。 订阅-发布模式(Events): 使用 Events.On 而不是轮询(Polling)。当用户在手机上看完广告,后端 API 收到微信的回调后,会通过 app.Emit(“ad-status”, …) 通知前端。这种实时性让用户体验非常丝滑——手机看完,电脑屏幕上的弹窗瞬间自动消失并连接。 UI 细节:单页面布局(Zero-Config UI): 由于 Mole 的界面追求极致精简,所有的 CSS 样式、HTML 布局和 JS 逻辑都高度集成在 index.html 和 main.js 中。对于这类工具软件,减少资源加载链比追求组件化更重要。 四、 结语 前端在 Wails 3 项目中扮演的是 “状态反馈器” 的角色。通过 Bindings 调用 Go 方法,通过 Events 监听实时动态。 ...

2025-12-04 · 2 min · Eagle

技术深挖(一):Wails 3 驱动 frp 客户端的核心实现与避坑指南

在上一篇文章中,我介绍了 Mole 客户端的业务闭环。今天,我们切入代码层面,聊聊基于 Wails 3 的 Go 后端是如何驱动 frp 核心并保证体验平滑的。 一、 Wails 3 项目启动架构 Wails 3 的启动逻辑高度模块化。在 main.go 中,一切从 application.New 开始。 1. 资产集成与服务绑定 首先,我们需要通过 embed.FS 将前端生成物(Vite 构建后的 dist 目录)打包进二进制文件: //go:embed all:frontend/dist var assets embed.FS func main() { app := application.New(application.Options{ Name: "Mole", Description: "Fast Reverse Proxy Manager", Services: []application.Service{ application.NewService(&FrpService{}), // 绑定核心服务 }, Assets: application.AssetOptions{ Handler: application.AssetFileServerFS(assets), }, }) // 创建窗口逻辑... } 关键点: Services 是 Wails 3 的精髓。它是一个切片,支持绑定多个服务实例。每个服务中定义的公开方法,前端都可以通过自动生成的 JS 绑定直接调用。 二、 核心经验总结:从“能跑”到“好用” 在开发 Mole 的原型过程中,我踩了不少坑,总结出以下 5 个核心经验: ...

2025-12-03 · 2 min · Eagle

Frp 管理客户端:基于 Wails 3 的 frp 智能管理实战

Mole 是一款基于 Wails 3 开发的跨平台 frp 管理客户端。它不仅解决了 frp 配置繁琐的痛点,更探索出了一种“桌面工具 + 小程序生态”的全新闭环模式。 🌟 核心业务流程 Mole 的核心逻辑在于其自动化配置流与激励机制的融合: 1. 智能激励连接 (One-Click Connect) 当用户点击“一键连接”时,客户端会启动一套自动验证逻辑: 广告触发:系统检查用户当前状态。若未满足条件,则弹出动态窗口显示专属小程序码。 多端协同:用户扫码后进入微信小程序观看激励视频。 自动握手:用户观看完毕后,小程序通过后端指令反馈给 Mole 客户端。 静默取消:客户端接收指令后自动关闭弹窗,进入连接阶段。 2. 云端配置自动下发 Mole 不再需要用户手动编辑复杂的 TOML 文件: 动态获取:从服务器端安全获取分配的随机子域名和 frp 配置 Token。 本地注入:Go 后端自动将配置写入内置的 frpc.toml。 二进制调用:自动调用内嵌的 frpc 核心组件启动服务。 3. 全链路事件反馈 利用 Wails 3 的高效事件机制,实现深度的 UI 反馈: 实时日志:Go 后端捕获 frpc 的标准输出,通过事件流(Events)实时推送到前端页面,用户可以清晰看到连接建立过程。 状态监控:实时反馈隧道运行状态,确保连接稳定性。 🛠️ 板块功能详情 连接控制台:一键启停,集成小程序激励流程。 端口配置页:灵活配置本地服务端口(如 8080, 3000 等),满足开发者调试需求。 实时日志页:直观展示 frp 运行日志,方便故障排查。 帮助中心:详尽的操作指南,降低内网穿透的使用门槛。 技术结缘 (About & Support): 集成优质云服务器推广(为用户提供可靠的 frp 服务端选择)。 扫码直达开发笔记,分享 Wails 3 与 Go 的底层实战。 👀 软件界面概览 1. 软件主界面 ...

2025-12-02 · 1 min · Eagle

系列开篇:告别命令行的束缚,我为何选择 Wails v3 打造 frp 管理客户端?

一、困境与契机:低配服务器、高性能需求与共享经济的思考 我的云服务器配置很“复古”:1 核 CPU、1G 内存、1M 带宽。它完美地满足了我个人博客的需求,直到我需要演示一套视频会议系统。该系统最低要求 2 核 4G。 我的服务器是包年做活动时购买,升级配置成本高昂,而我的本地电脑性能过剩,为了一次演示,我升级我的云服务器,我感觉不值。为了解决这个问题,frp(Fast Reverse Proxy) 成为了我的救星,它允许我将本地高性能服务安全地映射到公网。 新的痛点出现:命令行的局限性 在成功通过命令行 frpc 实现了内网穿透演示后,我发现了一系列生产力问题: 进程管理难题: 命令行窗口一旦关闭,frpc 进程随之终止,服务中断。这在日常使用中非常不便,因为会经常手误关闭窗口。 潜在的商业价值: 我有闲置资源服务器,公网 IP 以及域名。对于那些临时需要公网映射的用户来说,这是一个刚需。 流量变现与服务化: 我有一个小程序并集成了广告。如果能让用户通过看广告来获取临时的 frps 使用权(子域名分配),可以减少我的服务器支出,这将是一个双赢的模式。 为了解决这些问题并实现我的想法,我意识到可以做一个稳定、可后台运行、拥有友好 UI 的客户端,来替代原始的命令行操作。 二、技术选型坎坷路:Fyne、Tauri 的尝试与碰壁 我的技术栈主要集中在 Go、Rust 和 JavaScript,其中 Go 最擅长。我希望使用这些技术栈实现一个跨平台的客户端。 第一次尝试:Go & Fyne Fyne 是一款使用 Go 语言编写的跨平台 GUI 库。我首先尝试了它。 优点: 纯 Go 编写,上手快。 痛点: 在处理 frpc 实时打印的日志时,Fyne 对中文字符的支持不理想;复杂的列表和表格布局实现起来很痛苦。最终放弃。 第二次尝试:Rust & Tauri Tauri 是当时(也是现在)非常热门的跨平台框架,结合 Rust 的后端性能和 Web 前端技术。 优点: 性能强劲,打包体积小,前端界面开发体验好。 痛点: 我完成了界面设计,但在核心的 frp 控制模块上遇到了巨大的技术难题。Rust 的所有权(Ownership) 和生命周期管理让我头疼不已,始终无法优雅地实现 frp 进程的启动、停止和状态共享。项目卡壳。 三、柳暗花明:Wails v3 成为“天选之子” 在技术选型陷入僵局时,有读者告诉我可以尝试下 wails 3,我关注了 Wails 项目的 v3 版本。虽然现在它还处于 Alpha 阶段,但它宣传的特性完美契合我的所有需求: ...

2025-12-01 · 1 min · Eagle

跨平台桌面开发新选择:Wails 3 初体验及在 FRP 管理客户端中的选型实践

为什么选择了Wails 3 ? Wails 3 最大的改变在于它不再强绑定于某个特定的前端框架,且引入了多窗口支持和更轻量级的 Runtime。它允许你在不启动主窗口的情况下运行后端服务,这正是我们实现“系统托盘”和“后台演示服务”的基础。 在 v2 中,我们习惯于自动生成的 wailsjs 文件夹。但在 v3 中,这一逻辑被进一步标准化。 当你运行开发指令时,Wails 会扫描你的 Go 结构体方法,并将其映射为前端可以调用的 JavaScript 函数。这个过程在 v3 中被称为 Generate 过程。 Wails 3 通信灵魂 我们在前端调用在后端定义的 HandleConnect 返回自定义结构体,这在 Wails 3 中是前后端通信的灵魂。 在 Wails 3 开发中,最核心的动作就是:后端做功,前端表现。 当你调用 MoleService.HandleConnect() 时,Go 后端会产生一个结果。在本项目中,我们需要同时返回一个 Code(状态码)和一个 Content(数据内容)。 为了实现这一点,我们定义了一个结构体: type Response struct { Code int; Content string } 虽然 Go 内部使用的是结构体,但前端 JavaScript 只能读懂 JSON 对象。Wails 3 内部会自动帮你完成这个“翻译”过程。 但是,如果你想让前端看到的字段名是小写的(例如 res.code 而不是 res.Code),你必须在 Go 结构体定义时加上“注解”。 type Response struct { Code int `json:"code"` Content string `json:"content"` } 记住,所有通过 bindings 调用的 Go 方法,在前端返回的都是一个 Promise 对象。这意味着你必须使用 await 或者 .then() 来接收数据,否则你拿到的将是一个永不开启的“盲盒”。 ...

2025-11-01 · 4 min · Eagle

Mosquitto 安全加固指南:从匿名访问到多用户 ACL 细粒度权限控制

在 Mosquitto 实例上配置身份验证非常重要,这样未经授权的客户端就无法连接。 在 Mosquitto 2.0 及更高版本中,你必须明确选择身份验证选项,然后客户端才能连接。早期版本中,默认设置是允许客户端无需身份验证即可连接。 身份验证有三种选择:密码文件、身份验证插件和匿名访问。可以使用三个选项的组合。 在 Mosquitto2.0 及更高版本中,可以通过配置文件中将 per_listener_settings 设置为 true,让不同的侦听器使用不同的身份验证方法。 除了身份验证,您还应该考虑访问权限控制,以确定哪些客户端可以访问哪些主题。 密码文件设置 密码文件是一种将用户名和密码存储在单个文件中的简单机制。适合于有相对较少的静态用户。请注意,修改了密码文件后,需要重新加载 mosquitto。 创建密码文件,可以使用mosquitto_passwd工具,命令如下: mosquitto_passwd -c <password file> <username> 请注意,使用-c 标志将覆盖已存在的文件,如果向已存在文件添加,请去掉-c 标志。 要开始使用密码文件,需要配置 broker,在配置文件中,添加如下项: password_file <path to the configuration file> 密码文件必须能够被运行 mosquitto 的用户读取。 身份验证插件设置 如果你需要更多的控制来认证用户,你需要使用认证插件。具体的特性依赖于你使用的认证插件。 可使用的插件: mosquitto-go-auth ,它提供了各种后端来存储用户数据,例如 mysql,postgresql,jwt 或 redis 等。 Dynamic security,动态安全插件,仅适用于 2.0 及更高版本,可提供原创管理的灵活的代理客户端、组和角色。 配置身份验证插件依赖你的 Mosquitto 的版本。 版本 1.6.x 和以前的版本,使用auth_plugin选项。这个选项在版本 2.0 也被支持。 listener 1883 auth_plugin <path to plugin> 版本 2.0 及更高,使用plugin选项。 listener 1883 plugin <path to plugin> 匿名访问设置 配置匿名访问,请使用allow_anonymous选项。 ...

2025-11-01 · 1 min · Eagle

全栈实战:基于 Go + 小程序实现网页端“扫码登录”逻辑

扫码基石:构建视觉化的连接钥匙 二维码(QR Code)是连接物理世界与数字世界的“虫洞”。在登录系统中,它承载着一个临时的身份信标。 1. 生成有效二维码 登录二维码通常包含一个加密的 URL 或一个唯一的 UUID(通用唯一识别码)。 唯一性: 每一对扫描动作都必须对应一个独一无二的 ID。 时效性: 二维码必须配合 Redis 设置过期时间(如 2 分钟),逾期自动失效。 Redis 最核心的用法是 Key-Value (键值对)。 在本项目中,我们将用户的微信 OpenID 作为 Key,生成的验证码作为 Value: // 存储验证码,并设置 5 分钟过期 err := rdb.Set(ctx, "user:123:code", "8888", 5*time.Minute).Err() // 读取验证码 val, err := rdb.Get(ctx, "user:123:code").Result() 在 Go 生态中,我们使用 skip2/go-qrcode 等库来完成像素的绘制: // 生成二维码字节数组 var png []byte png, _ = qrcode.Encode("91demo.top"+sessionID, qrcode.Medium, 256) 为了防止用户伪造扫码请求,二维码里的内容通常是加密的或者是不可预测的长随机数(UUID)。只有真实存在的 ID 才能通过后端的 Redis 校验。 2. 传输二维码 我们不希望在用户硬盘上产生大量的临时 .png 文件。 最佳实践是将二进制图片转换为 Base64 字符串,通过结构体返回给前端: // 转换为前端可直接识别的 Data URL base64Img := "data:image/png;base64," + base64.StdEncoding.EncodeToString(png) 为了让前端不至于崩溃,后端必须返回统一的格式。 func HandleLogin(c *gin.Context) { // 逻辑处理... c.JSON(200, gin.H{ "code": 1, "content": base64Img, }) } 在前端,我们不再需要引入沉重的第三方库来做简单的请求。浏览器原生提供的 Fetch API 简洁且基于 Promise。 ...

2025-03-15 · 3 min · Eagle

Web 实战:构建高可靠验证码登录系统及后端防刷流控策略

我们都知道手机号验证码可以登录网站。这里我也做了类似的功能,方便在手机端不能使用扫码也能登录。 我在小程序端制作了一个获取验证码界面,它可以生成模拟编号和验证码。当用户点击获取验证码时,会返回编号和验证码,它同时绑定用户的小程序身份Openid。 当用户在网站端输入编号和验证码时,后端会校验是否存在这对编号和验证码,如果校验正确,将取出openid并绑定到sessionID上,然后返回给前端,存入cookie中。 除了在小程序生成验证码外,我还添加了在公众号发送消息获取验证码。 1. 公众号获取验证码 公众号不仅是内容分发平台,更是一个强大的身份认证中间件。 1. 握手与回调 (Webhook) 要在 Go 中接收微信消息,你必须先在微信公众平台配置一个 服务器地址 (URL)。 校验: 微信会向你的 URL 发送一个 GET 请求,包含签名、随机数等。你必须按照规定的算法计算并返回正确的 echostr,这被称为“服务器验证”。 消息推送: 验证通过后,每当用户发送消息,微信服务器就会以 POST 方式将消息体推送给你。 2. 解析 XML 数据 与现代 API 不同,微信公众号的推送采用的是 XML 格式。Go 的标准库 encoding/xml 提供了强大的解析能力: type WxMsg struct { ToUserName string `xml:"ToUserName"` FromUserName string `xml:"FromUserName"` // 这就是用户的 OpenID Content string `xml:"Content"` // 用户发来的文字 } 3. 验证码生存逻辑 (Redis 联动) 当用户发送“验证码”关键字时,我们的 Go 后端会: 生成一个 4-6 位的随机数。 将 OpenID 与 随机数 存入 Redis,并设置 TTL(如 5 分钟)。 通过 XML 响应将验证码发回给用户。 4. 身份对撞 用户在Web端输入这个验证码。后端从 Redis 中根据验证码反查 OpenID,若存在且有效,则代表身份验证成功。执行登录流程。 ...

2025-03-11 · 1 min · Eagle

ESP8266 工业级配网实战:基于 SoftAP 的可靠 WIFI 动态配置方案

背景介绍 在我将ESP8266采集代码灌入之后,它已经可以实现自动采集按钮信号了。核心逻辑实现之后,我需要能够连接网络,因为网络是连接MQTT的前置条件。 在连接之前,我需要将路由器的WIFI密码告知ESP8266芯片,这不同于Debug的时候,在代码内固定填写WIFI账号密码。我需要动态的获取并写入到ESP8266芯片。 网络配置有很多种方式,我选择了SoftAP配网,虽然配置有点繁琐,但这是一种很可靠的配网方案,兼容性极高,不依赖手机硬件的特殊协议。 准确来说,这不应该算是一个项目,但因为它具有通用性和一点实用性。我决定把它记录下来,可以方便地应用到我的其它项目中。 Soft配网原理 初始化模式:ESP8266 启动后进入 WIFI_AP_STA 模式。它会开启一个无密码(或已知密码)的热点(AP),并运行一个轻量级的 Web 服务器(HTTP Server)。 通道建立:手机通过小程序或系统设置连接到该热点。此时手机与 ESP8266 处于同一个局域网内。 数据交互:小程序通过 HTTP Post 请求将目标 WiFi 的 SSID 和 Password 发送给 ESP8266 的固定接口(如 /config)。 校验与切换:ESP8266 收到参数后,尝试作为客户端(STA)连接路由器。如果连接成功,则关闭 AP 热点,保存参数到 Flash;如果失败,则返回错误信息并维持 AP 状态。 ESP8266端核心代码(Arduino IDE) 在Arduino IDE打开项目后,需要安装ESP8266 WebServer库。这个库提供了Web服务,可以接收其它HTTP客户端的连接。 以下代码实现了一个非常简单的Web服务,它监听/config接口并能够接收WIFI信息,以及接收之后可以本地存储,实现动态配置的功能。 注意:当存储在本地之后,下次启动可以直接使用。而不需要重新配置。 #include <ESP8266WiFi.h> #include <ESP8266WebServer.h> // 定义AP热点的名称 const char* ap_ssid = "ESP8266_Config_Device"; ESP8266WebServer server(80); void handleConfig() { if (server.hasArg("ssid") && server.hasArg("pass")) { String target_ssid = server.arg("ssid"); String target_pass = server.arg("pass"); // 确保这里是 pass // 1. 先回复小程序,否则一旦开始连WiFi,AP就会失效,小程序收不到回复会报错 server.sendHeader("Access-Control-Allow-Origin", "*"); server.send(200, "text/plain", "SUCCESS"); Serial.println("配置信息已收到,准备连接..."); // 2. 延迟一下再连接,给 HTTP 响应留出传输时间 delay(1000); WiFi.begin(target_ssid.c_str(), target_pass.c_str()); int counter = 0; while (WiFi.status() != WL_CONNECTED && counter < 20) { // 等待10秒 delay(500); Serial.print("."); counter++; } if (WiFi.status() == WL_CONNECTED) { Serial.println("\nWiFi连接成功!"); WiFi.mode(WIFI_STA); // 关闭AP模式,切换为STA模式 WiFi.setAutoConnect(true); WiFi.persist(true); // 将账号密码永久保存到Flash } else { Serial.println("\n连接失败,请检查账号密码"); // 保持AP模式,等待下次尝试 } } else { server.send(400, "text/plain", "FAIL: Missing Params"); } } void setup() { Serial.begin(115200); // 设置为AP+STA模式 WiFi.mode(WIFI_AP_STA); WiFi.softAP(ap_ssid); Serial.println("AP热点已启动,IP地址: " + WiFi.softAPIP().toString()); // 注册接口 server.on("/config", HTTP_POST, handleConfig); server.begin(); Serial.println("HTTP服务器已启动"); } void loop() { server.handleClient(); // 如果连接成功,可以根据需要在这里处理业务逻辑 if (WiFi.status() == WL_CONNECTED) { static bool connected_msg = false; if (!connected_msg) { Serial.println("设备已联网,IP: " + WiFi.localIP().toString()); connected_msg = true; } } } 参考上面的原理,设备上电之后,启动AP模式,接收配置,这个时候可以通过指示灯来显示状态。接收完毕后,可以正常连接到WIFI,则将WIFI配置信息保存到本地并进入核心业务逻辑。 ...

2025-01-02 · 2 min · Eagle

Rust 实战进阶:深度复盘上传工具崩溃问题,优化报错提示

最近,更新了上传文章工具 rust 版本,这个版本使用的库为 nwg,即 native-windows-gui 库,该库支持老的 Windows GUI。 昨天,当我准备在另一台电脑上录制视频时,发现运行不起来,GUI 界面闪现一下,就退出了。我先升级了 Rust 版本到最新版本。 rustup update 升级完成后,cargo clean ,再 cargo run 重新运行。 郁闷,还是报错。 将 main.rs 文件中该行注释,#![windows_subsystem = "windows"],可以在命令行中查看报错信息,提示是找不到文件。我想了下,只有图片是文件,然后我将图片的路径修改,发现修改之后,编辑器提示错误,找不到文件。重新改回去后,编辑器提示错误消失。说明不是这里的错误。 我真的晕了。从网上查找问题原因,没有找到此类问题的解决方法,郁闷。在查找时,发现了另一个 Windows 官方支持的 rust 库,就叫 windows,打算有时间了用这个库重写一下。 今天,我又查看了下另一台电脑上的这个项目,发现可以正常运行,将 rust 升级到最新版本后,还是可以运行。在看到配置文件后,我才恍然大悟,原来是查找这个文件。我赶紧扒拉代码确认,发现确实是这个问题。 let conf = Ini::load_from_file("conf.ini").unwrap(); 上面是使用文件的地方,找不到文件 panic。我这里优化一下,让错误提醒的更明显一些。这样我就瞬间能知道问题原因了。 优化后的代码如下: let conf = Ini::load_from_file("conf.ini").expect("please config conf.ini file");

2025-01-01 · 1 min · Eagle