我正在做什么,听什么?
杂谈, Go

我正在做什么,听什么?

我正在做什么?

9分钟阅读

前言

在你看这篇文章的时候,可能会注意到顶部的头像旁边,有一个应用的图标和一个歌曲播放的展示。

图标实时显示了我正在我的 mac 上使用的软件,而音乐也与 macOS 中的 Apple Music 进行同步,显示歌曲,专辑封面和播放进度。

这篇文章就来说一下如何去实现这样的效果,顺便也从动画的角度说一下如何优化音乐组件中,鼠标hover 状态下专辑封面的动画效果。

架构

  1. macOS 上运行 Go 后端服务,后端服务中注册了定时任务
  2. 定时任务通过 swift 语言编写的工具去获取所需的信息
  3. 获取信息后上报到部署在 Vercel 的 Go Runtime API
  4. Go Runtime API 将数据缓存到 Redis 中
  5. 页面访问通过 Go Runtime API 获取对应的数据

为什么要通过 Go 调用 swift 的根本原因就是: xcode 实在是.. 不太好用。

获取当前使用应用程序

通过 swift 语言可以很方便的使用macOS 的系统 API 来获取当前使用的程序名。

代码也很简单:

获取当前收听的音乐信息

在 macOS 语言中,直接获取当前播放音乐信息的 API 是私有的,因此如果需要获取信息的话需要通过 Apple Script 来获取, 脚本的部分我通过请教 GPT 老师来完成。

Swift GPTs

编写完脚本后,基础的信息可以很方便的获取,但是专辑封面的同步问题就会比较困难,因此我没有直接将整个专辑图片的数据包含在 swift 程序的返回中,而是将图片存储在 macOS 的某个目录下,Go 程序再通过目录去获取当前的专辑封面图。

Go 程序上报数据

上报数据对于 Go 语言来说是十分轻松的,我采用了老朋友 Goframe 框架作为基础框架进行数据的处理和上报,Vercel Go Runtime API 稍后介绍,上传部分简单介绍一下如何存储专辑封面的问题。

主要通过 GitHub API 上传专辑图片至 GitHub 仓库,访问时通过 jsDelivr CDN 来加速访问速度。

首先 Go 程序通过 swift 程序返还的路径读取专辑封面图转换为 base64 数据,然后通过 GitHub API 在 GitHub 仓库中 commit 并上传文件,专辑封面图就可以通过 CDN 进行访问了。

Vercel Go Runtime API

因为在后端的数据保存上,对于 APP 与 Music 分别只需要两个接口(更新,获取)就可以完成功能,因此云函数的方式去部署是相对比较合适的,恰好 Vercel 的 Go Runtime 正在测试中,同时也可以很好的融合进前端项目中,因此采用了 Vercel 的云函数来构建。

Using the Go Runtime with Serverless Functions

缓存中间件

因为云函数是没有持续的运行环境的,因此存储数据需要连接外部的组件来进行,我选择了 Redis 作为缓存中间件,通过 go-redis 库连接与操作 Redis。Redis 可以选择服务器安装 Redis 或者其他服务商提供的 Redis 服务,比如 Vercel 官方的KV服务或直接使用背后的提供商 Upstash。

Upstash: Serverless Data for Redis® and Kafka®

简略的 Vercel Go Runtime API 代码如下:

(新)缓存中间件-Next.js API 实现

除了使用 Go 语言去实现外,我们也可以直接通过 Next.js 的 Serverless Function 来实现,在接口中订阅 Redis 的频道来获取最新数据。为了优化用户体验,我们也会在 Redis 中缓存最新一条数据。

首先定义一些必要的头部信息,其中 dynamic 变量会告诉 vercel 永远不要缓存这个接口的数据。

我们使用 ReadableStream 来存储信息流,再通过 SSE 的形式返回。

中段小结

至此,有关 macOS 应用与音乐数据获取与存储的部分就完成了,接下来便是前端通过 API 获取数据然后展示在页面上。

前端获取数据

其实在获取数据的方式上,可以选择的有很多种,比如轮训,长连接,websocket,SSE,在本次项目中我选择 SSE 搭配 Redis 订阅发布的方式去实现。

关于流传输,可以参考 vercel 官方的文章:

Streaming Data on Vercel

👉 Vercel 中 node.js 环境下超时时间为10s,这意味着我们需要处理重连的逻辑。

我们用到 EventSource 来对上文中的 Next.js API 进行连接与获取数据流。

同时我们手动处理收到数据后的解析与重连逻辑。

我们使用 useEffect 将这个 SSE 获取的功能挂载在组件上,完整代码如下:

这样子前端就可以通过 SSE 获取源源不断的数据了,同时也避免了轮训带来的额外消耗。

关于动画

前端获取数据后,需要的就是将数据传入至组件中展示。

这里重点解析一下播放组件的动画显示逻辑。

播放组件主要展示专辑封面、歌曲名、播放状态与播放进度,其中播放进度以进度条的形式展示。

专辑封面提取色彩

在专辑封面图的四周,有依据图片而生成的颜色光晕,同时进度条的颜色也与光晕颜色一致。

这里通过一个函数可以提取图片的主题色:

同时设置在每次获取到新专辑封面时重新提取

光晕用 box-shadow 来绘制

进度条搭配播放进度,使用 background-image 搭配 width 来实现效果

播放组件初始化动画

有关动画的部分采用 framer-motion 去实现,只需定义动画参数,用<motion.div>包裹元素即可。

专辑封面弹出

当鼠标移动到播放组件上时,组件会消失,专辑封面由模糊渐渐变成清晰的专辑封面大图。

当鼠标移出播放组件,专辑封面消失,播放组件由模糊渐渐还原,最后加上回弹的效果增加真实性。

首先,我们需要确定触发范围,如果我们直接将触发范围绑定在播放组件卡片上,就有可能造成弹出专辑封面图后,鼠标已经不再触发范围内了,用户体验会大幅下降。

理念是来自于菜单中也很常见的二级菜单消失的范围判定,合理的做法应该是将触发范围扩展至鼠标到二级菜单底部。

而在本项目中,我采用以播放卡片为中心,向外增加 padding 的方式来确定触发范围。

在播放器组件外部增加一个额外的 div 来绑定鼠标的触发,通过 py-4 在 y 轴上增加触发的面积。

剩下只需要定义好触发的动画参数即可。

在最外层用 AnimatePresence 包裹,确保可以触发进入与退出动画,同时将 mode 设置为 wait,这确保页面上只会出现一个动画组件。

最终专辑动画的效果就实现了。

感谢阅读📖。

相关文章