- IDE
- 代码编辑器
- 开发工具
- 前端
- 桌面应用
- 插件系统
- 后端
- AI 应用
【免费下载链接】theia
Eclipse Theia is a cloud & desktop IDE framework implemented in TypeScript.
@theia/api-provider-sample(位于 examples/api-provider-sample)是 Eclipse Theia 官方仓库中的一个编程示例扩展,用于演示如何为插件(plugin)定义并提供自定义的 API 对象。它实现了一个名为gotd(Greeting of the Day,今日问候语)的插件 API:插件可以在激活函数中导入该 API,创建"问候器"(Greeter)并获取风格各异的问候消息。阅读本文后,你将掌握 Theia API Provider 扩展的完整骨架——从gotd.d.ts类型声明、RPC 接口定义,到主进程侧实现与插件进程侧实现的 RPC 打通,以及如何通过 Inversify 依赖注入完成注册,并最终写一个真正消费该 API 的插件。
一、扩展概览:它解决什么问题
Theia 的插件体系(plugin system)让插件运行在独立的plugin-host Node 进程中,与主 Theia 进程隔离(相关架构说明见 doc/Plugin-API.md)。插件进程与主进程之间通过RPC(远程过程调用)通信,而vscode这样的 API 对象正是由@theia/plugin-ext以"API Provider"的形式注入给插件的。
@theia/api-provider-sample的核心目标是(见 README):
- 为开发者提供定义自定义 API 对象的真实可运行的编码范例;
- 为审查 Pull Request 时提供易用、易测的功能示例。
该扩展仅作参考与测试用途,package.json中声明了"private": true,因此不会发布到 npm(见 package.json)。扩展名为@theia/api-provider-sample,版本与仓库一致(当前为1.75.0),并声明了theiaExtensions的backend入口指向编译后的lib/node/gotd-backend-module:
{ "private": true, "name": "@theia/api-provider-sample", "version": "1.75.0", "description": "Theia - Example code to demonstrate Theia API Provider Extensions", "dependencies": { "@theia/core": "1.75.0", "@theia/plugin-ext": "1.75.0", "@theia/plugin-ext-headless": "1.75.0" }, "theiaExtensions": [ { "backend": "lib/node/gotd-backend-module" } ], "types": "src/gotd.d.ts" }依赖的三个包各有分工:@theia/core提供事件(Event/Emitter)、Disposable等基础设施;@theia/plugin-ext提供 RPC 协议(RPCProtocol、createProxyIdentifier)与MainPluginApiProvider等主进程侧接口;@theia/plugin-ext-headless提供ExtPluginApiProvider接口——它使得同一个自定义 API 既能被普通后端插件消费,也能被 headless 插件消费。这正是本示例同时依赖后两者的原因。
二、源码布局:三层目录结构
扩展的源码全部位于src/目录下,共四个部分(见 README):
examples/api-provider-sample/src/ ├── gotd.d.ts # gotd API 的 TypeScript 类型声明(插件侧导入) ├── common/ │ └── plugin-api-rpc.ts # RPC 的 Ext/Main 两侧接口定义 ├── node/ │ ├── gotd-backend-module.ts # Inversify 容器模块(注册 API Provider) │ ├── ext-plugin-gotd-api-provider.ts # ExtPluginApiProvider 实现 │ ├── gotd-main-plugin-provider.ts # MainPluginApiProvider 实现 │ └── greeting-main-impl.ts # GreetingMain 接口的主进程实现 └── plugin/ ├── gotd-api-init.ts # 插件 API 初始化脚本 └── greeting-ext-impl.ts # GreetingExt 接口的插件进程实现各目录的运行环境与职责:
gotd.d.ts—gotdAPI 对象的 TypeScript 定义,插件通过导入它来与"今日问候语"服务交互。package.json的"types": "src/gotd.d.ts"让插件可以直接import * as gotd from '@theia/api-provider-sample'获得类型提示。plugin/— API 初始化脚本与 API 对象的实现(GreetingExt等接口的实现类)。该目录下所有代码只运行在独立的 plugin-host Node 进程中,与主 Theia 进程隔离;它服务的对象既包括 headless 插件,也包括 VS Code 插件的后端部分。GreetingExtImpl等类通过RPC与主进程中实际的 API 实现(GreetingMainImpl等)通信。node/— 实现GreetingMain等接口的 API 类,以及注册 API Provider 的 Inversify 绑定。该目录下所有代码运行在主 Theia Node 进程中。common/— 与gotd插件 API 后端对应的RPC Ext/Main 接口定义,同时被两侧进程引用。
三、插件侧 API 类型定义(gotd.d.ts)
gotd.d.ts是插件开发者唯一需要接触的"门面"(完整源码):
export namespace greeting { export function createGreeter(): Promise<greeting.Greeter>; export enum GreetingKind { DIRECT = 1, QUIRKY = 2, SNARKY = 3, } export interface Greeter extends Disposable { greetingKinds: readonly GreetingKind[]; getMessage(): Promise<string>; setGreetingKind(kind: GreetingKind, enable = true): void; onGreetingKindsChanged: Event<readonly GreetingKind[]>; } } export interface Event<T> { (listener: (e: T) => unknown, thisArg?: unknown): Disposable; } export interface Disposable { dispose(): void; }需要理解的关键点:
greeting命名空间:源码注释明确说明,"严格来说greeting命名空间是一个不必要的组织层级,但它正好用来演示后端是如何实现 API 命名空间的"。即:API Provider 可以在顶层导出对象(如Disposable),也可以提供嵌套命名空间(如greeting),插件进程侧会按相同结构组装返回。GreetingKind枚举:定义三种问候风格——DIRECT(直白)、QUIRKY(古怪)、SNARKY(尖刻)。注意它在gotd.d.ts与common/plugin-api-rpc.ts中各声明了一份,因为两侧进程各自需要这个枚举:插件侧用于构造 API 对象,主进程侧用于解释 RPC 消息中的数值。Greeter接口:一个"问候器",继承Disposable,暴露只读的greetingKinds列表、getMessage()获取随机问候语、setGreetingKind()启停某种问候风格(第二参数enable默认为true),以及onGreetingKindsChanged事件用于监听风格变化。Event<T>的签名与 Theia 核心的事件模型一致(订阅返回Disposable)。
四、RPC 接口定义(common/plugin-api-rpc.ts)
common/plugin-api-rpc.ts(完整源码)是两侧进程共享的 RPC 协议契约,定义了"谁调谁"的接口与方法名规范。
主进程侧接口GreetingMain(带$前缀的方法表示由插件进程 RPC 调用主进程):
export const GreetingMain = Symbol('GreetingMain'); export interface GreetingMain { $getMessage(greeterId: string): Promise<string>; $createGreeter(): Promise<GreeterData>; $destroyGreeter(greeterId: GreeterData['uuid']): Promise<void>; $updateGreeter(data: GreeterData): void; }插件进程侧接口GreetingExt分两部分(源码):
export const GreetingExt = Symbol('GreetingExt'); export interface GreetingExt { // External protocol:供 API 工厂实现调用的"对外"方法 registerGreeter(): Promise<string>; unregisterGreeter(uuid: string): Promise<void>; getMessage(greeterId: string): Promise<string>; getGreetingKinds(greeterId: string): readonly greeting.GreetingKind[]; setGreetingKindEnabled(greeterId: string, greetingKind: greeting.GreetingKind, enable: boolean): void; onGreetingKindsChanged(greeterId: string): Event<readonly greeting.GreetingKind[]>; // Internal protocol:供主进程 RPC 回调插件进程 $greeterUpdated(data: GreeterData): void; }- External protocol:供 API 工厂实现(
GotdApiFactoryImpl)在本地调用; - Internal protocol:
$greeterUpdated带$前缀,表示它是由主进程 RPC 反向推送事件到插件进程的方法。
通信端点通过createProxyIdentifier(来自@theia/plugin-ext/lib/common/rpc-protocol)注册:
export const PLUGIN_RPC_CONTEXT = { GREETING_MAIN: createProxyIdentifier<GreetingMain>('GreetingMain'), }; export const MAIN_RPC_CONTEXT = { GREETING_EXT: createProxyIdentifier<GreetingExt>('GreetingExt'), };此外还定义了跨进程传输的数据结构GreeterData(uuid+greetingKinds数组),它是Greeter状态的序列化载体:主进程持有权威状态,插件进程持有其副本。
五、主进程侧:API 实现与注册(node/)
5.1 后端容器模块:一切的入口
gotd-backend-module.ts 是package.json中theiaExtensions.backend指向的模块,负责把三类绑定注册进主进程的 Inversify 容器:
import { ContainerModule } from '@theia/core/shared/inversify'; import { ExtPluginApiProvider } from '@theia/plugin-ext'; import { ExtPluginGotdApiProvider } from './ext-plugin-gotd-api-provider'; import { MainPluginApiProvider } from '@theia/plugin-ext/lib/common/plugin-ext-api-contribution'; import { GotdMainPluginApiProvider } from './gotd-main-plugin-provider'; import { GreetingMain } from '../common/plugin-api-rpc'; import { GreetingMainImpl } from './greeting-main-impl'; export default new ContainerModule(bind => { bind(Symbol.for(ExtPluginApiProvider)).to(ExtPluginGotdApiProvider).inSingletonScope(); bind(MainPluginApiProvider).to(GotdMainPluginApiProvider).inSingletonScope(); bind(GreetingMain).to(GreetingMainImpl).inSingletonScope(); });三个绑定的含义:
ExtPluginApiProvider(来自@theia/plugin-ext):告诉插件系统"本扩展提供了一个 API 注入点",并指出插件进程侧的初始化脚本路径;MainPluginApiProvider(来自@theia/plugin-ext/lib/common/plugin-ext-api-contribution):当 RPC 通道就绪时,把主进程实现对象注册到 RPC 上,供插件进程代理调用;GreetingMain:将接口符号绑定到具体实现GreetingMainImpl(单例)。
5.2 ExtPluginApiProvider:声明初始化脚本
ext-plugin-gotd-api-provider.ts 实现了ExtPluginApiProvider.provideApi(),返回 API 的初始化入口路径:
import * as path from 'path'; import { injectable } from '@theia/core/shared/inversify'; import { ExtPluginApi, ExtPluginApiProvider } from '@theia/plugin-ext-headless'; @injectable() export class ExtPluginGotdApiProvider implements ExtPluginApiProvider { provideApi(): ExtPluginApi { // 同时支持后端插件与 headless 插件,因此只需一个入口脚本; // 应用构建会把该脚本从源码 ../plugin/ 打包到 ../backend/ 目录, // 与其它所有插件 API Provider 的脚本放在一起。 const universalInitPath = path.join(__dirname, '../backend/gotd-api-init'); return { backendInitPath: universalInitPath, headlessInitPath: universalInitPath }; } }要点:backendInitPath与headlessInitPath指向同一个初始化脚本(这是示例特意展示的"通用入口"用法),说明同一份 API 初始化代码可以同时服务普通后端插件和 headless 插件。
5.3 MainPluginApiProvider:把实现挂到 RPC 上
gotd-main-plugin-provider.ts 在 RPC 初始化时调用rpc.set(...),把GreetingMainImpl单例注册为插件进程可代理调用的远端对象:
@injectable() export class GotdMainPluginApiProvider implements MainPluginApiProvider { @inject(GreetingMain) protected readonly greetingMain: GreetingMain; initialize(rpc: RPCProtocol): void { rpc.set(PLUGIN_RPC_CONTEXT.GREETING_MAIN, this.greetingMain); } }5.4 GreetingMainImpl:主进程的真正实现
greeting-main-impl.ts 持有问候语的"权威数据":GREETINGS常量表按三种GreetingKind各定义了三句问候语;构造函数通过rpc.getProxy(MAIN_RPC_CONTEXT.GREETING_EXT)拿到插件进程侧GreetingExt的代理,用于反向通知。
const GREETINGS = { [GreetingKind.DIRECT]: ['Hello, world!', "I'm here!", 'Good day!'], [GreetingKind.QUIRKY]: ['Howdy doody, world?', "What's crack-a-lackin'?", 'Wazzup werld?'], [GreetingKind.SNARKY]: ["Oh, it's you, world.", 'You again, world?!', 'Whatever.'], } as const;核心逻辑(源码):
$createGreeter():用generateUuid()(来自@theia/core/lib/common/uuid)生成唯一 ID,默认开启DIRECT风格,存入greeterData后返回;$destroyGreeter():按greeterId删除记录;$updateGreeter():更新主进程侧状态后,调用this.proxy.$greeterUpdated({ ...myData })把变化推回插件进程,从而触发插件侧的onGreetingKindsChanged事件;$getMessage():从当前启用的风格中随机选一种,再从该风格的问候语表中随机选一句返回;如果没有任何启用的风格,则抛出No greetings are available for greeter ${greeterId}错误。
六、插件进程侧:API 工厂与实现(plugin/)
6.1 初始化脚本 gotd-api-init.ts
gotd-api-init.ts 是插件进程侧的核心,它负责在插件模块导入自定义 API 时创建并返回该 API 对象。它导出一个containerModule(Theia 会用它来配置插件进程的 Inversify 容器,并在 plugin-host 进程 fork 时调用):
export const containerModule = PluginContainerModule.create(({ bind, bindApiFactory }) => { bind(GreetingExt).to(GreetingExtImpl).inSingletonScope(); bindApiFactory('@theia/api-provider-sample', GotdApiFactory, GotdApiFactoryImpl); });PluginContainerModule.create与bindApiFactory来自@theia/plugin-ext/lib/plugin/node/plugin-container-module。bindApiFactory('@theia/api-provider-sample', ...)表示:当某个插件require('@theia/api-provider-sample')时,插件系统会调用GotdApiFactoryImpl.createApi(plugin)来构造 API 对象。
工厂实现类(源码):
@injectable() class GotdApiFactoryImpl { @inject(RPCProtocol) protected readonly rpc: RPCProtocol; @inject(GreetingExt) protected readonly greetingExt: GreetingExt; @postConstruct() initialize(): void { this.rpc.set(MAIN_RPC_CONTEXT.GREETING_EXT, this.greetingExt); } createApi(plugin: Plugin): Gotd { const self = this; async function createGreeter(): Promise<gotd.greeting.Greeter> { const toDispose = new DisposableCollection(); const uuid = await self.greetingExt.registerGreeter(); toDispose.push(Disposable.create(() => self.greetingExt.unregisterGreeter(uuid))); const onGreetingKindsChanged = self.greetingExt.onGreetingKindsChanged(uuid); const result: gotd.greeting.Greeter = { get greetingKinds(): readonly GreetingKind[] { return self.greetingExt.getGreetingKinds(uuid); }, setGreetingKind(greetingKind: GreetingKind, enable = true): void { self.greetingExt.setGreetingKindEnabled(uuid, greetingKind, enable); }, getMessage(): Promise<string> { return self.greetingExt.getMessage(uuid); }, onGreetingKindsChanged, dispose: toDispose.dispose.bind(toDispose), }; return result; } const greeting: Gotd['greeting'] = { createGreeter, GreetingKind }; return { greeting, Disposable, }; }; }值得注意的设计细节:
@postConstruct initialize():在工厂初始化时把GreetingExtImpl注册到 RPC 上(rpc.set(MAIN_RPC_CONTEXT.GREETING_EXT, ...)),这样主进程的GreetingMainImpl才能反向调用插件进程;createApi(plugin: Plugin)接收当前插件对象作为参数,返回类型为Gotd(即typeof gotd),结构与gotd.d.ts中的命名空间一一对应:顶层导出Disposable与greeting命名空间,greeting内导出createGreeter与GreetingKind;createGreeter()的每个Greeter都通过DisposableCollection管理生命周期:dispose()时会自动调用unregisterGreeter(uuid),确保插件停用时主进程侧的记录也被清理;greetingKinds用 getter 实时从GreetingExt读取,保证拿到的是最新状态。
6.2 GreetingExtImpl:插件侧的 RPC 客户端
greeting-ext-impl.ts 实现GreetingExt接口。构造函数通过rpc.getProxy(PLUGIN_RPC_CONTEXT.GREETING_MAIN)获得主进程GreetingMain的代理,此后本地调用即被序列化为 RPC 消息发往主进程。
各方法行为(源码):
registerGreeter():调用主进程$createGreeter()拿到GreeterData,在本进程greeterData表中登记,并为其创建一个Emitter(用于onGreetingKindsChanged事件),最后返回uuid;unregisterGreeter():删除本地记录并调用主进程$destroyGreeter(uuid);setGreetingKindEnabled():先在本地判断"是否已经处于目标状态"(data.greetingKinds.includes(greetingKind) === enable),若无需变化则直接返回;否则增删元素并通过$updateGreeter()同步给主进程;getMessage():直接转发给主进程$getMessage();$greeterUpdated():被主进程 RPC 回调,更新本地greetingKinds副本并fire事件,从而通知插件侧所有订阅者。
这个类揭示了事件的双向传播路径:插件本地修改 →$updateGreeter→ 主进程更新并$greeterUpdated回推 → 插件进程Emitter.fire→ 插件订阅回调执行。
七、调用链全景:一次完整的问候流程
综合以上代码,可以梳理出gotdAPI 从创建到使用的完整调用链(从源码结构推断):
- Theia 启动,加载
@theia/api-provider-sample扩展,执行 gotd-backend-module.ts,注册ExtPluginGotdApiProvider、GotdMainPluginApiProvider与GreetingMainImpl; - plugin-host 进程 fork 时加载 gotd-api-init.ts 的
containerModule,绑定GreetingExtImpl与GotdApiFactoryImpl,并在@postConstruct中把GreetingExtImpl注册到 RPC; - 主进程 RPC 通道建立后,
GotdMainPluginApiProvider.initialize(rpc)把GreetingMainImpl注册为远端对象,两侧代理就绪; - 插件激活时执行
gotd.greeting.createGreeter():- 插件进程
GreetingExtImpl.registerGreeter()→ RPC → 主进程$createGreeter()生成uuid并返回; - 工厂用返回的
uuid组装出Greeter对象交给插件;
- 插件进程
- 插件调用
greeter.getMessage():插件进程转发 → 主进程$getMessage()随机选取问候语 → 结果经 RPC 返回插件进程并 resolve; - 插件调用
greeter.setGreetingKind(QUIRKY):本地更新 →$updateGreeter→ 主进程更新状态并$greeterUpdated回推 → 插件进程fire事件 → 插件订阅回调收到onGreetingKindsChanged。
八、配套示例插件:plugin-gotd
仓库中提供了真实的消费端示例 sample-plugins/sample-namespace/plugin-gotd。该插件的package.json(package.json)把@theia/api-provider-sample作为devDependencies引入,并声明了theiaPlugin.headless指向headless入口,也就是说它同时具有 VS Code 插件后端与 headless 两种激活路径:
{ "name": "plugin-gotd", "engines": { "vscode": "^1.125.0" }, "main": "extension", "activationEvents": ["*"], "devDependencies": { "@theia/api-provider-sample": "1.75.0", "@types/vscode": "^1.125.0" }, "theiaPlugin": { "headless": "headless" }, "headless": { "activationEvents": ["*"], "contributes": {} } }headless 入口 headless.js 展示了插件侧完整用法:
const gotd = require('@theia/api-provider-sample'); const GreetingKind = gotd.greeting.GreetingKind; async function greet(greeter) { const message = await greeter.getMessage(); console.log('[GOTD]', message); } async function activate() { const greeter = await gotd.greeting.createGreeter(); greeter.onGreetingKindsChanged(kinds => { console.log('[GOTD]', `Now supporting these kinds of greeting: ${greetingKindsToString(kinds)}.`); if (kinds.length > 0) { greet(greeter); } }); greet(greeter); later(() => greeter.setGreetingKind(GreetingKind.DIRECT, false)); later(() => greeter.setGreetingKind(GreetingKind.QUIRKY)); later(() => greeter.setGreetingKind(GreetingKind.SNARKY)); } module.exports = { activate, deactivate: /* 逐个 dispose 所有 greeter */ };这段代码演示了本 API 的全部能力:createGreeter()创建问候器 →getMessage()立即取一条DIRECT问候语 → 随后依次关闭DIRECT、开启QUIRKY、再开启SNARKY,每次变更都会通过onGreetingKindsChanged事件触发新的问候输出;deactivate时通过Disposable.dispose()统一清理,保证插件卸载后主进程状态同步清除。另一个入口 extension.js 则是标准的 VS Code 后端插件,在activate中打印运行环境信息,验证自定义 API 与 VS Code API 可以共存于同一插件。
九、如何构建、运行与验证
由于该扩展是 Theia 仓库的 monorepo 成员,构建与运行都遵循 Theia 的标准流程(参考 doc/Developing.md):
- 安装依赖:在仓库根目录执行
yarn(Theia 使用 yarn workspaces 管理多包仓库,根目录 package.json 定义了各工作区与脚本); - 构建扩展:在
examples/api-provider-sample目录下执行yarn build,该命令会调用theiaext build(@theia/ext-scripts),把src/下的 TypeScript 编译到lib/。注意构建产物路径与源码路径不同:插件初始化脚本从src/plugin/被打包到lib/backend/(这正是 ext-plugin-gotd-api-provider.ts 里path.join(__dirname, '../backend/gotd-api-init')的由来); - 运行:该示例作为 Theia 扩展,被
examples/browser、examples/electron等应用工程通过theiaExtensions机制加载;同时配合plugin-gotd示例插件,即可在应用启动后于控制台观察到[GOTD]前缀的问候日志(见 sample-plugins/sample-namespace/plugin-gotd); - 作为模板复用:如果你要为自己的产品定义自定义 API,只需照此模式替换四样东西——
gotd.d.ts中的 API 类型、common/plugin-api-rpc.ts中的 RPC 接口、node/下的主进程实现与绑定、plugin/下的工厂与插件进程实现,并在package.json的theiaExtensions.backend指向你的后端模块即可。
十、小结与扩展阅读
@theia/api-provider-sample用不到十个文件、一个命名空间级别的 API,完整演示了 Theia 自定义插件 API 的全部机制:类型声明(gotd.d.ts)→ RPC 契约(common/)→ 主进程实现与注册(node/)→ 插件进程工厂与实现(plugin/)→ 消费端示例(plugin-gotd)。其中ExtPluginApiProvider(来自@theia/plugin-ext-headless)与MainPluginApiProvider(来自@theia/plugin-ext)分别打通了"插件进程如何找到 API 初始化脚本"与"主进程如何暴露实现"两个方向,RPCProtocol/createProxyIdentifier则负责把接口符号映射为可跨进程调用的代理。
进一步深入可参考仓库内相关资源:
- doc/Plugin-API.md:Theia 插件 API 总体架构说明;
- packages/plugin-ext 与 packages/plugin-ext-headless:
RPCProtocol、ExtPluginApiProvider、PluginContainerModule等机制的实际实现; - examples/api-provider-sample/src/gotd.d.ts 与 examples/api-provider-sample/src/common/plugin-api-rpc.ts:本示例的类型与 RPC 契约源码;
- sample-plugins/sample-namespace/plugin-gotd:可直接运行消费该 API 的示例插件。
该扩展遵循 Eclipse Public License 2.0 与带 Classpath Exception 的 GPL-2.0 双许可(见 README),"Theia" 是 Eclipse Foundation 的商标。
- IDE
- 代码编辑器
- 开发工具
- 前端
- 桌面应用
- 插件系统
- 后端
- AI 应用
【免费下载链接】theia
Eclipse Theia is a cloud & desktop IDE framework implemented in TypeScript.
相关推荐
Eclipse Theia 示例插件(Sample Plugins)完全指南:编写、打包与在 Theia 中验证 VS Code 扩展
Eclipse Theia 示例插件(Sample Plugins)完全指南:编写、打包与在 Theia 中验证 VS Code 扩展 sample plugi
IDE代码编辑器开发工具前端桌面应用插件系统后端AI 应用中文医疗大模型全景实战指南:基座选型、医学数据构建与微调算力解析(Awesome-Chinese-LLM 医学篇)
中文医疗大模型全景实战指南:基座选型、医学数据构建与微调算力解析(Awesome Chinese LLM 医学篇) 本文以开源仓库 Awesome Chines
IDE代码编辑器开发工具前端桌面应用插件系统后端AI 应用Eclipse Theia API Samples 扩展指南:用内部 API 与依赖注入编写真实可测的示例代码
Eclipse Theia API Samples 扩展指南:用内部 API 与依赖注入编写真实可测的示例代码 @theia/api samples 是 Ecl
IDE代码编辑器开发工具前端桌面应用插件系统后端AI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考