IPC(Conveyor)
渲染进程与主进程之间类型化、经 Zod 校验的 IPC 层。
Conveyor 是本模板的类型化 IPC 系统。渲染进程从不直接接触 Node 或
ipcRenderer,而是调用 window.conveyor 上的类型化方法,每一次请求与响应都在
主进程侧经过 Zod 校验。没有任何代码生成步骤:TypeScript 直接从 schema 映射推断
出通道类型。
四个组成部分
所有内容都位于 lib/conveyor/ 下,再加上 lib/main/ 中的一个封装。
| 部分 | 运行于 | 职责 |
|---|---|---|
schemas/ | 共享 | 每个通道 args 与 return 的 Zod 元组,以及主进程 → 渲染进程的 eventSchemas。类型的唯一真实来源。 |
api/ | 预加载/渲染进程 | 调用 this.invoke('channel', ...args) 的轻量客户端类,组合为一个 conveyor 对象。 |
handlers/ | 主进程 | 真正的实现,通过 handle() 注册。 |
conveyor.d.ts | 渲染进程 | 声明 window.conveyor,使组件获得完整的智能提示。 |
目前接入的八个领域是 app、file、logs、notification、secure、shortcut、update 与 window:
// lib/conveyor/api/index.ts
export const conveyor = {
app: new AppApi(electronAPI),
file: new FileApi(electronAPI),
logs: new LogsApi(electronAPI),
notification: new NotificationApi(electronAPI),
secure: new SecureApi(electronAPI),
shortcut: new ShortcutApi(electronAPI),
update: new UpdateApi(electronAPI),
window: new WindowApi(electronAPI),
}在渲染进程中使用
在组件中优先使用 useConveyor 钩子——传入领域名即可只取该客户端:
import { useConveyor } from '@/app/hooks/use-conveyor'
function ThemeToggle() {
const { getSettings, setSettings } = useConveyor('app')
const enableDark = async () => {
await setSettings({ theme: 'dark' })
}
// ...
}在 React 之外,也可以在全局对象上调用同样的方法:
const version = await window.conveyor.app.version()
await window.conveyor.window.windowMinimize()事件(主进程 → 渲染进程)
请求/响应能覆盖大多数需求,但有些内容由主进程主动推送:app intent(深度链接、
托盘导航)与实时更新状态。它们使用 event-schema.ts 中的 eventSchemas,同样
会在监听器执行前完成校验。通过 API 客户端订阅,它会返回一个取消订阅函数:
const { onStatus } = useConveyor('update')
useEffect(() => {
const off = onStatus((status) => {
// status 是一个经过校验的 UpdateStatus 联合类型成员
})
return off
}, [onStatus])event-schema.ts 还保存着导航路由白名单(navigationRouteSchema)——深度
链接或托盘点击可跳转的有限路由集合。任何不在其中的路由都会在渲染进程看到之前
被拒绝。参见深度链接。
端到端添加一个通道
假设你想新增一个 file-read 通道,从 schema 开始向外扩展。
定义 schema
为参数和返回值各写一个 Zod 元组。
// lib/conveyor/schemas/file-schema.ts
import { z } from 'zod'
export const fileIpcSchema = {
'file-read': {
args: z.tuple([z.string()]),
return: z.string(),
},
}注册 schema
将它展开进中央的 ipcSchemas 映射,校验器与类型便会自动纳入。
// lib/conveyor/schemas/index.ts
import { fileIpcSchema } from './file-schema'
export const ipcSchemas = {
...windowIpcSchema,
...appIpcSchema,
...fileIpcSchema, // 在此添加
} as const添加 API 方法
扩展该领域的客户端类。invoke 完全依据 schema 进行类型推断。
// lib/conveyor/api/file-api.ts
import { ConveyorApi } from '@/lib/preload/shared'
export class FileApi extends ConveyorApi {
readFile = (path: string) => this.invoke('file-read', path)
}若这是一个全新领域,还需将它加入 api/index.ts 中的 conveyor 对象;
window.conveyor 的类型会自动更新。
实现 handler
使用 lib/main/shared.ts 中的 handle()。它会校验传入参数,运行你的函数,然后
校验返回值。
// lib/conveyor/handlers/file-handler.ts
import { readFile } from 'node:fs/promises'
import { handle } from '@/lib/main/shared'
export const registerFileHandlers = () => {
handle('file-read', (_event, path) => readFile(path, 'utf-8'))
}在主进程中注册
启动时在 lib/main/main.ts 里与其他 handler 一起调用注册函数。
import { registerFileHandlers } from '@/lib/conveyor/handlers/file-handler'
registerFileHandlers()调用
const contents = await window.conveyor.file.readFile('/path/to/file')校验在何处发生
handle() 是关键的收口点,双向都会检查:
// lib/main/shared.ts(节选)
ipcMain.handle(channel, async (event, ...args) => {
const validatedArgs = validateArgs(channel, args) // 渲染进程 → 主进程
const result = await handler(event, ...validatedArgs)
return validateReturn(channel, result) // 主进程 → 渲染进程
})若渲染进程发来的形状不对,validateArgs 会在你的 handler 运行前抛出;错误会带着
通道名被记录,被拒绝的 promise 会在渲染进程侧浮现。若 handler 返回的形状不对,
validateReturn 会抛出——这能在开发阶段就抓住契约漂移,而不是把畸形数据发给
UI。预加载客户端刻意跳过校验;主进程是唯一可信的校验方。
需要调用方窗口的 handler 使用 getRequestingWindow(event),当请求并非来自一个
存活的应用窗口时它会抛出。
安全姿态
切勿在应用代码中引入或暴露 ipcRenderer。请改为添加 schema、API 方法与
handler,让这条边界始终保持类型化并接受运行时校验。
渲染进程在沙箱中运行,contextIsolation: true 且 nodeIntegration: false
(参见架构)。预加载桥接只通过 contextBridge
暴露 conveyor 对象——没有裸露的 Node、没有 ipcRenderer、没有临时通道。由于
每个载荷都是带明确边界的 Zod 元组,被攻破或存在缺陷的渲染进程也无法把意料之外的
形状偷渡过这条边界。
