- Theia 插件零碎性能十分弱小,这么大的工程,仍然能放弃高质量的代码和清晰的架构,值得思考和学习
- VSCode Extension API 和配置的定义标准且形象
Eclipse Theia 是一个可扩大的平台,能够利用最先进的 Web 技术开发多语言的 Cloud & Desktop IDE。
名词解释
- Theia:可拓展的 Cloud & Desktop IDE 平台。
- Theia Extension:Theia 是由一系列 Extension 组成,Extension 提供了解决 widgets, commands, handlers 等的能力,在 编译时加载。
- Theia Plugin:概念上相似于 VSCode Extension,由 Theia 的其中一个 Extension:theia/packages/plugin-ext 定义了 加载机制、运行环境及 API 等。兼容 VSCode Extension,性能更为弱小,运行时加载。
- VSCode Extension:VSCode 运行时加载的 extensions,基于 VSCode Extension API 实现,概念上相似于 Theia 的 Plugin,运行时加载。
VSCode Extension 能够看成是 Theia Plugin 的子集。
Theia Extension 与 Plugin 的界限:外围的、形象的、编译时加载的采纳 Extension;业务的、具体的、运行时加载的采纳 Plugin。
Theia Plugin 类型分为前端和后端(VSCode 只有后端),其中后端运行在独立的插件过程;前端运行在 Web Worker 上,通过 postMessage 和 Browser 主过程通信。这里的 Web Worker 有点像微信小程序架构外面的 App Service。
概述
拓展 Theia Plugin 能力,让业务方简略、灵便地深度定制 IDE 的性能和界面。
动机
Theia Plugin 拓展形式和 能力 和 VSCode Extension 相似,并不满足咱们的需要:
- UI 定制能力十分单薄:主界面仅提供了大量的按钮与菜单反对自定义。但很多理论场景都有十分强烈的 UI 需要以满足不同的业务能力。如:例如 Taro IDE 主界面须要大量的按钮菜单注入以及模拟器、调试器等预览面板。
- 配置化的 UI 定制形式无奈满足定制需要:Theia Plugin 基于 Phosphor.js 实现布局零碎,将定制能力限定在了 配置化 这一层,随着 IDE Core 不同场景的业务方越来越多,容易造成「配置天堂」,因而在保留配置化的同时,最好提供布局相干 API,让业务方应用 JSX 自定义布局。(参考开天)
- 与外部业务、场景对接:如 ERP 登录认证、gitlab 仓库对接、团队合作与工作空间、监控 / 经营系统集成等。(参考 开天 + Eclipse Che)
因而,须要拓展 Theia 的插件零碎。
准则
- 屏蔽 IoC/ 布局零碎 /Widgt 等简单概念,让用户只须要领有 VS Code 插件开发教训就可能开发 Tide 插件。
- 尽可能复用 VS Code Extension 相干的设计和 API,尽可能参照 VS Code Extension API 现有的接口或标准进行拓展。
- 用户只须要领有 React 开发教训就能够定制布局零碎。
设计概览
设计总结
-
通过独立的 Extension 包拓展插件零碎。
- 参考 eclipse-che-theia-plugin-ext,提供 tide-theia-plugin-ext。
- 用户只须要加载 tide-theia-plugin-ext,就能够应用拓展的 API 及配置等。
-
提案参考 VS Code Extension 的拓展接口及标准,从配置、Command、VS Code API 等方面拓展插件零碎。
- 用户无额定学习老本,领有 VS Code 插件开发教训就可能开发 Tide 插件。
- 拓展 VS Code API,通过 tide namespace 裸露的办法和 interface 等。
- 拓展 package.json 配置,其中次要是 Contribution Points
- 拓展 Command,包含 built-in advanced commands api 和 keyboard shortcuts
- 如有须要,拓展 task。
-
绝对于 theia/vscode Namespace,提供 tide 的 Namespace 拜访 Tide API。
- 和 Theia Plugin 解耦
整体设计图示
tide 我的项目构造
IDE Core 和 Taro IDE 临时放在同一个我的项目 tide 里,倡议参考:che-theia。
./
├── configs
├── examples
│ ├── browser-app
│ └── electron-app
├── extensions
│ ├── tide-theia-about
│ ├── tide-theia-plugin // Tide API 接口标准定义
│ ├── tide-theia-plugin-ext // 插件零碎拓展实现
│ ├── tide-theia-user-preferences // 用户信息相干
│ ├── ...
└── plugins
├── dashboard-plugin
├── test-plugin
├── deploy-plugin
├── setting-plugin
└── ...
npm 包公布在 @tide
Scope 下。
VS Code Extension(概念上等同于 Theia 的 Plugin)能力是以下三种形式拓展:
具体设计
我的项目将参考 VS Code Extension 的拓展接口及标准,从配置、Command、VS Code API 等方面拓展插件零碎,其中,VSCode 最具代表性的拓展例子应该是 Tree View API,兼具以上三者形式。
Contribution Points 配置拓展
Contribution Points
是 package.json
中 contributes
字段的一系列 JSON 申明,插件通过注册 Contribution Points
来拓展 VSCode 的性能。
contributes
配置的解决能够分为配置扫描(scanner)和配置解决(handler)。次要在 plugin-ext 里实现。
scanner
plugin-ext/src/hosted/node/scanners/scanner-theia.ts 里的 TheiaPluginScanner
类实现了所有 package.json 配置的读取办法,包含 contribution 配置、activationEvents 配置等。
咱们应该是不须要增加新的配置读取,所以不须要批改这里。
handler
contribution 最终配置的 handle 都是在 PluginContributionHandler
里注入理论 handler 类兼顾解决的。
// packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts
export class PluginContributionHandler {@inject(MenusContributionPointHandler) // 注入 Menu 相干 handler
private readonly menusContributionHandler: MenusContributionPointHandler;
@inject(KeybindingsContributionPointHandler) // 注入 Keybindings 相干 handler
private readonly keybindingsContributionHandler: KeybindingsContributionPointHandler;
// ...
handleContributions(clientId: string, plugin: DeployedPlugin): Disposable {
// ...
pushContribution('commands', () => this.registerCommands(contributions));
pushContribution('menus', () => this.menusContributionHandler.handle(plugin));
pushContribution('keybindings', () => this.keybindingsContributionHandler.handle(contributions));
if (contributions.views) {for (const location in contributions.views) {for (const view of contributions.views[location]) {pushContribution(`views.${view.id}`,
() => this.viewRegistry.registerView(location, view) // 注册页面配置
);
}
}
}
// ...
}
registerCommandHandler(id: string, execute: CommandHandler['execute']): Disposable {}
registerCommand(command: Command): Disposable {}
// ...
}
拓展
和 API 拓展不同专门预留了拓展注入点 ExtPluginApiProvider
不同,Theia 代码里相似的并没有预留专门的接口,临时采纳以下步骤拓展:
- 定义
TidePluginContributionHandler
继承PluginContributionHandler
类 - 重写
handleContributions
办法 - 在 ContainerModule 里
rebind(TidePluginContributionHandler).to(PluginContributionHandler).inSingletonScope();
如果有更好的形式,请斧正。
Command 拓展
Commands 触发 Theia/VSCode
的 actions。VSCode 代码里蕴含大量 built-in commands,你能够应用这些命令与编辑器交互、管制用户界面或执行后盾操作。
Command 拓展能够参考:packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts
首先定义 XXXCommandsContribution
类实现 CommandContribution
,并注入对应的服务,如而后在 XXXCommandsContribution
中通过 commands.registerCommand
进行 Command 拓展,如:
// packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts
export class PluginVscodeCommandsContribution implements CommandContribution {@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
registerCommands(commands: CommandRegistry): void {commands.registerCommand({ id: 'openInTerminal'}, { // 注册命令
execute: (resource: URI) => this.terminalContribution.openInTerminal(new TheiaURI(resource.toString()))
});
}
}
而后 bind 到 container 即可:
bind(XXXCommandsContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(XXXCommandsContribution);
XXXCommandsContribution 会被注入到对应的 ContributionProvider,而后进行解决:
constructor(@inject(ContributionProvider) @named(CommandContribution)
protected readonly contributionProvider: ContributionProvider<CommandContribution>
) {}
Command 能够传入对象作为参数,无奈裸露接口和组件。
API 拓展
绝对下面两种拓展形式,API 的拓展形式比较复杂。
形式一:plugin-ext-vscode 的形式
这种形式是 VSCode 采纳的形式,通过批改 PluginLifecycle
外面的 backendInitPath
或 frontendInitPath
,这两个脚本相似于 preload 脚本,在插件加载前进行预加载,初始化插件环境。
具体是 VsCodePluginScanner 类里的 getLifecycle()
办法的 backendInitPath
。在这里 backendInitPath 被初始化为:backendInitPath: __dirname + '/plugin-vscode-init.js'
/**
* This interface describes a plugin lifecycle object.
*/
export interface PluginLifecycle {
startMethod: string;
stopMethod: string;
/**
* Frontend module name, frontend plugin should expose this name.
*/
frontendModuleName?: string;
/**
* Path to the script which should do some initialization before frontend plugin is loaded.
*/
frontendInitPath?: string; // 插件前端 preload
/**
* Path to the script which should do some initialization before backend plugin is loaded.
*/
backendInitPath?: string; // 插件后端 preload
}
而后在 PluginHostRPC 类里 new PluginManagerExtImpl()
实例时,在传入的 init 钩子中调用的 initContext
中通过 require()
办法加载。
留神:initContext
外面的 backendInitPath
来自于 PluginLifecycle
,并不是 ExtPluginApiProvider
。
// packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts
/**
* Handle the RPC calls.
*/
export class PluginHostRPC {
private apiFactory: PluginAPIFactory;
private pluginManager: PluginManagerExtImpl;
initialize(): void {this.pluginManager = this.createPluginManager(envExt, storageProxy, preferenceRegistryExt, webviewExt, this.rpc);
}
initContext(contextPath: string, plugin: Plugin): any {const { name, version} = plugin.rawModel;
console.log('PLUGIN_HOST(' + process.pid + '): initializing(' + name + '@' + version + 'with' + contextPath + ')');
const backendInit = require(contextPath); // 加载 PluginLifecycle 的 backendInitPath
backendInit.doInitialization(this.apiFactory, plugin); // 调用 backendInitPath 脚本裸露的 doInitialization 办法
}
createPluginManager(){
const pluginManager = new PluginManagerExtImpl({loadPlugin(plugin: Plugin): any {},
async init(raw: PluginMetadata[]): Promise<[Plugin[], Plugin[]]> {
let backendInitPath = pluginLifecycle.backendInitPath;
// if no init path, try to init as regular Theia plugin
if (!backendInitPath) {backendInitPath = __dirname + '/scanners/backend-init-theia.js';}
self.initContext(backendInitPath, plugin); // backendInitPath 来自于 pluginLifecycle
},
initExtApi(extApi: ExtPluginApi[]): void {const extApiInit = require(api.backendInitPath); // 加载 ExtPluginApiProvider 注入的 backendInitPath
extApiInit.provideApi(rpc, pluginManager);
},
loadTests: extensionTestsPath ? async () => {}
})
}
}
而 backendInitPath 配置的 plugin-vscode-init.ts
文件提供了 doInitialization
办法,在 doInitialization
办法中通过 Object.assign
合并 Theia API 到 vscode namespace,增加简略的 API 和字段。
// packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts
export const doInitialization: BackendInitializationFn = (apiFactory: PluginAPIFactory, plugin: Plugin) => {const vscode = Object.assign(apiFactory(plugin), {ExtensionKind}); // 合并 API
// use Theia plugin api instead vscode extensions
(<any>vscode).extensions = {get all(): any[] {return vscode.plugins.all.map(p => asExtension(p));
},
getExtension(pluginId: string): any | undefined {return asExtension(vscode.plugins.getPlugin(pluginId));
},
get onDidChange(): theia.Event<void> {return vscode.plugins.onDidChange;}
};
}
这种办法实质是在插件加载前运行脚本,不波及到 RPC,通过 Object.assign
合并简略的 API。
这种形式不如 ExtPluginApiProvider 的形式优雅,社区有人提里 PR 将其改成 ExtPluginApiProvider 的模式:Make “theia” and “vscode” contributed API’s #8142,目前为止仍然还没有被合并。
形式二:ExtPluginApiProvider 的形式
eclipse/che-theia 就是采纳了这种形式,性能十分弱小。具体可见:ChePluginApiProvider
Theia 官网文档没有提到这种形式,不过在 plugin-ext/doc 下倒是有一片简略的介绍文档:This document describes how to add new plugin api namespace
Che-Theia plug-in API 提供了 che 的 namespace。
首先申明 ExtPluginApiProvider 实现:
// extensions/eclipse-che-theia-plugin-ext/src/node/che-plugin-api-provider.ts
export class ChePluginApiProvider implements ExtPluginApiProvider {provideApi(): ExtPluginApi {
return {
frontendExtApi: {
initPath: '/che/api/che-api-worker-provider.js',
initFunction: 'initializeApi',
initVariable: 'che_api_provider'
},
backendInitPath: path.join('@eclipse-che/theia-plugin-ext/lib/plugin/node/che-api-node-provider.js')
};
}
}
而后注入到 backend moudule:
// extensions/eclipse-che-theia-plugin-ext/src/node/che-backend-module.ts
bind(ChePluginApiProvider).toSelf().inSingletonScope();
bind(Symbol.for(ExtPluginApiProvider)).toService(ChePluginApiProvider);
这样,前端及后盾都有了插件拓展的入口。
createAPIFactory
createAPIFactory 用于定义 Client API 接口,而后别离挂载到前端及后端插件运行时的 namespace。
createAPIFactory 办法的实现,和 Theia 源码中 packages/plugin-ext/src/plugin/plugin-context.ts
里 createAPIFactory 的实现统一:
export function createAPIFactory(rpc: RPCProtocol): CheApiFactory {return function (plugin: Plugin): typeof che {}}
前端入口 initializeApi
前端入口脚本 che-api-worker-provider.js
,实现并 export initializeApi
办法。在 initializeApi 中,传入 RPC,挂载到 che namespace。
// extensions/eclipse-che-theia-plugin-ext/src/plugin/webworker/che-api-worker-provider.ts
export const initializeApi: ExtPluginApiFrontendInitializationFn = (rpc: RPCProtocol, plugins: Map<string, Plugin>) => {const cheApiFactory = createAPIFactory(rpc); // 外围在于 createAPIFactory
const handler = {get: (target: any, name: string) => {const plugin = plugins.get(name);
if (plugin) {let apiImpl = pluginsApiImpl.get(plugin.model.id);
if (!apiImpl) {apiImpl = cheApiFactory(plugin);
pluginsApiImpl.set(plugin.model.id, apiImpl);
}
return apiImpl;
};MainPluginApiProvider
}
ctx['che'] = new Proxy(Object.create(null), handler); // 间接挂载到 che namespace
};
后端入口 provideApi
后端入口脚本 che-api-node-provider.js
,代码里须要裸露 export provideApi()
。
后端也是通过 createAPIFactory 定义 Client API 接口。
export const provideApi: ExtPluginApiBackendInitializationFn = (rpc: RPCProtocol, pluginManager: PluginManager) => {cheApiFactory = createAPIFactory(rpc);
plugins = pluginManager;
if (!isLoadOverride) {overrideInternalLoad();
isLoadOverride = true;
}
};
而后在 overrideInternalLoad()
办法中改写 module._load
,使 require('@eclipse-che/plugin')
返回定义的 Client API。
function overrideInternalLoad(): void {const module = require('module');
// save original load method
const internalLoad = module._load;
// if we try to resolve che module, return the filename entry to use cache.
module._load = function (request: string, parent: any, isMain: {}): any {if (request !== '@eclipse-che/plugin') {return internalLoad.apply(this, arguments);
}
apiImpl = cheApiFactory(plugin);
return apiImpl;
}
}
前后端 Client API 注入
Client API 能够看成是接口的定义,裸露到前后端运行时中,供插件调用。
new PluginManagerExtImpl()
传入的第一个参数 host 是 PluginHost 类型,其中的 initExtApi 等办法前端后盾别离实现:
export interface PluginHost {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
loadPlugin(plugin: Plugin): any;
init(data: PluginMetadata[]): Promise<[Plugin[], Plugin[]]> | [Plugin[], Plugin[]]; // 初始化插件
initExtApi(extApi: ExtPluginApi[]): void; // 初始化从内部引入的前后端 API,ExtPluginApi 蕴含 frontendExtApi 或 backendInitPath
loadTests?(): Promise<void>;}
initExtApi 前端,挂在 window 下。
initExtApi(extApi: ExtPluginApi[]): void {if (api.frontendExtApi) {ctx.importScripts(api.frontendExtApi.initPath);
ctx[api.frontendExtApi.initVariable][api.frontendExtApi.initFunction](rpc, pluginsModulesNames);
}
}
其中 const pluginsModulesNames = new Map<string, Plugin>();
插件的汇合。
initExtApi 后端,间接 require,并运行 provideApi()
initExtApi(extApi: ExtPluginApi[]): void {if (api.backendInitPath) {const extApiInit = require(api.backendInitPath);
extApiInit.provideApi(rpc, pluginManager); // 调用 provideAp
}
}
Server API 的注入
Server API 能够看成是接口的实现,通过 MainPluginApiProvider 注入到 browser,监听 Client API 的 RPC 音讯并触发对应解决办法。
MainPluginApiProvider 的实现应该蕴含新命名空间的 Plugin API 的 main(接口实现)局部。
/**
* Implementation should contains main(Theia) part of new namespace in Plugin API.
* [initialize](#initialize) will be called once per plugin runtime
*/
export interface MainPluginApiProvider {initialize(rpc: RPCProtocol, container: interfaces.Container): void;
}
注入到浏览器的 HostedPluginSupport 中,而后在 initRpc 办法一次调用注入 MainPluginApiProvider 的 initialize 办法进行初始化。
// packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
@injectable()
export class HostedPluginSupport {@inject(ContributionProvider)
@named(MainPluginApiProvider)
protected readonly mainPluginApiProviders: ContributionProvider<MainPluginApiProvider>;
protected initRpc(host: PluginHost, pluginId: string): RPCProtocol {const rpc = host === 'frontend' ? new PluginWorker().rpc : this.createServerRpc(pluginId, host);
setUpPluginApi(rpc, this.container); // 初始化 VScode API 的 Server 端实现
this.mainPluginApiProviders.getContributions().forEach(p => p.initialize(rpc, this.container)); // 初始化内部注入的接口实现
return rpc;
}
}
简化的 API 通信架构图大抵如下:
ExtPluginApiProvider 的拓展形式十分成熟优雅且功能强大,倡议采纳这一种。
Demo
见 Tide 我的项目 master 分支 extension/tide-theia-plugin-ext 模块。
参考
- Theia – Authoring Theia Plug-ins
- eclipse/che-theia
- VS Code API
- Contribution Points
- Compare Theia vs VS Code API
- This document describes how to add new plugin api namespace
- Tree View API
- Commands
- KAITIAN IDE 是如何构建扩大能力极强的插件体系的?