WheatField
WheatField

做一个跨设备、跨平台的 universal clipboard

July 23, 20241493 words, 8 min read
Authors

Apple 生态里有一个非常方便的功能:universal clipboard,可以方便的实现跨设备的复制粘贴。比如你在 iPhone 上复制了一个文本,然后可以直接在 Mac 上粘贴。对经常使用多个 Apple 设备的用户来说,这个功能非常实用。

但这个功能有时候不是很灵敏,经常出现的现象是手机端已经 copy 了很多次,而电脑端的 clipboard 还是空的,有时候需要等十多秒才能完成同步。也因为如此,网友调侃称 universal clipboard 是「薛定谔的剪贴板」。

除了这点外,universal clipboard 还有两点很大的局限是:

  • 生态内封闭,也就是只能在 Apple 设备上使用,如果是 Linux, Windows 系统,那就没得玩了;
  • 即使是 Apple 设备,也必须是同一个账号下才可以同步。设想一位用户有多台 iPhone,分别登录了不同的账号,这它们之前就不能进行 universal copy paste。

为什么要造轮子

在手机、电脑同步文本、链接是一个我经常遇到的需求,之前都是通过 Telegram (TG 是为数不多的支持全平台的聊天工具,Windows, Linux, Mac, iOS, Android 都有 App,而且还支持 web 版本,非常方便) 或者邮件的方式同步,但操作下来要多个步骤,终究不太方便。考虑到安全因素,我又不想用第三方的工具,所以一直有做一个自己的 universal clipboard 的想法。

但笔者忍耐力极强,被同步文本的问题折磨了很久,一直忍辱负重,咬牙坚持没有还手。奈何最近年事渐高,耐力也不如从往,最近终于不堪其扰,着手做了 pasteit 这个工具: pasteit.ultrasev.com

preview of pasteit

工具的核心功能就两个:CopyPaste。点击 Paste 时,会将当前设备的剪贴板内容上传到服务器,在另外一个设备上点击 Copy 时,会将服务器上的内容写入到当前设备的剪贴板。后面可能还会加入更多的功能,比如历史记录、标签、搜索等。

遇到的坑

本来想着这是很简单的一个功能,随便搞个一两个小时就可以了,没想到连续写了一整天才做出来一个 MAP,其中一大部分时间卡在跟 iPhone safari 读写 clipboard 较劲了。

先说一下技术栈:

  • 框架 NestJS + TS + Tailwindcss
  • 存储使用 supabase database
  • 账号管理也是 supabase,毕竟如果多人使用,数据应该按账号互相独立。
  • 部署在 vercel 上,DNS 使用了 cloudflare。
chatflow of pasteit

supabase auth 及 database 服务都是免费的,而且提供了很多方便的 API,vercel 部署也是免费的,所以整个项目的部署成本基本上是零,在此感谢这些厂商提供的优质服务。

直接交互与异步操作的矛盾

Web 端的数据同步还好处理,上传数据时,通过 navigator.clipboard.readText 读取完剪切板,直接调用 API 进行后台同步。同样从其他设备同步数据时,点击 copy,即开始调用 API 从数据库读取数据,然后再写入到剪切板上,中间可能有半秒到一秒的延迟,但不影响剪切板的写入功能。

但到手机端情况就不一样了。从手机上往其他设备同步数据时,问题不大,仍然是通过 navigator.clipboard.readText 读取数据,然后通过 API 同步到数据库。

Const Text=Navigator.Clipboard.Readtext();

麻烦的地方在于,如果要从其他设备同步数据到手机上,需要要先从数据库拉取数据,然后再通过 navigator.clipboard.writeText() 去写入剪切板。而这两步操作之间有一段时间差,这个时间差会导致,原有的 Copy 实现逻辑在 iPhone safari 上行不通。

在 Google 搜索了一番,最后在 Claude Sonet 的加持下,才慢慢了解到问题出在哪里。为了确保安全,在 iOS Safari 上,剪贴板 API 的调用通常需要用户的直接交互。如果异步操作时间过长,可能会被视为非用户互动,那么剪切板的写入操作可能会被 safari 拒绝。

为验证这种说法,我做了几个测试,目前观察到的现象有:

  1. 点击 copy 时,如果直接写入一个固定的字符串(比如 "hello"),则没有问题;
  2. 点击 copy 时,如果直接通过 randomText() 生成一个随机的文本,再写入剪切板也没有问题;
  3. 点击 copy 时,如果先通过 API 从远程读取数据,再写入到剪切板,则写入失败。

第一种是直接交互,符合 Apple webkit 的操作规范。

第二种多了一步字符串生成的模拟步骤,但耗时极短,很接近直接交互。

最后一种耗时范围在 0.5 ~ 1 秒之间,已经不能算直接交互了,因此我猜测是这一步导致写入失败。

曲线救国

针对这个问题,我考虑了几种解决方案,比如:

  • 通过 websocket/sse 实时同步,但会引入额外复杂度,API,前端都需要适配,旧的设备可能不支持;
  • 定时(每隔一秒)轮询,但这样会增加服务器的负担,而且也不是很实时;

最终权衡复杂度和实时性,实现了一个“曲线救国”的方法,也就是分两步走。在 safari 上点击 copy 时,先从数据库读取数据,同步完成后,再点击一次 copy 写入到剪切板。

这个方案不是很让人满意,但也是一个折中的办法了。实际体现下,用户体验还是可以接受的,只是多了一次点击而已。

【完】