我受够了 Electron 的 IPC 样板代码,于是写了 electron-ipc-auto-import

写 Electron 的人多半都干过这件事:加一个再普通不过的功能------比如「点个按钮,让主进程读一下本地文件」------结果要改三个文件,写两遍同一个字符串,再手动补一份类型声明。然后某天手一抖把 'file:read' 敲成了 'file:raed',编译器一声不吭,运行时按钮点下去毫无反应,你对着控制台找了二十分钟。

这个库就是为了让上面这段话再也不会发生。

先说清楚疼在哪

原生 Electron 里加一个 IPC 调用,标准动作是这样的。

主进程注册:

ts 复制代码
// main.ts
ipcMain.handle('user:getById', async (_e, id: number) => {
  return db.users.find(id)
})

preload 里架桥:

ts 复制代码
// preload.ts
contextBridge.exposeInMainWorld('api', {
  user: {
    getById: (id: number) => ipcRenderer.invoke('user:getById', id),
  },
})

再手写一份类型,不然渲染层一片 any:

ts 复制代码
// global.d.ts
interface Window {
  api: { user: { getById(id: number): Promise<User> } }
}

渲染层终于能用了:

ts 复制代码
const u = await window.api.user.getById(1)

四个文件,'user:getById' 这个字符串出现两次而且必须一字不差,User 类型靠你自觉手动同步。功能越多,这套「三处对齐」的仪式就越折磨人。它没有任何技术含量,但每一处都能让你悄无声息地翻车。

它怎么解决:你只写一个函数

换成 electron-ipc-auto-import,上面那一坨变成一个文件:

ts 复制代码
// src/main/ipc/user.ts
export async function getById(id: number): Promise<User> {
  return db.users.find(id)
}

渲染层直接调:

ts 复制代码
const u = await window.ipc.user.getById(1)

没了。preload 不用碰,类型声明不用写,字符串一个都不用记。文件名 user.ts 自动成了命名空间,导出的函数名就是方法名。你把 getById 的参数从 number 改成 string,渲染层那行立刻飘红------因为类型是从你那个函数本人推导出来的,不是手抄的。

加第二个功能?再 export 一个函数。删功能?删掉函数。整个心智负担就是「写普通的 TypeScript 函数」。

背后没有黑魔法

我知道「自动」两个字容易让人警惕,所以讲清楚原理:全部发生在编译期

构建时,插件用 ts-morph 静态扫描你的 handler 目录,读出每个导出函数的签名和 JSDoc,然后生成两段代码------主进程那段把函数挨个绑到 ipcMain.handle,preload 那段把对应的 ipcRenderer.invoke 包好、contextBridge 暴露出去------再顺手生成一份 .d.ts 给渲染层。运行时没有反射、没有 eval、没有动态字符串拼 channel,渲染层能调的方法是构建时就钉死的那几个,多一个都调不到。

也就是说,它生成的正是你本来要手写的那些代码,只是不用你写了。出了问题,你能直接去看生成的虚拟模块长什么样,而不是对着一坨运行时魔法干瞪眼。

顺带一提,生成的 preload 在 sandbox: true 下也能正常跑,主进程抛出的错误会原样穿回渲染层------namemessagestack 还有你挂在 error 上的自定义字段都在,try/catch 抓到的是一个真正的 Error,而不是被 Electron 序列化得面目全非的字符串。

几个你大概会用得上的点

  • 不只服务于 Vite。 底层是 unplugin,Vite / webpack / Rspack / esbuild / Rollup 都有对应入口,配置长得几乎一样,换构建工具不用换思路。
  • 命名空间跟着目录走。 默认按文件名分组(ipc/user.tswindow.ipc.user.*);想按文件夹、或者干脆全平铺,一个 namespace 选项切换。
  • 留了拦截的口子。 想在所有调用前统一校验参数、记日志、做鉴权?registerIpcHandlers({ validate }) 一处搞定。错误怎么序列化也能自定义。
  • JSDoc 会跟着走。 你写在 handler 上的注释,会出现在渲染层的类型提示里,IDE 里 hover 一下就能看到。

三分钟上手

bash 复制代码
pnpm add -D electron-ipc-auto-import

构建配置里挂上插件(以 electron-vite 为例,三个 target 都加):

ts 复制代码
// electron.vite.config.ts
import electronIpc from 'electron-ipc-auto-import/vite'

const ipc = () =>
  electronIpc({
    dirs: ['src/main/ipc'],
    dts: 'src/preload/ipc-auto-import.d.ts',
  })

export default defineConfig({
  main: { plugins: [ipc()] },
  preload: { plugins: [ipc()] },
  renderer: { plugins: [ipc()] },
})

主进程开机时注册一次:

ts 复制代码
// src/main/index.ts
import { registerIpcHandlers } from 'virtual:electron-ipc/main'

app.whenReady().then(() => {
  registerIpcHandlers()
  createWindow()
})

preload 一行:

ts 复制代码
// src/preload/index.ts
import 'virtual:electron-ipc/preload'

然后开始往 src/main/ipc/ 里扔函数就行了。

它不打算做的事

为了不浪费你的时间,也把边界说在前面:这是个 请求-响应(invoke/handle) 的库,不是大一统 RPC 框架。主进程往渲染层主动推送(webContents.send 那种事件流)它不管,长连接、流式传输也不在范围内。它只想把「渲染层调一个主进程函数」这件最高频的事做到极致省心。

如果你的 80% IPC 都是「我问,主进程答」,那它基本能帮你把这部分代码量砍到只剩业务逻辑本身。


灵感来自 unplugin-auto-import------既然导入语句能自动,IPC 这种更机械的样板,凭什么还要手写。

仓库在 github.com/yyues/elect...,觉得有用的话点个 star,有想法欢迎来提 issue。

相关推荐
梦想的颜色1 小时前
TypeScript 完全指南(中):函数、接口、类与高级类型
前端·typescript
鹏多多1 小时前
OpenSpec+SDD规范驱动AI Agent开发项目实战指南
前端·vue.js·react.js
叶小树咯1 小时前
React 为什么不能像 Vue 那样 state.count++
前端·react.js
ricardo19731 小时前
防抖节流进阶 + requestAnimationFrame:滚动与输入场景的性能优化
前端·面试
wjj不想说话1 小时前
你项目里的 Pinia,可能已经成了第二个 localStorage
前端·vue.js
wuhen_n1 小时前
LangChain JS 入门:快速搭建前端 AI 开发环境
前端·langchain·ai编程
天蓝色的鱼鱼2 小时前
画1万个图形就卡成PPT?试试这款国产高性能2D引擎
前端·javascript
云水一下2 小时前
JavaScript 从零基础到精通系列:异步编程与网络请求
前端·javascript
卡卡军2 小时前
🌈 react-sketch-ruler v3 升级之旅:当 React 遇上跨框架标尺引擎
前端·react.js