基于 Eventa IPC 的 Electron Vue 组合式 API 库 @proj-airi/electron-vueuse 实战指南
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
本文围绕 AIRI 项目中面向 Electron 渲染进程的 VueUse 风格组合式函数(composable)集合@proj-airi/electron-vueuse展开,讲解它如何把鼠标跟踪、窗口边界、自动更新等 Electron 高频行为封装为可复用的 Vue 响应式 API,并基于 Eventa 上下文/调用(context/invoke)模式打通渲染进程与主进程的 IPC 通信。读者读完可掌握该包的完整导出面、各 composable 的调用方式与参数语义,以及主进程useLoop/createRendererLoop循环工具的用法,并能据此在 AIRI 桌面应用中快速搭建自定义无边框窗口的交互逻辑。
包定位与设计思路
@proj-airi/electron-vueuse是 AIRI monorepo 中的内部工具包(private: true),其定位在 README 中表述为 "VueUse-like composables and helpers shared across AIRI Electron apps"——即为所有 AIRI Electron 应用共享的、模仿 VueUse 风格的组合式函数与辅助工具集合。
从 包清单 可以看到它的几个关键设计决策:
- 以 Eventa 为 IPC 基座:依赖
@moeru/eventa(提供defineInvoke、createContext)以及@proj-airi/electron-eventa(提供 IPC 契约定义)。 - 以 VueUse 为交互基座:直接依赖
@vueuse/core,鼠标跟踪等能力复用了useMouse、useAsyncState、useIntervalFn等成熟实现。 - 依赖版本约束:
peerDependencies要求electron >=39 <44且vue >=3,意味着该包面向较新的 Electron 版本设计。 - 双入口导出:
exports定义了"."(渲染进程 composables,输出dist/index.mjs)与"./main"(主进程循环工具,输出dist/main/index.mjs)两个子路径。
一个值得强调的架构原则是:IPC 的契约定义与使用侧分离。包内并不自行声明 Electron IPC 的 channel 名称,而是统一从@proj-airi/electron-eventa引入(如electron.window.getBounds、cursorScreenPoint、bounds等事件与调用定义),README 对此有明确说明:IPC contract 定义请使用@proj-airi/electron-eventa。这样 renderer 侧只写"我要调用什么",channel 字符串、参数类型、返回值类型都集中在契约包中维护。
渲染进程入口与 Eventa 上下文基础设施
渲染进程的全部公开 API 由 src/index.ts 统一导出,覆盖四大类:
- 鼠标相关:
useElectronMouse、useElectronRelativeMouse、useElectronMouseInElement、useElectronMouseInWindow、useElectronMouseAroundWindowBorder; - 窗口相关:
useElectronWindowBounds、useElectronWindowResize、useElectronAllDisplays; - 自动更新:
useElectronAutoUpdater; - 上下文基建:
useElectronEventaContext、useElectronEventaInvoke及测试辅助resetElectronEventaContextForTesting。
其中最底层的是 use-electron-eventa-context.ts,它把 Eventa 的 renderer 适配器封装成全局单例:
let sharedContext: EventaContext | undefined export function getElectronEventaContext(ipcRenderer?: IpcRendererLike): EventaContext { sharedContext ??= createContext(resolveIpcRenderer(ipcRenderer)).context return sharedContext } export function useElectronEventaContext(ipcRenderer?: IpcRendererLike): ShallowRef<EventaContext> { return shallowRef(getElectronEventaContext(ipcRenderer)) }resolveIpcRenderer的解析优先级值得注意:如果调用方显式传入ipcRenderer则优先使用;否则回退到globalThis.window?.electron?.ipcRenderer。若两者都不可用,会抛出明确错误Electron ipcRenderer is not available. Pass it explicitly to useElectronEventaContext().——这在预加载脚本未把ipcRenderer暴露到window.electron的调试场景下非常有用。
useElectronEventaInvoke则把契约对象转换为可调用函数:
export function useElectronEventaInvoke<Res, Req, ResErr, ReqErr>( invoke: InvokeEventa<Res, Req, ResErr, ReqErr>, context?: EventaContext, ) { return defineInvoke(context ?? getElectronEventaContext(), invoke) }README 中的官方示例正是这种模式的浓缩:
import { electron } from '@proj-airi/electron-eventa' import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse' const openSettings = useElectronEventaInvoke(electron.window.getBounds)调用openSettings()即可得到契约约定的返回值。由于契约包 electron/index.ts 中cursorScreenPoint、startLoopGetCursorScreenPoint、bounds、startLoopGetBounds等都是通过defineEventa/defineInvokeEventa声明的具名事件,channel 命名(如eventa:event:electron:window:bounds)与消息体类型在编译期即可被校验,渲染进程代码无需关心底层ipcRenderer.send/invoke细节。
鼠标跟踪系列:从屏幕坐标到窗口内元素命中
鼠标相关 composable 是这套工具链的亮点,它们把"系统级鼠标位置"转化为 Vue 响应式数据,整体呈分层结构。
屏幕级鼠标:useElectronMouse
use-electron-mouse.ts 的核心思想是把 Electron 主进程推送的屏幕坐标事件转译为标准MouseEvent再喂给 VueUse 的useMouse:
context.on(cursorScreenPoint, (event) => { const e = new MouseEvent('mousemove', { screenX: event.body?.x, screenY: event.body?.y }) sharedEventTarget?.dispatchEvent(e) })useElectronMouseEventTarget维护一个模块级共享的EventTarget单例,首次调用时通过defineInvoke(context, startLoopGetCursorScreenPoint)()通知主进程启动光标位置轮询/推送循环。useElectronMouse在此基础上以type: 'screen'调用useMouse,得到的就是屏幕绝对坐标(screenX/screenY),这与浏览器内useMouse默认的页面坐标语义不同,是理解后续所有派生 API 的基础。
窗口相对坐标:useElectronRelativeMouse
use-electron-relative-mouse.ts 是鼠标系列的"数学中枢":它同时消费屏幕坐标与窗口边界,用computed求差得到窗口相对坐标:
const x = computed(() => mouse.x.value - windowX.value) const y = computed(() => mouse.y.value - windowY.value)窗口相对坐标对于"鼠标悬停在窗口哪个位置"这类判断至关重要,也是下面几个 API 的实现基础。
元素级命中:useElectronMouseInElement / useElectronMouseInWindow
use-electron-mouse-in-element.ts 提供与 VueUseuseMouseInElement对齐的返回值:elementX/elementY(元素内相对坐标)、elementPositionX/Y(元素左上角位置)、elementWidth/Height、isOutside、sourceType以及stop()。它的update()用getBoundingClientRect()计算元素位置,并监听三类变化源:
useResizeObserver与useMutationObserver(attributeFilter: ['style', 'class'])跟踪元素自身的尺寸/样式变化;watch([targetRef, x, y], update)跟踪鼠标移动与目标切换;scroll(捕获阶段)与resize事件处理页面滚动与窗口缩放。
useElectronMouseInWindow则只是useElectronMouseInElement(undefined, options)的别名——把"元素"退化为整个document.body,语义即"鼠标是否在窗口内、位于窗口内的哪个相对位置"。
窗口边缘感应:useElectronMouseAroundWindowBorder
use-electron-mouse-around-window-border.ts 是典型的"自绘无边框窗口"能力:检测光标是否贴近窗口四边与四角,用于显示自定义缩放手柄。它基于useElectronRelativeMouse的坐标与useElectronWindowBounds的尺寸做纯计算,注释里明确写出设计意图——"Fast path: no extra listeners; reuses existing mouse and window bounds streams",即复用既有数据流、不额外挂监听器。
它接受两个可选参数:
| 参数 | 默认值 | 语义 |
|---|---|---|
threshold | 8 | 距窗口边缘多少像素内算"贴近"(px) |
overshoot | 同threshold | 允许鼠标略微越出窗口仍算贴近,便于用户"摸索"到边缘 |
返回值包括nearLeft/nearRight/nearTop/nearBottom、四个角nearTopLeft/nearTopRight/nearBottomLeft/nearBottomRight以及汇总的isNearAnyBorder,均以模块级单例数据流驱动,多个组件同时调用不会产生重复监听。
窗口与显示器:边界跟踪、窗口缩放与多屏枚举
窗口边界:useElectronWindowBounds
use-electron-window-bounds.ts 与鼠标事件流同构:模块级持有x/y/width/height四个ref,首次调用时订阅 Eventa 的bounds事件并启动startLoopGetBounds推送循环,之后所有调用者共享同一份响应式数据:
context.on(bounds, (event) => { windowBoundsX.value = event.body.x // ... y / width / height }) void defineInvoke(context, startLoopGetBounds)()返回值即{ x, y, width, height }四个Ref。
窗口缩放(Windows 专用):useElectronWindowResize
use-electron-window-resize.ts 提供无边框窗口在 Windows 平台的自定义缩放能力。handleResizeStart(e, direction)首先通过electron.app.isWindows契约校验平台(非 Windows 直接返回),随后preventDefault/stopPropagation,并在document上注册mousemove与mouseup监听:移动时计算screenX/screenY增量,调用electron.window.resize({ deltaX, deltaY, direction })请求主进程调整窗口尺寸,mouseup时移除监听。direction类型ResizeDirection同样来自@proj-airi/electron-eventa。
由于缩放逻辑需要拦截原生标题栏行为,通常配合useElectronMouseAroundWindowBorder的isNearAnyBorder来切换cursor样式,形成完整的"边缘感应 → 光标变化 → 拖拽缩放"交互闭环。
多显示器枚举:useElectronAllDisplays
use-electron-all-displays.ts 是对多显示器场景的封装:基于useAsyncState调用electron.screen.getAllDisplays,并借助useIntervalFn每 5 秒自动刷新一次:
const { state: allDisplays, execute } = useAsyncState(() => getAllDisplays(), []) useIntervalFn(() => { void execute() }, 5000)初始值为空数组,适合在副屏布局、跨屏定位等场景中消费。
自动更新:useElectronAutoUpdater
use-electron-auto-updater.ts 把 electron-updater 的完整状态机暴露为响应式数据,契约同样来自@proj-airi/electron-eventa/electron-updater。它维护一个state: Ref<AutoUpdaterState>,默认{ status: 'idle' },并派生三个常用判定:
isBusy:status为'checking'或'downloading';canDownload:status === 'available';canRestartToUpdate:status === 'downloaded'。
对外暴露四个操作函数(均为useElectronEventaInvoke包装):checkForUpdates、downloadUpdate、quitAndInstall,以及初始化时拉取当前状态的getState。onMounted时先getState同步一次状态,再订阅electronAutoUpdaterStateChanged事件持续更新;两步都包了 try/catch,避免在非完整环境中挂载报错。UI 层可据此渲染"检查中 / 可下载 / 可重启更新"等状态按钮。
主进程循环工具:useLoop 与 createRendererLoop
主进程侧的工具通过子路径@proj-airi/electron-vueuse/main导入,README 给出了入口示例:
import { createRendererLoop } from '@proj-airi/electron-vueuse/main'通用定时循环:useLoop
loop.ts 提供带互斥防重入的定时循环。关键实现点:
- 默认间隔:
options.interval ?? 1000 / 60,约 60Hz(16.67ms),契合光标/窗口边界跟踪的实时性需求; - Mutex 防重入:使用 es-toolkit 的
Mutex,若上一轮fn()尚未结束(异步任务较长),本轮 tick 直接跳过,避免回调堆积; - 定时器:使用
@moeru/std的setClockInterval/clearClockInterval而非原生setInterval; - 生命周期:
autoStart默认为true(构造时即启动),返回值提供start/resume/pause/stop四个控制方法(start与resume等价,pause与stop等价)。
export interface LoopOptions { interval?: number autoStart?: boolean }感知渲染进程存活的循环:createRendererLoop
renderer-loop.ts 是面向"主进程持续驱动渲染进程"场景的增强封装,它在useLoop之上增加了三项防御:
- 存活检查:每轮 tick 先
ensureRendererIsAvailable,内部调用isRendererUnavailable(window)——即window.isDestroyed() || webContents.isDestroyed() || webContents.isCrashed(),任一为真则停止循环; - 错误熔断:用
attemptAsync包裹run(),若错误消息包含'Render frame was disposed before WebFrameMain could be accessed'(shouldStopForRendererError判定),说明渲染帧已销毁,主动stop();其他错误则原样抛出; - 事件兜底:
stopLoopWhenRendererIsGone同时监听closed、webContents的destroyed与render-process-gone三个事件,任一触发即停止循环。
createRendererLoop的autoStart默认false,需要显式调用start();而start()内部会再做一次存活检查,保证不会对已销毁窗口启动循环。同文件还导出safeClose(window)——关闭前先检查渲染进程是否可用,避免对已崩溃的窗口执行close()引发异常。
createRendererLoop({ window, run: async () => { /* 例如向渲染进程推送实时数据 */ }, interval: 1000 / 60, })组合示例:一个自绘无边框窗口的典型用法
将上述 API 组合起来,即可在渲染进程中实现"窗口边缘感应 + 光标样式切换 + Windows 自定义缩放 + 状态栏自动更新提示"的完整交互:
import { useElectronMouseAroundWindowBorder } from '@proj-airi/electron-vueuse' import { useElectronWindowResize } from '@proj-airi/electron-vueuse' import { useElectronAutoUpdater } from '@proj-airi/electron-vueuse' import { computed, watch } from 'vue' const { isNearAnyBorder, nearTop, nearBottom, nearLeft, nearRight } = useElectronMouseAroundWindowBorder({ threshold: 8 }) const cursor = computed(() => { if (nearTop || nearBottom) return 'ns-resize' if (nearLeft || nearRight) return 'ew-resize' return 'default' }) watch(cursor, (v) => { document.body.style.cursor = v }) const { handleResizeStart } = useElectronWindowResize() // 在四个边缘的透明拖拽条上绑定 @mousedown="e => handleResizeStart(e, 'bottom-right')" const { state, isBusy, canDownload, canRestartToUpdate, checkForUpdates, downloadUpdate, quitAndInstall } = useElectronAutoUpdater() // 按 isBusy / canDownload / canRestartToUpdate 渲染更新按钮状态在这个示例里,useElectronMouseAroundWindowBorder提供边缘判定(纯计算、零额外监听),useElectronWindowResize处理 Windows 下的拖拽缩放,useElectronAutoUpdater驱动更新 UI——三者共享同一条由useElectronEventaContext单例建立的 Eventa 数据流,这正是该包"shared across Electron apps"设计目标的直观体现。
测试与调试要点
- 显式注入 ipcRenderer:
useElectronEventaContext(ipcRenderer)/useElectronEventaInvoke(invoke, context)都支持传入自定义 context,便于在测试中替换真实 IPC 通道。 - 重置单例:
resetElectronEventaContextForTesting()会把模块级sharedContext置空,供测试用例之间隔离上下文状态。 - 错误提示:若在非 Electron 环境(或未暴露
window.electron.ipcRenderer)调用,会抛出Electron ipcRenderer is not available...错误,这是定位环境配置问题的最快信号。 - 契约一致性:IPC 契约(channel 名、消息体结构)一律以
@proj-airi/electron-eventa为准,渲染与主进程两侧应引用同一契约包,避免字符串漂移。
相关源码导航
- 包说明与用法: README
- 包配置与导出映射: package.json
- 渲染进程全部导出: src/index.ts
- Eventa 上下文基建: use-electron-eventa-context.ts
- 鼠标系列: use-electron-mouse.ts、use-electron-relative-mouse.ts、use-electron-mouse-in-element.ts、use-electron-mouse-around-window-border.ts
- 窗口系列: use-electron-window-bounds.ts、use-electron-window-resize.ts、use-electron-all-displays.ts
- 自动更新: use-electron-auto-updater.ts
- 主进程循环: src/main/loop.ts、src/main/renderer-loop.ts
- IPC 契约定义: packages/electron-eventa/src/electron/index.ts(如
cursorScreenPoint、startLoopGetCursorScreenPoint、bounds、startLoopGetBounds)
需要注意的是,本包目前仅作为内部共享库被引用(如 stage-pages 中已有将其移植复用的 TODO 标注),使用时请以 monorepo 当前工作区版本为准;IPC 契约若需扩展,应优先在@proj-airi/electron-eventa中声明,而非在本包内硬编码。
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考