你是否也曾为了在 Electron 应用中实现更贴近原生体验的 UI 效果而绞尽脑汁?特别是像 WinUI3 那样拥有细腻渐进显示动画的 Tooltip 组件,总让人心向往之。

也许你尝试过在渲染进程中实现这些动画,但当你的 Tooltip 需要像原生应用一样,自由地显示在任何渲染进程窗体之外时,问题就来了。你需要为 Tooltip 单独分配渲染进程窗体,但为了性能,你可能选择了在多个窗口间共享同一个 Tooltip 实例,这本是明智之举,但随之而来的跨进程通信却成了新的噩梦。
IPC 的困局:异步与"穿帮"
Electron 的 IPC 机制虽然强大,但其异步的特性在需要精确同步的动画场景下却显得力不从心。想象一下,你需要动画式地显示或隐藏共享的 Tooltip,或者动态地改变其显示的内容。如果通过 IPC 通知 Tooltip 渲染进程进行动画,再反过来通知主进程动画结束,(因为主进程单独计时可能更不可靠,)这中间的延迟和不同步很可能导致"穿帮"------共享的 Tooltip 在不该出现的时候出现,在该消失的时候不消失,或者内容更新不及时,给用户留下粗糙、不专业的印象。
另辟蹊径:让动画在主进程中舞动
有没有想过,动画的控制中心可以不在渲染进程,而是在 Electron 的"大脑"------主进程中?这听起来可能有些反直觉,毕竟渲染进程通常是动画的主场。但对于我们这种需要极致同步和避免跨进程通信时间开销的场景来说,这反而是一条更平坦的道路。
将动画逻辑放到主进程,你可以直接、同步地控制 Tooltip 窗口的属性(例如透明度、位置),从而实现流畅的动画效果,彻底告别 IPC 可能带来的时间差和"穿帮"隐患。
你的秘密武器:Tickerjs
你可能会问,Node.js 环境下,没有像浏览器那样方便的 requestAnimationFrame
API,如何在主进程中实现流畅的动画呢?别担心,你并不需要从零开始造轮子。
隆重介绍你的秘密武器------ Tickerjs
!这是一个你可能还未曾注意到的宝藏,它为你打开了在 Electron 主进程中轻松实现动画的新世界大门。
Tickerjs
并非简单地模拟 requestAnimationFrame
,它更懂你在 Node.js 环境下进行动画的痛点:
- 指定帧率,掌控节奏: 通过
frameRate
参数,你可以精确地控制动画的帧率,比如固定到 60fps。这让你能够预先计算好动画的关键帧(例如贝塞尔曲线的阶段值),避免了运行时动态计算的性能开销。 frameCount
在手,状态尽在掌握:Tickerjs
的回调函数会提供当前所在的帧计数 (frameCount
),即使系统繁忙导致掉帧,你也能准确地知道当前动画应该处于哪个阶段,从而保证动画状态的正确性,避免画面错乱。- 灵活的结束方式: 你无需预设动画的总时长。在每帧的回调中,你可以返回一个特定的值来立即结束动画,这在 Node.js 这种没有原生
raf
的环境下非常实用,让你的动画在达到预期效果后能立刻干净利落地停止。 - 可控的动画生命周期:
Tickerjs
提供的cancel
函数允许你强制停止动画,并且不会触发动画结束的处理回调。这让你可以在动画过程中根据需要随时打断它,方便你编写复杂的交互逻辑,而不用担心动画结束回调会过早执行。
typescript
// 相关函数的签名
const requestAnimationFrames: (args: {
totalTime?: number;
frameRate?: number;
actionOnStart?: () => void;
actionOnFrame: (args: {
remainingTime: number;
frameCount: number;
delta: number;
time: number;
}) => void | {
continueHandleFrames: boolean;
};
actionOnEnd?: () => void;
}) => (never | (() => void));
想象一下你的工作流程:
- 当需要显示 Tooltip 时,主进程接收到通知。
- 主进程使用
Tickerjs
启动一个动画,例如改变 Tooltip 窗口的透明度,使其从完全透明到完全可见。 - 在
Tickerjs
的每一帧回调中,你根据当前的frameCount
和预先计算好的贝塞尔曲线值,更新 Tooltip 窗口的透明度。 - 当达到指定的帧数后,你在回调中返回结束信号,
Tickerjs
会停止动画。 - 如果用户在动画过程中触发了其他操作,你可以随时调用
Tickerjs
返回的cancel
函数来停止动画。 - 在
Tickerjs
的动画结束处理回调中,你可以安全地执行动画完成后才需要进行的操作。
javascript
// 简单的示例
import { sixty, requestAnimationFrames } from '@projectleo/tickerjs'
const cancelAnimationFrames = requestAnimationFrames({
frameRate: sixty.fps,
actionOnStart: () => {
renderer.setPosition(windowX, windowY)
},
actionOnFrame: ({ frameCount }) => {
const opacity = [
0.161,
0.31,
0.449,
0.576,
0.687,
0.785,
0.867,
0.929,
0.971,
0.996,
1,
][frameCount - 1]
if (typeof opacity === 'number') {
renderer.setOpacity(opacity)
} else {
return {
continueHandleFrames: false,
}
}
},
actionOnEnd: () => {
// 保底,防止系统繁忙正好跳过最后一帧
renderer.setOpacity(1)
},
})
为什么这种方式更胜一筹?
- 同步控制,告别"穿帮": 主进程直接控制窗口属性,动画过程完全同步,避免了异步 IPC 带来的因时间差导致的视觉问题。
- 性能更优: 对于共享的 UI 组件,将动画放到主进程可以减少不必要的渲染进程间的通信开销。
- 逻辑更清晰: 动画逻辑集中在主进程,代码结构更简单,易于理解和维护。
- 更贴近原生体验: 通过精确的帧率控制和流畅的动画,你的自定义 Tooltip 将更接近原生 WinUI3 组件的体验。
现在就开始尝试!
如果你也正在为 Electron 应用中自定义窗口动画的实现而苦恼,特别是涉及到共享组件和跨进程同步的问题,不妨尝试一下将动画逻辑放到主进程,并使用 Tickerjs
来简化你的开发工作。相信我,你会惊喜地发现,原来在主进程------ Node.js 环境下也能如此优雅地实现流畅的窗口动画!
赶快行动起来,让你的 Electron 应用拥有更精致、更专业的用户界面吧!
(注:文章中动图里的应用基于 Electron 开发,动图的帧率有点问题,实际的动画效果要更流畅。)