Tauri + Wake Lock 实战:双端状态机、重获锁与发布一致性

发布时间:2026/8/3 7:23:59
Tauri + Wake Lock 实战:双端状态机、重获锁与发布一致性 ScreenAwake 最初只想解决一个很小的问题演示、照着教程操作或等待任务时不要让屏幕先熄灭。真正开始实现后我发现“保持常亮”不是一个按钮而是两套不同的系统契约网页只能申请浏览器提供的 Screen Wake Lock桌面端则要持有操作系统级的电源抑制资源。它们可以共用界面却不能共用对生命周期的承诺。这篇复盘不讲“几小时做完一个小工具”只拆四个工程问题用户意图与资源状态为什么要分开、网页锁为何必须重获、桌面端如何确定性清理以及双端发布物怎样避免版本身份漂移。先定义两种产品承诺ScreenAwake 同时提供网页临时模式和 Tauri 桌面模式。两端看起来都只有“开启常亮”背后的约束完全不同。维度网页模式桌面模式核心能力navigator.wakeLock.request(screen)macOScaffeinate、WindowsSetThreadExecutionState、Linuxsystemd-inhibit可见性只有可见、活跃文档能可靠申请主窗口隐藏后仍可由进程持有资源资源句柄WakeLockSentinel可能被浏览器或系统释放子进程或系统电源状态必须由应用主动清理用户预期临时使用标签页需保持可见托盘、倒计时、开机启动、关闭窗口后继续主要失败面浏览器不支持、页面隐藏、低电量或系统策略权限、平台 API、进程残留、退出清理、安装签名MDN 明确说明Screen Wake Lock 只面向可见的活跃文档系统可以因为页面不可见、电量或平台设置释放锁。W3C 草案还把底层获取描述为 advisory-only调用成功不等于应用获得了绕过所有系统策略的保证。所以网页端的产品文案必须是“在支持的浏览器和允许条件下保持屏幕常亮”不能写成“永久阻止休眠”。桌面端能力更强也不能绕过企业 MDM、手动锁屏或组织安全策略。用户意图不等于资源状态网页端最容易写出的实现是拿state.active同时表示两件事用户想继续保持常亮以及当前 Wake Lock 仍然有效。问题是浏览器释放锁时active会变成false当标签页再次可见程序已经不知道用户是主动点了“停止”还是资源被系统暂时收走。ScreenAwake 后来把这两个状态拆开letwebWakeLock:WakeLockSentinel|undefined;letwebKeepAwakeRequestedfalse;asyncfunctionstart(){webKeepAwakeRequestedtrue;webWakeLockawaitnavigator.wakeLock.request(screen);}asyncfunctionstop(){webKeepAwakeRequestedfalse;awaitwebWakeLock?.release();webWakeLockundefined;}webKeepAwakeRequested是用户意图webWakeLock是当前资源句柄。前者只有用户操作才能取消后者可以被浏览器、系统或代码释放。这个拆分不只适用于 Wake Lock。WebSocket、麦克风流、蓝牙连接、文件句柄和长任务也应该区分“用户仍然需要”与“资源此刻可用”。否则一次短暂断开就会被误判为用户取消。被释放的 Sentinel 不能复用WakeLockSentinel被释放后不能重新激活如果用户意图仍然存在应用必须申请一个新对象。ScreenAwake 在release事件中先确认回调对应的仍是当前句柄再清理状态页面重新可见时只在用户仍有意图且句柄不存在或已释放时重获锁acquiredWakeLock.addEventListener(release,(){if(webWakeLockacquiredWakeLock)webWakeLockundefined;state.activefalse;state.powerGuardActivefalse;render();});document.addEventListener(visibilitychange,(){constshouldReacquiredocument.visibilityStatevisiblewebKeepAwakeRequested(!webWakeLock||webWakeLock.released);if(shouldReacquire)voidstart();});这里保留acquiredWakeLock局部引用是为了防止旧对象的延迟release事件把新句柄清掉。它和网络请求里的 request ID、搜索建议里的 sequence number 是同一类并发防线旧事件不能覆盖新状态。桌面端先持有主资源再叠加可选兜底桌面端没有把鼠标微动当成主方案。主路径是操作系统电源抑制锁macOS 启动caffeinate -d -iWindows 调用SetThreadExecutionStateLinux 启动systemd-inhibit。鼠标微动、Shift 短按和隐藏窗口 ping 都是可选策略。鼠标微动在 macOS 上还要先检查辅助功能权限键盘兜底默认关闭。这个顺序很重要能用系统提供的电源 API就不应先模拟用户输入。桌面状态机每次开始前先停止旧控制器再创建新资源和截止时间pubfnstart(self,options:AwakeOptions)-ResultAppStatus,String{self.stop();letmutcontrollerself.controller.lock().map_err(|_|状态锁不可用.to_string())?;controller.power_guardSome(PowerGuard::start()?);controller.deadlineoptions.duration_seconds.filter(|seconds|*seconds0).map(|seconds|Instant::now()Duration::from_secs(seconds));Ok(Self::status_locked(controller))}start()先调用stop()可以避免重复点击后残留两份caffeinate子进程、输入线程或倒计时。停止时再依次翻转原子标志、结束输入线程、停止窗口 ping、释放电源锁并清空截止时间。这是一条适合小工具的简单不变量任意时刻最多只有一组持有资源的控制器。关闭窗口不等于停止任务桌面端另一个容易混淆的状态是“窗口关闭”和“应用退出”。ScreenAwake 拦截主窗口关闭把窗口隐藏到托盘只有托盘退出或进程退出时才停止电源防护。.on_window_event(|window,event|{iflettauri::WindowEvent::CloseRequested{api,..}event{api.prevent_close();let_window.hide();}}).run(|app,event|{ifletRunEvent::ExitRequested{..}event{app.state::AppState().stop();}});如果产品把“关窗口”定义成“停止常亮”就不该隐藏到托盘如果定义成后台继续界面必须明确提示托盘也必须提供停止和退出入口。生命周期不是框架默认行为而是产品契约。双端发布要记录两种构建身份ScreenAwake 的网页端在main推送后自动部署 GitHub Pages桌面端则在v*标签上构建 Release。两条流水线本身没有问题但它们会让“当前版本”出现两种身份。本次核验时发布面核验身份结果GitHub Pages /main1802fa2922ac1ddaebb748b9649f9636cdbaae10包含返回可见时重获 Wake Lock 的修复桌面 Releasev0.1.2b6e5e8f7c8e6291553e9656cc8ed27f13c661cc4早于上述网页修复package.json/ Tauri 配置0.1.2两端仍显示同一语义版本号这意味着“版本号都是 0.1.2”不能证明网页与桌面包含同一提交。更稳妥的做法是让两个界面显示version short commit发布记录同时保存 Pages 提交、Release tag 和资产校验值。最小检查可以写成gitrev-parse HEADgitrev-parsev0.1.2^{commit}gitlog--onelinev0.1.2..HEAD如果网页修复也应该进入桌面包就应发布新 tag如果只影响网页也要在发布记录中明确两个分发面的能力差异。本次验证通过了什么我在 2026-08-02 对公开仓库和页面做了重新核验npm ci成功npm run build成功Vite 生产构建生成distcargo check --manifest-path src-tauri/Cargo.toml成功cargo test --manifest-path src-tauri/Cargo.toml命令成功但实际运行0 个单元测试、0 个文档测试Ego 真实浏览器打开公开网页页面标题、四档时长和navigator.wakeLock支持回读正常GitHub Releases 公开页显示v0.1.2为 Latest并能追溯到 tag 提交源码共 9 个主要前后端文件仓库内未发现 JavaScript / TypeScript 测试文件或 Rust#[test]。所以当前能说的是“源码可以完成前端生产构建和本机 Rust 静态检查”不能说“桌面端已被自动化测试覆盖”也不能说 Windows x64、macOS ARM64、托盘、开机启动、快捷键、倒计时和退出清理都已经完成本轮端到端回归。下一版最该补的三道门第一道是状态机测试。至少覆盖用户主动停止、页面隐藏自动释放、返回可见重获、旧 Sentinel 延迟事件、重复开始、倒计时到期和退出清理。第二道是发布物身份。网页和桌面都显示提交短 SHARelease 记录资产 SHA-256面向 macOS 公开分发前补齐 Developer ID 签名和公证而不是把临时签名写成完整分发能力。第三道是真机矩阵。网页端验证支持与不支持 Wake Lock 的浏览器、页面切后台、低电量和手动锁屏桌面端分别在 macOS ARM64、Windows x64 和计划支持的 Linux 环境验证系统 API 与退出清理。可复用检查表用户意图与当前资源句柄分开保存资源被系统释放时不误判为用户取消旧资源的延迟事件不能覆盖新资源重获资源前重新检查可见性、权限和系统条件重复开始前确定性停止上一组资源关闭窗口、停止任务和退出应用有明确区分主能力优先使用平台 API模拟输入只做可选兜底网页部署提交与桌面 Release tag 分别记录“测试命令成功”与“实际测试数量”分开报告未签名、公证、真机回归的能力不写成已完成。小工具真正难的地方通常不是把按钮点亮而是把“用户还想要什么”“系统现在给了什么”“资源何时必须收回”三件事分清楚。生命周期和发布身份一旦清楚网页与桌面才能共享产品体验而不是共享一组含糊承诺。来源与验证边界ScreenAwake 仓库网页端实现核验提交桌面状态机核验提交系统电源防护实现核验提交GitHub ReleasesMDN Screen Wake Lock APIW3C Screen Wake Lock 工作草案验证边界本文验证了公开网页、源码提交、前端生产构建、本机 Rustcheck与 Release 身份没有执行 Windows / macOS 安装包真机端到端回归也没有把cargo test的 0 个测试写成测试覆盖。