Theia 扩展:自定义视图——用 Lumino 与 ViewContribution 挂一个侧边栏面板
在 Theia 里,很多插件最终都会落到「在侧边栏/面板里多一块自己的 UI」:比如任务列表、日志窗口、资源树、诊断面板等等。
这一篇聚焦一件事:如何基于 Lumino 的 Widget 系统,再配合 Theia 的ViewContribution扩展点,在侧边栏挂出一个自己的视图,并和命令/菜单打通。
Theia 视图的大致结构:Widget + ViewContribution
先用一个简单模型来记忆 Theia 里的视图系统:
- Lumino Widget:
- 更底层的视图抽象,负责「这块矩形区域里渲染什么内容」;
- Theia 在此之上封装出一系列 Widget 基类(包括支持 React 的版本)。
- Theia ViewContribution
- 一个扩展点接口,负责「把某个 Widget 挂到 ApplicationShell 的哪个位置」;
- 控制视图的默认位置(左侧栏/右侧栏/底部面板)、id、可见性,以及与命令的联动。
可以简单理解为:
- Widget 解决的是「画什么」;ViewContribution 解决的是「画在哪里、怎么打开」。
写一个自定义视图,通常需要两步:
- 写一个 Widget(或 ReactWidget)实现具体 UI;
- 写一个 ViewContribution,把这个 Widget 注册到某个视图容器里。
第一步:实现一个简单的 Widget(或 ReactWidget)
Theia 提供了多种 Widget 基类,这里可以先用更常见的 ReactWidget 来承载内容(即便你不用复杂 React,也方便后续扩展)。
示意结构(简化版):
1import { injectable, postConstruct } from "inversify";
2import { ReactWidget } from "@theia/core/lib/browser";
3import * as React from "react";
4
5@injectable()
6export class MySidePanelWidget extends ReactWidget {
7 static readonly ID = "my-extension:side-panel";
8 static readonly LABEL = "My Side Panel";
9
10 @postConstruct()
11 protected init(): void {
12 this.id = MySidePanelWidget.ID;
13 this.title.label = MySidePanelWidget.LABEL;
14 this.title.closable = true;
15 this.update();
16 }
17
18 protected render(): React.ReactNode {
19 return <div>这里是自定义侧边栏内容</div>;
20 }
21}
要点:
- 通过
static ID/LABEL约定 Widget 的 id 和标签; - 在
init(@postConstruct)里设置 Widget 的基本属性; - 在
render里返回 React 节点(也可以用其他 Widget 类型自行处理 DOM)。
第二步:用 ViewContribution 把 Widget 挂到侧边栏
接下来,实现一个 ViewContribution 来声明:
- 这个视图的 id 是什么;
- 默认出现在哪个区域(例如左侧栏);
- 是否默认可见;
- 以及相关命令菜单。
示意代码(省略 import,只看结构):
1@injectable()
2export class MySidePanelContribution extends AbstractViewContribution<MySidePanelWidget> {
3 constructor() {
4 super({
5 widgetId: MySidePanelWidget.ID,
6 widgetName: MySidePanelWidget.LABEL,
7 defaultWidgetOptions: {
8 area: "left",
9 },
10 toggleCommandId: "my-extension.toggleSidePanel",
11 });
12 }
13
14 // 可选:注册命令,让用户可以通过命令面板/快捷键打开视图
15 registerCommands(registry: CommandRegistry): void {
16 registry.registerCommand(
17 {
18 id: "my-extension.toggleSidePanel",
19 label: "切换 My Side Panel",
20 },
21 {
22 execute: async () => {
23 await this.openView({ reveal: true });
24 },
25 }
26 );
27 }
28}
AbstractViewContribution 已经帮你实现了很多样板逻辑,例如:
- 通过
openView打开或聚焦视图; - 根据
defaultWidgetOptions把 Widget 挂到 ApplicationShell 的指定区域。
你需要做的是:
- 在构造函数里给出视图配置;
- 视情况重写
registerCommands/registerMenus/registerKeybindings来挂命令和入口。
第三步:在前端模块中绑定 Widget 与 ViewContribution
和命令/菜单一样,Widget 和 ViewContribution 也需要通过 DI 容器绑定。
在 frontend-module.ts 里,大致会是这样:
1export default new ContainerModule((bind) => {
2 bind(MySidePanelWidget).toSelf();
3 bind(WidgetFactory)
4 .toDynamicValue((ctx) => ({
5 id: MySidePanelWidget.ID,
6 createWidget: () => ctx.container.get(MySidePanelWidget),
7 }))
8 .inSingletonScope();
9
10 bind(ViewContribution).to(MySidePanelContribution).inSingletonScope();
11 bind(CommandContribution).toService(MySidePanelContribution);
12});
要点:
- 先把 Widget 绑定到容器,并通过
WidgetFactory告诉框架如何创建它; - 再把
MySidePanelContribution作为ViewContribution绑定进去; - 如果在贡献类里重用其命令注册逻辑,可以让
CommandContribution指向同一个实例(toService)。
这样,应用启动后:
- 框架会自动根据
ViewContribution注册视图; - 当用户执行 toggle 命令时,通过
openView打开或聚焦侧边栏面板。
视图与命令/菜单/快捷键的协同
自定义视图通常不会单独存在,它经常和命令/菜单/快捷键协同工作。
常见的几种联动模式包括:
- 在 ViewContribution 里注册命令:
- 比如「打开/关闭某个侧边栏面板」、「刷新视图数据」、「在视图中执行某个操作」;
- 在 MenuContribution 里把这些命令挂到主菜单或视图区域右键菜单;
- 在 KeybindingContribution 里给「打开视图」之类的命令配置快捷键。
这样,用户可以:
- 通过命令面板/快捷键快速打开你的侧边栏;
- 在视图内部通过按钮+命令结合,触发更多行为。
从架构角度看:
- 命令系统依然是核心中枢;
- ViewContribution/Widget 提供了一块 UI 区域来承载命令触发结果。
和 VS Code TreeView / Webview 的差异感知
如果已经熟悉 VS Code 视图扩展,可以用它来反向理解 Theia:
- VS Code TreeView:
- 通过
TreeDataProvider提供数据结构; - UI 受 VS Code 控制。
- 通过
- VS Code Webview:
- 完全自定义 HTML/CSS/JS,一块沙箱区域。
Theia 的 Widget + ViewContribution 更偏向于:
- 给你一个完整的 UI 容器(Widget),你可以用 React/Lumino 任意搭布局;
- 通过 ViewContribution 决定这个容器在 IDE 壳中的位置和打开方式。
可以粗略记成:
- VS Code:在既有插槽里扩展视图;
- Theia:你自己就是在拼装 IDE 的一块视图模块。
小结:在 Theia 里挂一个侧边栏视图的心智模型
可以用一条简单的步骤来回顾这一篇:
- 写一个 Widget(或 ReactWidget),负责「这块视图区域里长什么样」;
- 写一个 ViewContribution,指定「它在哪个区域、用什么命令打开」;
- 在前端模块里把两者通过 DI 容器绑定进去,让框架启动时自动发现并挂载。
掌握了这条路线之后,你可以很自然地把它推广到更多插件场景:
日志面板、任务状态视图、分析结果面板、工具面板……本质上都是在 IDE 壳里多挂了一个「领域专属的 Widget」。