IPC(Conveyor)

渲染进程与主进程之间类型化、经 Zod 校验的 IPC 层。

Conveyor 是本模板的类型化 IPC 系统。渲染进程从不直接接触 Node 或 ipcRenderer,而是调用 window.conveyor 上的类型化方法,每一次请求与响应都在 主进程侧经过 Zod 校验。没有任何代码生成步骤:TypeScript 直接从 schema 映射推断 出通道类型。

四个组成部分

所有内容都位于 lib/conveyor/ 下,再加上 lib/main/ 中的一个封装。

conveyor.d.ts(全局 window.conveyor 类型)
部分运行于职责
schemas/共享每个通道 argsreturn 的 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: truenodeIntegration: false (参见架构)。预加载桥接只通过 contextBridge 暴露 conveyor 对象——没有裸露的 Node、没有 ipcRenderer、没有临时通道。由于 每个载荷都是带明确边界的 Zod 元组,被攻破或存在缺陷的渲染进程也无法把意料之外的 形状偷渡过这条边界。

相关页面

On this page