我受够了 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。

相关推荐
lichenyang45324 分钟前
从 Demo 到本地 HAR 包:把 HarmonyOS JSBridge 运行时做成可复用库
前端
不简说32 分钟前
拖拽DIY表格插件来了!sv-print 七月更新:这次真的能自己拼表格了
前端·javascript·前端框架
ssshooter1 小时前
小程序分包页面报 "has not been registered yet" 的隐蔽根因:一个动态 import 拖垮整个分包
前端·javascript·微信小程序
鹏多多1 小时前
Web浏览器后台离线通知方案实现以及兼容指南
前端·javascript·vue.js
yangmu32031 小时前
Vue3 响应式系统深度解析:从 Proxy 到依赖追踪
前端·javascript·vue.js
还有多久拿退休金1 小时前
AI 写了代码,谁替你"试毒"?
前端·架构·ai编程
大E帝国子民11 小时前
OpenCloudOS Server 9 安装 Nginx 完整指南
前端·网络·nginx
少年白马醉春风丶1 小时前
【AI Agent面试题】Agent 的自主性分级,怎么理解?
前端
yingyima2 小时前
Docker Compose 紧急救援:凌晨3点服务器报警的解决方案
前端
石小石Orz2 小时前
星云SDK + 油猴:给LLM塑造肉身,陪伴你在每个网页
前端·人工智能