- 人工智能
- AI Agent
- MCP 服务
- 工具调用
- GUI 自动化
- 语音
- 数字人
- 交互助手
【免费下载链接】airi
💖🧸 自托管、归你拥有的 Grok 风格 AI 伴侣与 waifu / 赛博生命灵魂容器,目标是接近 Neuro-sama 的高度;支持实时语音聊天、Minecraft 和 Factorio 游玩,支持 Web / macOS / Windows。
导读
useIpcRendererInvoke是 VueUse 生态中针对 ElectronipcRenderer.invoke的响应式封装:它把渲染进程向主进程发送请求、等待响应的异步流程,转化为一个可被 Vue 响应式系统直接跟踪的ShallowRef,让异步操作在组合式 API 中"看起来像同步"。本文以 AIRI 仓库中的参考文档(.agents/skills/vueuse-functions/references/useIpcRendererInvoke.md)为主体,结合仓库内packages/electron-vueuse、packages/electron-eventa以及apps/stage-tamagotchi的真实调用链,讲解其用法、类型契约、双重重载语义,以及 AIRI 如何在 preload 桥接、事件契约与测试中落地这一模式。
一、核心概念:把ipcRenderer.invoke变成响应式结果
在原生 Electron 中,渲染进程通过ipcRenderer.invoke(channel, ...args)向主进程发送消息,并返回一个Promise——请求结果只有在 Promise resolve 之后才能拿到,且原生 API 本身不参与 Vue 的响应式系统。
useIpcRendererInvoke的核心思想是:
Reactive
ipcRenderer.invokeAPI result. Make asynchronous operations look synchronous.
即:发送一次 invoke 请求,把返回的响应写入一个ShallowRef,组件模板与computed可以直接读取ref.value,主进程返回后响应式数据自动更新。对于"一次性查询"类操作(读取窗口尺寸、查询配置、拉取状态),这是一种比事件订阅更直接、更贴合声明式 UI 的写法。
对应的 Electron 原生语义为:ipcRenderer.invoke返回Promise<any>,并在主进程的ipcMain.handle注册的 handler 执行完毕后 resolve。
二、基础用法与典型代码
参考文档给出了最简用法:
import { useIpcRendererInvoke } from '@vueuse/electron' import { computed } from 'vue' // enable nodeIntegration if you don't provide ipcRenderer explicitly // Ref result will return const result = useIpcRendererInvoke<string>('custom-channel', 'some data') const msg = computed(() => result.value?.msg)要点拆解:
- 泛型参数
T:useIpcRendererInvoke<string>声明了主进程返回数据的类型,返回值是ShallowRef<T | null>;在响应尚未到达时为null,到达后为完整结果。 - channel 与参数透传:第二个参数
'custom-channel'是 IPC 通道名,'some data'以及后续任意个参数都会原样传给主进程 handler。 - 响应式派生:
computed(() => result.value?.msg)可以安全地链式访问嵌套字段,主进程返回后该 computed 自动重算。 - nodeIntegration 说明:若不显式传入
ipcRenderer,则依赖渲染进程开启了nodeIntegration,从而能从electron模块直接拿到ipcRenderer;否则需要按下面的重载显式传入实例。
三、类型声明:双重重载的语义
参考文档完整给出了两个函数重载:
export declare function useIpcRendererInvoke<T>( ipcRenderer: IpcRenderer, channel: string, ...args: any[] ): ShallowRef<T | null> export declare function useIpcRendererInvoke<T>( channel: string, ...args: any[] ): ShallowRef<T | null>两个重载的差异在于ipcRenderer的来源:
| 重载形式 | ipcRenderer 来源 | 适用场景 |
|---|---|---|
显式传入ipcRenderer: IpcRenderer | 调用方传入(例如import { ipcRenderer } from 'electron') | 关闭了nodeIntegration、通过 preload 或外部注入获得实例 |
仅传channel, ...args | 自动获取 | 开启nodeIntegration,渲染进程可直接访问electron模块 |
两种重载的返回值一致,均为ShallowRef<T | null>。文档注释同时强调:
- invoke 返回
Promise<any>,最终以主进程 handler 的响应 resolve; - 组合式 API 的包装让异步操作"看起来像同步";
ShallowRef意味着只对.value这一层做响应式代理,不对结果对象做深层递归响应化——对于 IPC 返回的大体积对象(如显示配置、窗口矩形、插件清单),可以避免不必要的深度代理开销。
与useIpcRenderer的关系
仓库内的姊妹文档(.agents/skills/vueuse-functions/references/useIpcRenderer.md)提供了useIpcRenderer():它把整个ipcRenderer及其全部 API 响应式化,其中invoke方法同样返回ShallowRef<T | null>,还包含on(组件卸载时自动移除监听)、once、send、sendSync、postMessage、sendTo、sendToHost等方法。两者对比:
useIpcRenderer()适合既需要 invoke 请求、又需要持续事件订阅(on/once)的场景;useIpcRendererInvoke()则只聚焦"发一次请求、等一个结果",API 面更窄、意图更清晰,适合封装成自定义 composable 或在模板中直接消费。
四、AIRI 的落地实践:Eventa 契约驱动的 invoke 封装
AIRI 并没有直接依赖@vueuse/electron,而是在 monorepo 内自建了两个与本文主题直接相关的包,把"invoke 请求"抽象为强类型的事件契约(Eventa):
packages/electron-eventa:负责定义 IPC 契约,例如packages/electron-eventa/src/electron/window.ts中用defineInvokeEventa声明:const getBounds = defineInvokeEventa<ReturnType<BrowserWindow['getBounds']>>('eventa:invoke:electron:window:get-bounds') const setBounds = defineInvokeEventa<void, Parameters<BrowserWindow['setBounds']>>('eventa:invoke:electron:window:set-bounds')channel 采用
eventa:invoke:electron:*的统一命名空间,把通道名与类型绑定在一起,杜绝手写字符串导致的拼写错误。packages/electron-vueuse:提供 VueUse 风格的 renderer composable,其README.md说明了定位:"VueUse-like composables and helpers shared across AIRI Electron apps",并给出示例:import { electron } from '@proj-airi/electron-eventa' import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse' const openSettings = useElectronEventaInvoke(electron.window.getBounds)
从源码结构看,packages/electron-vueuse/src/composables/use-electron-eventa-context.ts中的resolveIpcRenderer完整复现了"显式传入 / 自动获取"的分支逻辑:
function resolveIpcRenderer(ipcRenderer?: IpcRendererLike): IpcRendererLike { if (ipcRenderer) { return ipcRenderer } const globalIpcRenderer = (globalThis as { window?: { electron?: { ipcRenderer?: IpcRendererLike } } }).window?.electron?.ipcRenderer if (!globalIpcRenderer) { throw new Error('Electron ipcRenderer is not available. Pass it explicitly to useElectronEventaContext().') } return globalIpcRenderer }可以推断:AIRI 的自动获取路径不再依赖nodeIntegration,而是从window.electron.ipcRenderer读取——这正是 preload 脚本通过contextBridge注入的全局对象,符合现代 Electron 的contextIsolation安全实践(见apps/stage-tamagotchi/src/preload/shared.ts中的contextBridge.exposeInMainWorld('electron', electronAPI))。若注入缺失,会抛出明确的错误提示,要求显式传入实例。
渲染进程侧的密集使用
在apps/stage-tamagotchi/src/renderer/App.vue中,大量 IPC 请求都以这种形式组织:
const getMainLocale = useElectronEventaInvoke(i18nGetLocale) const setLocale = useElectronEventaInvoke(i18nSetLocale) const getServerChannelConfig = useElectronEventaInvoke(electronGetServerChannelConfig) const listPlugins = useElectronEventaInvoke(electronPluginList) const getGodotStageStatus = useElectronEventaInvoke(electronGodotStageGetStatus) const syncArtistryConfig = useElectronEventaInvoke(artistrySyncConfig)可以看到:插件管理、服务端通道配置、舞台状态、i18n 语言、Artistry 配置同步等模块的"查询—返回"型操作,都通过 invoke 封装暴露为可调用的响应式函数;useElectronEventaInvoke返回的调用结果可直接在组件模板与computed中消费,与useIpcRendererInvoke的"让异步看起来同步"理念一脉相承。
主进程侧的对称实现
IPC 是双向契约,主进程一侧使用@moeru/eventa的defineInvokeHandler注册 handler,例如apps/stage-tamagotchi/src/main/services/airi/auth.ts:
defineInvokeHandler(params.context, electronAuthGetStatus, (_, options) => { /* ... */ }) defineInvokeHandler(params.context, electronAuthStartLogin, async (_, options) => { /* ... */ })handler 中允许async函数——ipcRenderer.invoke返回的 Promise 正是等待这段异步逻辑完成后 resolve,这从主进程侧印证了"invoke 请求会等待 handler 执行完毕并返回结果"的完整闭环。
测试中的验证
仓库测试同样围绕这一 invoke 机制展开。例如apps/stage-tamagotchi/src/main/services/electron/global-shortcut.test.ts用 mock 的invokeHandlers: Map<string, (payload: unknown) => unknown>模拟主进程 handler 注册表,并按 channel(如eventa:invoke:electron:shortcut:register)取出 handler 后手动触发、断言返回值:
const handler = ctx.invokeHandlers.get('eventa:invoke:electron:shortcut:register')! const result = handler!(exampleBinding('toggle')) as { id: string, ok: boolean }这种测试方式与useIpcRendererInvoke的消费模型完全对齐:测试关注的是"channel → handler → 响应值"这一条单向数据流,渲染进程侧的响应式 ref 只是这条数据流的 UI 投影。
五、使用注意事项
综合参考文档与 AIRI 的实践,使用时建议留意以下几点:
- ipcRenderer 来源二选一:要么开启
nodeIntegration让函数自动获取,要么显式传入实例。AIRI 采用的现代做法是后者——通过 preload +contextBridge注入window.electron.ipcRenderer,resolveIpcRenderer在找不到时会抛错而不是静默失败。 - channel 命名一致性:渲染进程 invoke 的通道名必须与主进程
ipcMain.handle注册的通道完全一致。AIRI 通过 Eventa 契约将通道名收敛为eventa:invoke:electron:*并绑定类型,建议业务侧同样建立统一的通道命名与类型声明。 - 返回值形态:结果是
ShallowRef<T | null>,请求未返回时读取为null,模板中需要做空值处理(如v-if或可选链result.value?.msg)。 - 只适合"请求—响应":若需要主进程持续推送事件(如窗口尺寸变化、自动更新进度),应配合事件类 API(
useIpcRenderer的on,或 AIRI 的defineEventa事件通道),而非反复 invoke 轮询。 - 错误处理边界:根据当前类型声明,返回的 ref 只承载成功结果
T或null;主进程 handler 抛错时 invoke 的 Promise 会 reject,这部分错误需要结合具体封装(如 AIRI 的 Eventa 错误通道或调用方的 try/catch)另行处理。
六、结语
useIpcRendererInvoke是一个小而美的组合式 API:它以ShallowRef<T | null>为统一出口,把 Electron 渲染进程与主进程之间"发请求、等响应"的异步过程折叠进 Vue 的响应式数据流。AIRI 仓库在此基础上进一步演化出了 Eventa 契约 +useElectronEventaInvoke的强类型方案,并沉淀出可复用的@proj-airi/electron-vueuse包。读者可以在以下位置继续深入:
- 参考文档:
.agents/skills/vueuse-functions/references/useIpcRendererInvoke.md与姊妹篇 useIpcRenderer.md - 封装实现:use-electron-eventa-context.ts、包说明
- 契约定义:window.ts
- 渲染进程调用示例:App.vue
- preload 桥接:shared.ts
- 主进程 handler 与测试:auth.ts、global-shortcut.test.ts
- 人工智能
- AI Agent
- MCP 服务
- 工具调用
- GUI 自动化
- 语音
- 数字人
- 交互助手
【免费下载链接】airi
💖🧸 自托管、归你拥有的 Grok 风格 AI 伴侣与 waifu / 赛博生命灵魂容器,目标是接近 Neuro-sama 的高度;支持实时语音聊天、Minecraft 和 Factorio 游玩,支持 Web / macOS / Windows。
相关推荐
Airi Electron 渲染进程 IPC 调用响应式封装:useIpcRendererInvoke 实战指南
Airi Electron 渲染进程 IPC 调用响应式封装:useIpcRendererInvoke 实战指南 导读 在 AIRI 项目中,Electron
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染Electron多进程数据同步:electron-store在主进程和渲染进程中的完美应用
Electron多进程数据同步:electron store在主进程和渲染进程中的完美应用 在Electron应用开发中, 数据同步 是一个常见且关键的挑战。如
桌面应用在 Electron 中使用 RxDB Electron 插件:主进程与渲染进程的 RxStorage IPC 桥接实战
在 Electron 中使用 RxDB Electron 插件:主进程与渲染进程的 RxStorage IPC 桥接实战 RxDB 的 Electron 插件(
数据库NoSQL嵌入式数据库实时数据库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考