我受够了 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 下也能正常跑,主进程抛出的错误会原样穿回渲染层------name、message、stack 还有你挂在 error 上的自定义字段都在,try/catch 抓到的是一个真正的 Error,而不是被 Electron 序列化得面目全非的字符串。

几个你大概会用得上的点

  • 不只服务于 Vite。 底层是 unplugin,Vite / webpack / Rspack / esbuild / Rollup 都有对应入口,配置长得几乎一样,换构建工具不用换思路。
  • 命名空间跟着目录走。 默认按文件名分组(ipc/user.ts → window.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。

相关推荐
福兮说10 小时前
设计稿是 #4A7C6F,页面量出来是 #4B7C6F:HEX、HSL、透明度、canvas 来回转的七个坑
前端·javascript·css·canvas
郑州光合科技余经理10 小时前
海外版外卖加盟:总站与分站配送规则怎么分开管
java·开发语言·前端·后端·uni-app·php·ai编程
小呆呆66611 小时前
副业搞起来,小说,漫画,漫剧的成本优化思路
前端·后端·面试
Dovis(誓平步青云)11 小时前
浇水提醒刚弹出又消失,植物状态别只存一个百分比
开发语言·前端·javascript·pdf·ecmascript·电脑
码艺-Alimjan12 小时前
Vben Admin 新增维吾尔语 Vben-Modal的关键坑之一
前端·javascript·vue.js
可乐鸡翅yeah_12 小时前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
IT_陈寒13 小时前
Vite静态资源导入这个坑我帮你们踩过了
前端·人工智能·后端
广州华水科技13 小时前
大坝安全监测解决方案:单北斗GNSS形变监测系统应用与维护
前端
yivifu14 小时前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub
默_笙14 小时前
🛴 从散件到整机:DeepAgents 与 Agent 身上预留的那些"插槽"(前置介绍)
前端·javascript