首页
/ OpenScreen 原生桥(Native Bridge)架构解析:四层模型、版本化契约与 IPC 传输设计

OpenScreen 原生桥(Native Bridge)架构解析:四层模型、版本化契约与 IPC 传输设计

2026-09-05 14:19:35作者:咎岭娴Homer

本文基于 OpenScreen 仓库的架构文档 docs/architecture/native-bridge.md,系统讲解其 Native Bridge 的设计目标、四层架构分层、版本化契约(versioned contracts)与统一结果封装,并结合 src/native/contracts.tselectron/ipc/nativeBridge.ts 等源码说明各层的实际实现。读完后,你将能够理解 Electron 应用中如何为渲染进程提供一套“单一事实来源、能力可探测、错误可预期”的平台原生能力访问层,并掌握在其中新增 domain/action 的完整路径。

一、设计目标:薄传输、厚契约

文档开宗明义地给出了 Native Bridge 的目标:

Provide a single, resilient source of truth for platform-native capabilities while keeping Electron transport thin and renderer APIs unified. (在保持 Electron 传输层薄、渲染端 API 统一的前提下,为平台原生能力提供一个单一且坚韧的事实来源。)

拆开来看包含三个约束:

  1. 单一事实来源(Single source of truth):所有运行时原生状态(当前平台、当前项目路径、当前视频路径、光标遥测加载记录等)集中在 Electron 主进程,而不是散落在渲染进程的组件状态里;
  2. 传输层要薄(Thin transport):IPC 通道不承载业务逻辑,只负责把一个结构化的 NativeBridgeRequest 传进主进程、把结构化的 NativeBridgeResponse 传回来;
  3. 渲染端 API 统一(Unified renderer APIs):React 代码只依赖一个客户端(src/native/client.ts),不直接绑定零散的 Electron IPC 通道。

这套设计的收益在于:渲染进程永远不需要知道“光标数据到底来自 macOS 的 ScreenCaptureKit helper 还是 Windows 的 WGC 采集器”(参见 electron/native/screencapturekitelectron/native/wgc-capture 两个平台原生实现目录),它只面对统一的 cursor domain API。

二、四层架构:从原生适配器到渲染客户端

文档把整个桥接体系划分为四层,仓库中的目录结构与之一一对应。下面逐层展开,并给出每层的落地代码。

1. Native adapters(原生适配器层)

平台特定的 provider 实现稳定的领域接口,例如光标遥测或系统资产发现。

该层的核心是一份“能力接口”:electron/native-bridge/cursor/adapter.ts 定义了 CursorNativeAdapter 接口:

export interface CursorNativeAdapter {
	readonly kind: CursorProviderKind; // "native" | "none"
	getCapabilities(): Promise<CursorCapabilities>;
	getRecordingData(videoPath?: string | null): Promise<CursorRecordingData>;
	getTelemetry(videoPath?: string | null): Promise<CursorTelemetryLoadResult>;
}

关键点在于 kind 字段:适配器必须自报家门是 "native"(真正的平台原生实现)还是 "none"(回退实现)。当前仓库中注册的适配器是 electron/native-bridge/cursor/telemetryCursorAdapter.ts

export class TelemetryCursorAdapter implements CursorNativeAdapter {
	readonly kind = "none" as const;
	async getCapabilities(): Promise<CursorCapabilities> {
		return { telemetry: true, systemAssets: false, provider: this.kind };
	}
	// getRecordingData / getTelemetry 内部先 resolveVideoPath,
	// 解析不到视频路径时返回空数据,而不是抛错
}

它通过构造函数注入三个依赖(loadRecordingDataresolveVideoPathloadTelemetry),自身不含任何平台代码。这正是“适配器模式”的价值:上层服务只依赖 CursorNativeAdapter 接口,未来把 macOS/Windows 原生光标采集接进来时,只需新增一个 kind: "native" 的实现并在 electron/ipc/nativeBridge.ts 的装配处替换,服务层与渲染端零改动

此外,electron/native-bridge/cursor/recording/ 目录下的 factory.tswindowsNativeRecordingSession.tsmacNativeCursorRecordingSession.ts 等文件,就是各平台录制会话的具体实现载体,由工厂按平台选择。

2. Main-process services(主进程服务层)

服务编排适配器,持有运行时状态,并对外暴露领域级操作。

服务层位于 electron/native-bridge/services/,当前有三个:

服务 职责 关键行为
cursorService.ts 光标遥测与录制数据 从适配器取数据后,把“最近一次遥测加载”(视频路径 + 样本数 + 时间戳)写入状态存储
projectService.ts 项目/视频上下文 每个变更类操作(保存、加载、设置当前视频路径等)执行完都会调用 getCurrentContext() 刷新 store 中的项目上下文
systemService.ts 平台与能力信息 聚合平台、光标能力、项目能力,生成 SystemCapabilities 并缓存到 store

CursorService.getTelemetry 为例,可以看到服务层在“取数据”之外承担的两件事:

async getTelemetry(videoPath?: string | null): Promise<CursorTelemetryPoint[]> {
	const result = await this.options.adapter.getTelemetry(videoPath);
	if (!result.success) {
		throw new Error(result.message || result.error || "Failed to load cursor telemetry");
	}
	const resolvedVideoPath = videoPath ?? this.options.store.getState().project.currentVideoPath;
	if (resolvedVideoPath) {
		this.options.store.markCursorTelemetryLoaded(resolvedVideoPath, result.samples.length);
	}
	return result.samples;
}
  • 失败语义转换:适配器返回的软失败(success: false + 消息)被转换为异常,交由最外层统一封装成 INTERNAL_ERROR 错误响应;
  • 状态回写:成功加载后记录 lastTelemetryLoad,使主进程可以回答“当前遥测对应哪个视频、多少样本”。

3. Unified IPC transport(统一 IPC 传输层)

渲染代码只与单个 native-bridge:invoke 通道通信,使用版本化契约。

这是整个架构中最“薄”的一层,由两端各半段组成:

Preload 端electron/preload.ts 中只暴露了一个桥接方法:

contextBridge.exposeInMainWorld("electronAPI", {
	invokeNativeBridge: <TData>(request: NativeBridgeRequest) => {
		return ipcRenderer.invoke(NATIVE_BRIDGE_CHANNEL, request) as Promise<TData>;
	},
	// ... 其余为向后兼容的 legacy 方法
});

主进程端electron/ipc/nativeBridge.tsregisterNativeBridgeHandlers 是唯一注册点。它做了三件体现“坚韧性”的事:

  1. 幂等注册:入口处先 ipcMain.removeHandler(NATIVE_BRIDGE_CHANNEL)ipcMain.handle,重复调用不会因通道已存在而崩溃;
  2. 入参校验isBridgeRequest 先确认请求是带 domainaction 字符串的对象,否则直接返回 INVALID_REQUEST 错误封装;
  3. 路由 + 兜底:按 domainsystem / project / cursor)二级 switch 分发到对应服务,未识别的 domain 或 action 返回 UNSUPPORTED_ACTION,任何未捕获异常统一转换为带 retryable: trueINTERNAL_ERROR
ipcMain.handle(NATIVE_BRIDGE_CHANNEL, async (_, request: unknown) => {
	if (!isBridgeRequest(request)) {
		return createErrorResponse(undefined, "INVALID_REQUEST", "Invalid native bridge request.");
	}
	// ... domain/action 二级路由 ...
} catch (error) {
	return createErrorResponse(requestId, "INTERNAL_ERROR",
		error instanceof Error ? error.message : "Unknown native bridge error.", true);
});

注意 NativeBridgeContext 接口(electron/ipc/nativeBridge.ts#L19-L40):处理器并不直接依赖 Electron 的具体实现,而是依赖一组注入的回调(getPlatformsaveProjectFileloadCursorRecordingData 等)。这种依赖注入使路由逻辑与主进程的窗口管理、文件 I/O 解耦,也更便于测试。

4. Renderer client(渲染端客户端层)

React 代码应当消费 src/native/client.ts,而不是直接绑定临时的 Electron API。

src/native/client.ts 对外导出 nativeBridgeClient,按 domain 组织了三个命名空间:systemprojectcursor,外加一个原始入口 rawInvoke

nativeBridgeClient.cursor.getRecordingData?.(videoPath)   // → CursorRecordingData
nativeBridgeClient.system.getCapabilities()               // → SystemCapabilities
nativeBridgeClient.project.saveProjectFile(data, name)   // → ProjectFileResult

两个细节值得注意:

  • 请求追踪invokeNativeBridge 会在请求未带 requestId 时自动生成一个(优先 crypto.randomUUID(),回退为 req-<时间戳>-<随机数>,见 src/native/client.ts#L15-L21),主进程在响应的 meta.requestId 中原样带回,可用于日志串联;
  • 两种消费姿势invokeNativeBridge 返回完整的结果封装(ok 判别联合),requireNativeBridgeData 则在 ok: false 时直接抛出 Error(response.error.message)。命名空间方法统一采用后者,让调用方拿到“已保证成功”的数据类型;而需要区分错误码的场景(如判断是否可重试)则使用 rawInvoke 拿原始封装。

客户端对 window.electronAPI.invokeNativeBridge 缺失时会抛出明确的 Native bridge unavailable 错误(src/native/client.ts#L23-L31),提示开发者 preload 未正确暴露传输,属于快速失败(fail fast)设计。

三、版本化契约:Channel、Version 与 Domain/Action 路由

契约文件 src/native/contracts.ts 被主进程与渲染进程共同引用,这是“契约版本化”能成立的前提——两端看到的是同一份 TypeScript 类型:

export const NATIVE_BRIDGE_CHANNEL = "native-bridge:invoke";
export const NATIVE_BRIDGE_VERSION = 1;

请求形态:domain + action + payload

NativeBridgeRequest 是一个判别联合(discriminated union),当前收录了三个 domain 共 13 个 action:

domain action 说明 载荷
system getPlatform 归一化后的平台(darwin/win32/linux
system getAssetBasePath 渲染端资源基路径
system getCapabilities 能力总览(含桥版本、平台、各域能力)
project getCurrentContext 当前项目路径 + 当前视频路径
project saveProjectFile 保存工程文件 projectDatasuggestedName?existingProjectPath?
project loadProjectFile 打开工程文件(可预填目录) projectFolder?
project loadCurrentProjectFile 加载当前工程
project loadProjectFileFromPath 按路径加载 path
project setCurrentVideoPath 设置当前视频 path
project getCurrentVideoPath 查询当前视频路径
project clearCurrentVideoPath 清除当前视频路径
cursor getCapabilities 光标能力
cursor getTelemetry 光标遥测点序列 videoPath?
cursor getRecordingData 完整光标录制数据(样本 + 资产) videoPath?

主进程对平台做了归一化处理(electron/ipc/nativeBridge.ts#L42-L48):只有 darwinwin32 原样保留,其余 Node 平台一律折叠为 linux,保证契约枚举封闭。

结果封装:Envelope 与稳定错误码

文档原则中的“每个响应使用一致的结果封装与稳定错误码”,对应契约中的这组类型:

export type NativeBridgeErrorCode =
	| "INVALID_REQUEST"      // 入参不是合法的桥请求
	| "UNSUPPORTED_ACTION"   // domain 存在但 action 未实现
	| "NOT_FOUND"
	| "UNAVAILABLE"
	| "INTERNAL_ERROR";      // 服务端异常,且 retryable: true

export interface NativeBridgeMeta {
	version: typeof NATIVE_BRIDGE_VERSION;
	requestId: string;
	timestampMs: number;
}

export type NativeBridgeResponse<TData = unknown> =
	| { ok: true;  data: TData; meta: NativeBridgeMeta }
	| { ok: false; error: { code: NativeBridgeErrorCode; message: string; retryable: boolean }; meta: NativeBridgeMeta };

这个封装带来三个工程收益:

  1. 错误可分类:渲染端可以依据 code 做差异化处理(例如 UNAVAILABLE 时切换 UI 降级方案,retryable 为 true 时重试),而不必解析错误字符串;
  2. 版本可协商meta.versionNATIVE_BRIDGE_VERSION 一同下发,未来契约升级时可做新旧版本的兼容性判断;
  3. 可观测性requestId 贯穿请求-响应两端,便于在主进程日志与渲染端表现之间建立因果。

此外契约还定义了主进程 → 渲染进程的事件名project.contextChangedcursor.providerChangedcursor.telemetryLoaded,见 src/native/contracts.ts#L230-L239),为“推送式”状态变更预留了通道,与“拉取式”的 invoke 请求互补。

四、状态存储:主进程内的 Single Source of Truth

文档第一条原则——“运行时原生状态活在 Electron 主进程”——由 electron/native-bridge/store.ts 中的 NativeBridgeStateStore 落实。它的状态结构按三个 domain 分片:

export interface NativeBridgeState {
	system: {
		platform: NativePlatform;
		capabilities: SystemCapabilities | null;
	};
	project: ProjectContext;            // currentProjectPath / currentVideoPath
	cursor: {
		capabilities: CursorCapabilities | null;
		lastTelemetryLoad: {
			videoPath: string;
			sampleCount: number;
			loadedAt: number;
		} | null;
	};
}

实现上有两个特点:

  • 不可变更新:所有 setter(setProjectContextsetSystemCapabilitiessetCursorCapabilitiesmarkCursorTelemetryLoaded)都通过展开旧对象创建新状态,避免服务层意外持有可变引用;
  • 共享单例:在 registerNativeBridgeHandlers 中,同一个 store 实例被注入三个服务,因此 project 域的上下文刷新(ProjectService 每次变更操作后调用 getCurrentContext())能立即被 cursor 域感知——TelemetryCursorAdapterresolveVideoPath 正是靠这个共享状态在调用方未显式传 videoPath 时兜底解析出当前视频。

从源码结构看,store 目前尚未订阅任何持久化事件,它是进程内易失状态;跨会话记忆(如上次打开的工程目录)由渲染端偏好存储体系负责,两者职责分离。

五、Capability-first:先探测,再行动

第二条原则“能力优先”在契约层面体现为三级能力查询链:

  1. cursor.getCapabilities 返回 CursorCapabilitiestelemetry 是否支持、systemAssets 是否提供系统光标资产、当前 providernative 还是 none);
  2. system.getCapabilities 将其聚合进 SystemCapabilities,连同 bridgeVersionplatformproject.currentContext 一起下发(systemService.ts#L27-L42);
  3. 渲染端组件据此决定 UI:例如当 provider"none" 时,编辑器不应承诺“逐像素还原系统光标”,而是使用遥测点位 + 自绘光标渲染。

当前 TelemetryCursorAdapter 声明的是 { telemetry: true, systemAssets: false, provider: "none" },意味着本仓库现阶段提供的是遥测级光标能力;而 NativeCursorAsset(含 imageDataUrlhotspotX/YscaleFactorcursorType 等字段)契约已经就绪,是为 macOS/Windows 原生光标资产(provider: "native")预留的数据结构。

六、当前落地范围与 Legacy 兼容策略

文档“Current rollout”一节列出的初始脚手架,在仓库中均可验证:

文档列出的组件 仓库中的对应文件
共享契约 src/native/contracts.ts
渲染端 SDK src/native/client.ts
主进程状态存储 electron/native-bridge/store.ts
光标遥测适配器 electron/native-bridge/cursor/telemetryCursorAdapter.ts
领域服务 electron/native-bridge/services/(cursor / project / system 三个服务)
统一处理器注册 electron/ipc/nativeBridge.ts

文档同时明确指出:legacy 的 window.electronAPI 表面仍然存在以兼容旧代码,新特性应优先使用统一桥接客户端。这一点在 electron/preload.ts 中可以直观印证——invokeNativeBridge 与大量既有方法(getCursorTelemetrysaveProjectFilegetPlatform 等零散通道)并存于同一个 electronAPI 对象上。因此实际维护时的迁移纪律是:

  • 新代码:只 import src/native/client.tsnativeBridgeClient,不新增对 ipcRenderer 直接绑定的 preload 方法;
  • 旧代码:逐步替换为桥接调用,替换一个删一个 legacy 通道,最终让 electronAPI 收敛为只剩 invokeNativeBridge(与 assetBaseUrl 这类非领域性暴露)的极简表面。

七、设计原则小结与扩展路径

回到文档的四条原则,它们分别落到了具体机制上:

原则 实现机制
Single source of truth NativeBridgeStateStore 集中持有系统/项目/光标状态,服务层每次变更后回写
Capability-first cursor / system 两级 getCapabilities,渲染端先探测再行动
Versioned contracts 单一契约文件被两端共享;NATIVE_BRIDGE_VERSION + meta.version 随响应下发;请求为判别联合,扩展新 action 时编译期即可暴露两端遗漏
Resilience 统一 NativeBridgeResponse 封装、五个稳定错误码、入参校验、removeHandler 幂等注册、异常兜底为 retryableINTERNAL_ERROR

基于这套结构,向桥中新增一个 domain(例如 webcam)的路径是明确的:

  1. src/native/contracts.tsNativeBridgeRequest 联合中追加该 domain 的 action 分支,并补充对应响应数据类型与错误码(必要时);
  2. electron/native-bridge/services/ 新增服务类,构造函数注入 store 与具体依赖;
  3. electron/ipc/nativeBridge.ts 的装配函数中实例化服务,并在 domain switch 中新增一个 case 分支(未知 action 会自动落入 UNSUPPORTED_ACTION);
  4. src/native/client.tsnativeBridgeClient 上新增对应命名空间方法,渲染端只通过该命名空间访问。

整个过程中,IPC 通道数量保持为 1,preload 保持零改动——这正是“薄传输、厚契约”架构的最终体现:扩展性来自契约的类型系统,而不是新增传输面

登录后查看全文
热门项目推荐
相关项目推荐