自从落网复活以后,它就成了我日常干活时的主力 BGM。但它本身只是一个网页,如果窗口开的多了,这个时候想切歌或者看看歌名,就得在多个窗口和 tab 之间来回切换,略微有点麻烦。
鉴于此,我把网站包装成了一个简易的桌面客户端,修改它的样式,将它变成了一个桌面浮窗。项目已发布且开源在 flora-studio/luoo-player 。
制作桌面应用并非我常涉及的领域,这篇文章主要记录下与此相关的一些技术探索。
说起桌面应用,第一时间就会想到 electron、nwjs、tauri 这三巨头个在前端领域最主流的选择。
不过在和老婆讲这个小工具的功能时,她说,听上去像是针对音乐的画中画功能。这启发了我,让我一下子想到了尚处于实验性的 Document Picture-in-Picture API 。如果这个 API 可用,我们就能够通过一个浏览器插件解决问题,而无需真的打包一个桌面应用出来,轻量很多。
说干就干,我进行了一番测试。浏览器的视频画中画功能很简单,就是调用 video.requestPictureInPicture()
完事。我预想中,让任意元素进入画中画,无非也是一样,将这个 API 扩展到任意 dom 元素。
不过它实际并没有想象中的那么开箱即用,而是更偏底层一些。一个浮窗可以视为一个独立的窗口,就像一个全新的 tab 页。而父窗口可以完全操控子浮窗的 window
和 document
,又像是一个同源的 iframe。我们需要在打开浮窗时手动把元素添加到浮窗的文档中,并把 CSS 也复制过去。在关闭浮窗时,也需要我们手动把元素还原回去。
跟随 MDN 的样板代码,浮窗的展示功能很顺利地搞定了。不过涉及到交互时却遇到了麻烦:窗口会出现无规律的抖动(这一点似乎是 <a>
元素的问题,MDN 的 example 也存在此问题),且所有的点击事件都没有生效。其实对于这一点我并不意外。雀乐的网站是用 Next.js 即 React 实现的。正常情况下,我们基于框架编写代码,React 会通过 vdom 操作真实 dom,点击事件也会走 React 自己的合成事件机制。现在我们越过了 React 直接去操作真实 dom,要没有问题反而才是大问题。
如果你是在正经 React 开发过程中使用画中画功能,你可以尝试使用 React Portal 将元素渲染到浮窗中。但我们只是一个插件,能做的事情有限。故我暂时停止了对浏览器插件功能的进一步研究,继续把目光放回打包桌面应用上。
要在 electron、nwjs、tauri 中三选一,答案是不言而喻的。一方面,我们想做的只是一个小工具,打包一整个浏览器进去并不值得。另一方面,我们是对现有的网站再封装,网站本身就会适配各大主流浏览器,没必要再去考虑兼容性问题。因此 tauri 就是最合适的选择。
tauri 的教程通篇都是开发自己的网页应用。像我这样要把一个现有第三方网站打包成应用的是小众场景,参考资料很少。
tauri 为几乎每种原生能力都提供了 Rust 和 JavaScript 两种版本的 API,即使是不会 Rust 的开发者上手起来也不会太艰难。一开始我自然选择了自己熟悉的 JavaScript API,但由于我们的前端并非通常的 localhost
而是一个第三方网站,于是遇到了一些”安全问题”。
因为时隔有点久,我已经忘了具体的报错是什么。我不确定是否和操作系统有关:我在 macOS 上看到了一些错误信息,而 Windows 上并没有;我也不确定是否和 dev 环境有关:查资料好像说 tauri 在 macOS 上的运行,dev 和正式打包的运行方式也有所区别。
尝试了几种方法绕开后无果,我最终还是决定用 Rust 来写了。毕竟我们是打包第三方网站,把原生逻辑放在服务端也合理,而不懂 Rust 是我的问题,不是 Rust 的问题。
于是不出所料地事情开始变得举步维艰了起来。我既不懂 Rust 的语法,又不熟悉 tauri 的 API,我甚至不知道怎么把一段代码抽离到一个单独的方法里去。也不是没有尝试过 AI 的帮助,但效果只能说差强人意。一会儿是幻觉出现了不存在的配置项;一会儿是生成了 tauri v1 的代码(这个问题比较严重),要么就是为了解决编译的问题反倒使得功能出现了 bug。
不过总归也不是什么特别复杂的功能,对照着模版代码摸爬滚打一番,也算是实现了。真正让人挠头的还是不同操作系统之间的能力差异。比如,不同平台对窗口大小是否包含边框、标题栏的判断;macOS 禁用不了 resize;Windows 快捷键不起作用;不同平台对点击和拖动事件的处理不同等等。这只是个 side side project,我也没有兴趣再去深究,做个差不多就完事。
第一次接触 Rust,虽然很仓促,但感觉其实挺有意思的。平时在写代码的时候,其实也会有直觉某些做法是好的,某些是不太好的。而 Rust 把这一部分强制了。Rust 也确实做到了只要能通过编译,那么跑起来也大概率没问题;语法错误给的提示也很贴心(大概是犯的都是初学者典型错误吧)。不过相对地,开发体验就没这么好了。有的时候你只想快速验证一些东西,但却不得不先解决所有的编译错误,非常打断心流。还有就是作为编译型语言,hot reload 速度还是慢了。哪怕只是个小项目,一次重新编译也要 10~20 秒,作为前端开发者会很不习惯。
虽然通过 tauri 做出了成品,但我还是对画中画方案念念不忘,总觉得还是错过了什么。很快我便意识到了问题所在:我陷入了对画中画功能的思维定式中。
最典型的画中画功能自然是将原有的一块区域转移到单独的浮窗中。但浏览器提供的 API 并没有限制我们只能这么做。它提供的是一个完全由你控制的窗口,就像是一块任你挥洒的画布。我们完全可以不使用原网站的 dom 元素,而是自己手搓一个播放器界面。又因为浮窗与原网页是同源的,我们可以将点击事件转发到原网页的对应按钮,并同步播放状态与原网页一致,让它看上去就像是从原网页抠下来的一样。
一念天地宽,之后就是按部就班的开发时间了。
总之,我们现在有了两个版本的播放器了。其一是独立 exe,其二是一个浏览器插件。
当然它们之间会有一些区别。最大的区别是通过浏览器画中画打开的浮窗,上方会带有一个无法去除的标题栏,且只有在标题栏区域才可以拖拽窗口;而 exe 版本的浮窗可以隐藏标题栏并直接拖拽窗口,更为简洁易用。其他的诸如禁止 resize、托盘图标等更加 native 的特性,也都是只是 exe 版本才可以控制的。
不过虽然桌面版本的上限更高,但插件版本充分利用的浏览器的现有能力,有着更小的体积与更美好的开发体验。并且 Document Picture-in-Picture 目前仍然是实验式 API,有理由相信未来会开放更多的可定制功能。如果让我选择,仅就这个用例而言,我会更喜欢浏览器插件的版本。