联调工具里,更新是次要事件

大多数桌面应用把自动更新当成主流程的一部分:发现新版本,弹窗,下载,重启。编辑器可以这么做,浏览器也可以这么做。你关掉文档、等几十秒,再打开,损失通常只是一次上下文切换。

DevPeek 不是这种应用。

它早上开、傍晚关。代理挂着,HTTPS 还在解密,Mock 和参数转换规则还在工作,列表里留着刚才那批请求,手机或 WebView 仍然指着本机端口。你真正在做的事,是对着一条加密 Body、一个错误码、一页 H5 反复试。工具只是这条工作台的底座。

在这种场景里,更新提示不是「顺便提醒一下」,而是一次强制抢占。弹窗问你要不要立即重启时,真正被中断的不是 DevPeek,是正在进行的联调。点「是」,代理停了,会话断了,真机还指着刚才那个端口;点「否」,过一会儿又来一遍。更糟的是:对话框出现时,安装包往往还没下完。你先付了一次中断,再去盯进度条。

所以重做自动更新时,第一个问题不是「怎么下包」,而是:

对一个必须长时间在线的代理工具,更新到底有没有资格打断正在进行的工作?

答案很明确:没有。 更新可以发生,但不该变成主窗口里的紧急事件。下载可以慢慢做,覆盖必须等人点头,重启可以短,但时机必须由联调的人决定。

这就是「一键更新」的起点。它看起来像功能,其实是一条产品约束:工具可以升级,但不能来抢窗口。

「静默」很容易做成两种错的东西

一说静默更新,现成模板其实就两种,两种我们都不想要。

第一种是发现即打断。应用启动后不久弹出「发现新版本,是否立即重启?」下载和安装还绑在一起。用户为了让提示消失,只能现在就停下来,或者反复点取消。取消并不等于「等我忙完再说」,它只是把同一个问题推迟几分钟再问一次。对联调来说,这等于在工作台边上放了一个会定时拍桌子的人。

第二种是后台装完直接拉起新版本。看起来更先进:用户什么都不用点,下次打开已经是新版。对聊天工具、启动器也许合适。对代理工具不行。Core 一停,系统代理、证书、Mock、正在解密的 HTTPS 都会一起掉。如果这件事发生在你不知道的时候,浏览器和手机只会表现为「怎么突然上不了网」,排查成本比更新本身高得多。

于是「静默」必须重新定义。我们要静默的是准备,不是覆盖。

检查版本、下载安装包、校验文件,这些都可以在用户抓包、改规则、看列表时完成。主窗口不需要进度条,也不需要在包还没就绪时先问一句「要不要重启」。真正替换文件的那一步,仍然需要一次明确的「立即更新」。重启省不掉——正在运行的程序不可能一边占着文件,一边安全地覆盖自己——但等待下载的时间可以省掉,时机也可以交给用户。

一键的意义,不是替你点了重启,而是把所有可以提前做的事做完,只留一次你能控制的中断。

现成 updater 假设的是另一种应用

如果 DevPeek 还是「一个窗口就是整个应用」,这件事本来有现成方案。Electron 时代用的就是 electron-updater:桌面主进程既是入口,也是更新入口,检查、下载、重启都挂在同一个进程上。窗口在,更新器就在;窗口关了,应用往往也差不多结束了。

后来壳换成了 Tauri。这不是换皮肤,是应用形态已经变了。

业务早就被拆出去:Core 跑代理和全部能力,Launcher 管托盘和进程,Tauri Shell 只负责打开 Core 托管的那套 Web UI。关掉主窗口,Launcher 和 Core 仍留在后台,代理继续工作。这正是联调工具该有的形态——界面可以藏起来,底座不能跟着消失。

可更新逻辑立刻对不上了。electron-updater 绑的是 Electron 主进程,壳一换,这条路就断了。Tauri 自带 updater 我们也看过。它能更新 Tauri 应用自己,前提仍然是:窗口就是应用。 对我们来说,窗口只是 Core 地址上的一层皮。关窗之后,还得更新外面的 Launcher 和 Core;要动的也不是一个二进制,是三个还在占着文件的进程。

现成 updater 不是能力不够,是它默认了一种我们已经放弃的产品结构:

  • 应用有一个明确的主进程;
  • 用户看着窗口,就等于应用还在前台;
  • 关窗约等于退出;
  • 更新这个窗口,就等于更新整个产品。

DevPeek 四条都不成立。主窗口可能早就关了;Lite 模式甚至根本没有桌面壳,界面跑在系统浏览器里;真正不能停的是 Core;真正不能随便退出的是 Launcher。如果把更新入口继续放在窗口里,关窗就等于错过升级,Lite 模式更是没有安装桌面程序的资格。

所以更新任务最后落到了 Launcher。不是因为它「更底层」,而是因为它是唯一同时满足三个条件的角色:知道 Core 和 Shell 还在不在,不会随窗口退出,安装完成后还要把自己重新拉起来。UI 只负责确认「缓存里的包已经校验过了」。谁停进程、谁恢复系统代理、谁启动安装,必须是同一个不会中途消失的管家。

换壳之后必须自研更新,本质不是技术洁癖。是更新入口必须跟应用的真实生命周期对齐,而不是跟「现在有没有一个窗口」对齐。

联调会话比新版本更贵

为什么一定要把下载和覆盖拆开?因为两者的成本完全不对称。

下载可以发生在任何时候。几小时检查一次,启动一分半后再查,对正在过代理的流量几乎没有影响。安装包在后台慢慢来,用户甚至不必知道这件事正在发生。失败了再试,也不会破坏当前会话。

覆盖不行。覆盖意味着停 Core、停 Shell、退出 Launcher,系统代理也必须先恢复。这几秒里,DevPeek 必然离线。列表还在不在、Mock 还生不生效、手机还指不指向本机,都取决于你是不是正好卡在改一条规则、复现一个错误码的当口。

如果在包还没下完时就问「要不要立即重启」,等于把高成本动作的决定,做在低成本动作尚未完成的时候。用户点了「是」,接下来却要盯着进度条,会话已经断了,收益还没兑现。这是最不划算的交换。

所以正确的顺序只能是:

先把贵的准备做完,再问人要不要付一次便宜的中断。

包准备好后,右下角浮出一个「更新就绪」。可以立刻装,也可以关掉提示,等手头这轮联调结束。提示可以消失,包还在。它不是催促,只是告诉你:现在点下去,不会再等下载。

包已下载完成后的「更新就绪」浮窗

这也解释了为什么我们不在运行中热替换。三个进程都占着磁盘上的文件,覆盖到一半失败,落地的会是半棵目录树。对代理工具来说,半新半旧比暂时离线更糟——端口、证书、规则引擎可能已经对不上,用户看到的却只是「怎么突然抓不到包了」。停干净再一次性覆盖,失败了下次启动接着完成或回滚,排错范围才收得住。

差量更新是同一类诱惑。v1.2.1 换壳后目录结构变化很大,差量省下的流量有限,却会多出版本匹配、文件缺失、跨版本升级。对当前体量,整包更好验证。用户感知到的「快」,不该来自把安装包切碎,而来自包在你点下去之前就已经在本地。

更新器不能走自己正在维护的那条路

还有一个只在代理工具里才尖锐的问题:DevPeek 自己常常就是当前的系统代理。

普通应用的更新请求走系统代理,最多慢一点、偶发失败。DevPeek 若也这么做,请求会先进入自己,再由自己发往更新源。平时未必出错,安装阶段却会把自己卡死。Core 一旦退出,这条路径就断了。如果更新逻辑还依赖代理,就会出现很具体的循环:

为了覆盖文件必须停代理 → 停了代理,更新请求走不通 → 包没下完,系统代理却已经被摘掉。

浏览器和手机在这几秒里会表现为「整个网络都挂了」。对用户来说,这不是 DevPeek 在更新,而是电脑突然不能上网。联调现场最怕的就是这种说不清的掉线。

因此检查和下载必须绕开系统代理,直接连更新源。这条请求不能经过自己正在维护的管道。正式覆盖前,Launcher 还要先把系统代理恢复,避免 Core 已经停了,操作系统仍把流量指到一个没人监听的本机端口。

这和正常退出时的处理是同一原则:不要把一条已经没人接的代理留在系统里。更新可以让工具短暂离线,但不该把整台机器的网络一起带走。

很多人会把「绕开代理」理解成实现细节。对我们来说,它是产品约束的一部分:更新路径必须独立于被更新的对象。 自己是代理,就不能让更新依赖代理;自己要停 Core,就不能让系统还以为 Core 还在。

窗口可以关,升级通道不能关

DevPeek 还有一种很常见的用法:主窗口关掉,代理留在托盘里。Lite 模式更进一步,界面直接跑在系统浏览器里,桌面壳甚至不出现。

如果更新器住在窗口或 Tauri 壳里,这两种用法都会变成升级盲区。关窗等于错过新版本;浏览器更不可能去替换三个桌面进程的文件。结果是:最需要长时间挂着的人,反而最不容易升上来。

把更新放到 Launcher 里,就是为了让升级通道跟「代理还在不在」绑定,而不是跟「窗口开没开」绑定。Lite 和标准模式可以共用「更新就绪」提示,安装动作仍由 Launcher 完成。关于页的手动检查也留着,想自己触发不必等后台轮询。

关于页仍可手动检查更新

这看起来只是入口位置的差别,实际影响的是发布节奏。联调工具的新版本,经常带着证书、Mock、抓包列表这类必须尽快落到手头的修复。如果只有开着主窗口的人能升,修复会在真正挂一整天的那批用户那里迟到。

所以「关着窗也能更新」,不是附加功能,是这套架构下更新必须成立的前提。

我们真正想换来的,不是少点一次确认

把上面这些选择放在一起,会发现它们都在避开同一类成本:在错误的时间,用错误的方式,打断一条还不能停的会话。

后台检查和下包,是为了不让下载变成窗口事件。
托盘提示「更新就绪」,是为了让中断发生在包已经准备好之后。
必须点「立即更新」才覆盖,是为了代理、Mock、系统代理这种状态不能在用户不知道时被换掉。
由 Launcher 收尾,是为了关窗、Lite、三个进程占用文件时,仍然有人能把安装做完。
绕开系统代理、覆盖前先恢复,是为了更新失败时,锅只留在工具自己身上,不扩散成整台电脑不能上网。
整包而不是差量,是为了少一类「补丁对不上目录」的现场,让失败可以在下次启动续上或回滚。

用户直接感知到的,往往只有三句很短的话:联调时没有人催你重启;想更新时只点一下;起来之后浏览器还是正常的。

工程上换来的,是更新路径和产品形态对齐。DevPeek 已经不是「一个窗口应用」,更新器就不能继续假装它还是。现成方案能更新窗口;我们要更新的是一套还在后台跑代理的进程,以及这套进程暂时接管过的系统网络。

它看起来不如「完全无感」酷,但对联调工具更诚实。完全无感意味着覆盖时机不由你决定。我们不能接受这种无感。

判断它是否成立,只看三件事

自动更新做对了没有,不看少弹了几次窗,也不看是不是自研。

只看联调的人能不能继续做这三件事:代理还在跑,列表还在,想更新时只点一下。

如果包已经在托盘里就绪,点下去只停几秒,起来后系统代理也还干净——更新就没有变成主流程,只是底座自己换了一版。这才是这套设计想达到的目标。

开发实录系列(同一条线)

这是 DevPeek「开发实录」系列里,和自动更新相关的一篇。前几篇分别聊:

本篇想说的是同一件事的另一面:长时间联调时,工具自己少添乱。更新可以发生,但不该来抢窗口。


延伸阅读

  • 安装与首选项:https://devpeek.ypgao.com/docs/install/
  • 代理与 SSL 证书:https://devpeek.ypgao.com/docs/proxy-ssl/
  • 1.3.0 更新日志:https://devpeek.ypgao.com/changelog/

欢迎到 DevPeek 官网 试用当前版本。不同看法可以到 GitHub Discussions 聊。

参考原文:https://devpeek.ypgao.com/blog/dev-build-log-silent-auto-update/

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐