SuperAPI AI 工具集

desktop-deep-link-plumbing

编程与开发

查看项目

更新于 10-17 16:16

技能介绍

用途

桌面深链接(deep link)需要依次通过四个相互独立的环节,任一环节都可能静默丢弃链接:

  1. 注册——操作系统必须知道该 scheme 属于你的可执行文件。
  2. 启动/投递——操作系统把 URL 交给进程,各平台方式不同。
  3. 应用自身的过滤——启动代码判断哪个参数是链接。
  4. 应用状态——URL 必须送达正在等待它的那部分状态。

链接被丢弃时几乎不会报错:应用回到前台,看起来一切正常,因此这类问题常被报成“重定向不工作”,并被误以为出在浏览器侧。

各平台注册

三种不同机制;你支持的每个 scheme 都必须同时存在于这三处,而日后新增第二个 scheme 时,恰恰最容易漏掉其中一处。

  • macOS——bundle 属性列表中的 CFBundleURLTypes 数组,由打包器生成。
  • Windows——每用户注册表键(无需管理员权限):HKCU\Software\Classes<scheme>,带 URL Protocol 值以及 shell\open\command = "<exe>" "%1"。在应用内首次运行时注册是合理的;每次启动都要重新检查存储的路径,因为用户换位置重装后可执行文件会移动。
  • Linux——.desktop 文件中的 MimeType=x-scheme-handler/<scheme>,外加 update-desktop-database 以便处理器被索引。

陷阱

Linux 上到达用户的 .desktop 文件往往不是你配置的那个。 如果打包器输出目录树,后续步骤再把它包成便携单文件 bundle,该 bundle 的启动脚本通常会把自己的启动器文件写入用户 applications 目录并建立索引。桌面实际查询的处理器是这份副本。只加在打包器配置、没加在包装脚本文件里的 scheme,等于哪里都没注册。两处都要更新,然后到用户 applications 目录下 grep 已安装的文件来验证——而不是查源码。

macOS 不通过 argv 传递 URL。 它以应用事件形式到达,因此只实现 main(args) 的代码在 macOS 上会“失聪”,而在 Windows 和 Linux 上却正常。启动时注册 open-URI 处理器,并把它路由到与参数路径相同的入口点。顺带一提:点击正在运行的应用的 dock 图标是 reopen 事件,不是第二次启动,所以第二实例路径里的恢复逻辑永远不会为它触发——需要显式处理。

启动过滤器必须匹配任意 scheme://,绝不能是固定列表。 像 {"yourapp://", "http://", "https://"} 这样的列表在你注册第二个 scheme 那天就会失效——操作系统带着 URL 启动进程,这一行把它丢弃,窗口来到前台,而等待中的流程永远无法完成。应按形状匹配(RFC 3986 §3.1:一个字母,后跟字母/数字/+/-/.):

private val DEEP_LINK_ARG = Regex("^[A-Za-z][A-Za-z0-9+.\\-]*://.+")

val deepLinkArg = args.firstOrNull()?.takeIf { DEEP_LINK_ARG.matches(it) }

第二实例在触碰共享状态之前就要转发。 有单实例守卫时,操作系统仍会为链接启动一个全新进程。该进程必须在初始化任何会写入共享磁盘文件的东西之前,把 URL 交出去并退出。实际观察到的情况:一个在启动时被急切创建的偏好设置存储,被第二进程触碰,重命名临时文件覆盖活动文件失败,以 I/O 错误拖垮了启动(在 Windows 上观察到——两个进程在同一文件上竞争)。其他被急切且独占打开的东西,嵌入式数据库是最明显的例子,都值得检查是否有同样形态。一个临时文件加一个恢复信号就够了:

// adapted
if (!isSingleInstance(onRestoreRequest = { RestoreSignal.request() })) {
    deepLinkArg?.let { DeepLinkHandler.writePendingUri(it) }   // hand off
    return                                                     // before any store opens
}

存活下来的实例在处理恢复请求时消费这个待处理文件。

在监听器存在之前缓存 URL。 冷启动会在任何界面被组合之前很久就投递链接。处理器应持有最近的 URL,并在监听器一被挂上时立即重放,然后清除——否则每次冷启动深链接都会丢失,只有热启动的能用,表现为“有时能用”。

把认证回调的 token 直接交给应用状态——不要走导航。 当登录重定向从系统浏览器返回时,用户出发的那个界面仍在导航栈上:浏览器是从它打开的,应用从未离开。再次导航到该界面会压入第二份副本,而“登录成功后关闭自己”这一步只弹出那份副本,把用户留在了一个一模一样的登录界面上。看起来就像“已登录,却卡在登录页”。正确做法:

  • 从链接处理器把 token 直接投递给共享状态持有者;
  • 让界面通过观察存储的结果(出现一条会话记录)来关闭自己,而不是靠它自己的局部状态——重定向根本不经过该界面。

内嵌 web view 的界面不会遇到这个问题,因为流程从未离开应用。这是专门针对交给系统浏览器的流程,而这正是桌面上的常规形态——桌面上通常没有真正的内嵌浏览器可用。

保持回调 URL 的形状不变。 如果你的链接处理器会把应用链接规范化为内部规范 URL,请把回调 scheme 排除在该重写之外——读取 token 的代码按原始 scheme 和 host 匹配,被重写过的 URL 会静默地不再匹配。

提供粘贴回调的兜底方案。 在完全没有 scheme 处理器的宿主上(精简桌面会话、远程 shell、锁定机器),浏览器会落在回调 URL 上并在地址栏显示它,却没有任何东西可以接手。登录界面上一个接受粘贴 URL 并提取 token 的文本框,能把死路变成两秒钟的变通方案,成本只是复用你已有的一个解析器。

验证

# Linux: does a handler exist, and is it yours?
xdg-mime query default x-scheme-handler/yourapp
xdg-open 'yourapp://open?x=1'

# macOS: is the scheme in the built bundle at all?
grep -A6 CFBundleURLSchemes /Applications/YourApp.app/Contents/Info.plist

# Windows (PowerShell): the per-user registration
reg query 'HKCU\Software\Classes\yourapp\shell\open\command'

每个 scheme 都要单独测试,分别在应用关闭和应用运行时各测一次——这是两条不同的代码路径(冷启动参数 vs 第二实例转发或应用事件),各自有各自的丢链接方式。