前言
Theia 目前提供的开发文档中对于 API 的介绍不太详细,缺少可以直接执行的示例,新手在新功能开发中不太容易理解,本文将阅读源码过程的一些代码片段摘出来进行归纳总结,通过局部的代码片段窥探如何开发 Theia 扩展。
获取应用根路径
通用方法:
environment.electron.isDevMode() ? process.cwd() : `${process.resourcesPath}/app`;
theia 里面:
process.env.THEIA_APP_PROJECT_PATH
获取 node_modules 下模块的路径
import { ApplicationPackage } from '@theia/core/shared/@theia/application-package';
@inject(ApplicationPackage)
protected readonly applicationPackage: ApplicationPackage;
this.applicationPackage.resolveModulePath('@theia/core');
获取工程路径
前端:
import { WorkspaceService } from '@theia/workspace/lib/browser';
private getCurrentWorkspaceUri(): URI | undefined {
return this.workspaceService.workspace?.resource;
}
后端:
import { WorkspaceServer } from '@theia/workspace/lib/common';
private getProjectPath(): string | undefined {
const projectPath = await this.workspaceServer.getMostRecentlyUsedWorkspace();
if (projectPath) {
return new URI(projectPath).path.toString();
}
}
打开文件选择框
类似于 Electron showOpenDialog,自动根据运行环境切换。
import { FileDialogService } from '@theia/filesystem/lib/browser';
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
const uri = await this.fileDialogService.showOpenDialog({
title: '标题',
canSelectFiles: true,
canSelectFolders: false
});
文件系统
前端部分推荐 FileService + EnvVariableServer,后端部分使用 Node.js APIs.
// frontend
const configDirUri = await this.envServer.getConfigDirUri();
const userInfoConfigUri = new URI(configDirUri).resolve('userinfo.config.json');
await this.fileService.write(userInfoConfigUri, JSON.stringify(userInfo));
// backend
const userInfoConfigPath = path.join(os.homedir(), '.mmp-studio', 'userinfo.config.json');
fs.writeFileSync(userInfoConfigPath, JSON.stringify(userInfo));
获取后端接口信息
前端拓展获取服务接口信息:
import { Endpoint } from '@theia/core/lib/browser/endpoint';
// 获取 http://localhost:{port}/files
new Endpoint({ path: 'files' }).getRestUrl().toString();
Tips: IDE 前端启动过程中获取到后端服务 address 信息后,将端口设置在 location.search 上。
后端拓展获取端口信息:
@injectable()
export class SimulatorEndpoint implements BackendApplicationContribution {
onStart(server: http.Server | https.Server) {
console.log('address: ', server.address());
}
}
监听视图的添加与隐藏
this.shell.onDidAddWidget((widget: Widget) => {
console.log('add widget: ', widget.id);
});
this.shell.onDidRemoveWidget((widget: Widget) => {
console.log('remove widget: ', widget.id);
});
自定义配置文件目录
settings.json
, keymaps.json
, recentworkspace.json
等配置文件的根目录默认地址是 ~/.theia,可以通过复写 EnvVariablesServer API 的 getConfigDirUri 方法进行自定义,最简单的方法是继承EnvVariablesServerImpl 子类,然后将其重新绑定到 backend 模块:
// your-env-variables-server.ts:
import { injectable } from 'inversify';
import { EnvVariablesServerImpl } from '@theia/core/lib/node/env-variables';
@injectable()
export class YourEnvVariableServer extends EnvVariablesServerImpl {
async getConfigDirUri(): Promise<string> {
return 'file:///path/to/your/desired/config/dir';
}
}
// your-backend-application-module.ts:
import { ContainerModule } from 'inversify';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { YourEnvVariableServer } from './your-env-variables-server';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(EnvVariablesServer).to(YourEnvVariableServer).inSingletonScope();
});
v1.5.0 有更加简单的配置方式,通过配置 process.env.THEIA_CONFIG_DIR 字段。
前端工程的配置修改方式:
rebind(PreferenceConfigurations).to(class extends PreferenceConfigurations {
getPaths(): string[] {
return [CUSTOM_CONFIG_DIR, '.theia', '.vscode'];
}
}).inSingletonScope();
自定义编辑器视图
Theia 自定义编辑器视图比 VS Code 更简单,有两种实现方式:
- 方法一:实现 OpenHandler 接口的 canHandle 和 open 方法
- 方法二:继承抽象类 WidgetOpenHandler,复写 createWidgetOptions 和 canHandle 方法关联 widget
bind(OpenHandler).to(ProjectConfigOpenHandler).inSingletonScope();
bind(WidgetFactory).toDynamicValue(ctx => ({
id: ProjectConfigWidget.ID,
createWidget: (options: ProjectConfigWidgetOptions) => {
const child = ctx.container.createChild();
child.bind(ProjectConfigWidgetOptions).toConstantValue(options);
child.bind(ProjectConfigWidget).toSelf();
return child.get(ProjectConfigWidget);
}
})).inSingletonScope();
// project-config-open-handler.ts
@injectable()
export class ProjectConfigOpenHandler extends WidgetOpenHandler<ProjectConfigWidget> {
readonly id = ProjectConfigWidget.ID;
readonly label?: string = 'Preview';
private defaultFileName: string = 'project.config.json';
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
protected createWidgetOptions(uri: URI): Object {
return {
uri: uri.withoutFragment().toString()
};
}
canHandle(uri: URI): number {
if (uri.path.toString().includes(this.defaultFileName)) {
return 1000;
}
return 0;
}
}
面板中 iframe、WebView 无法响应鼠标点击事件
import { ApplicationShellMouseTracker } from '@theia/core/lib/browser/shell/application-shell-mouse-tracker';
@inject(ApplicationShellMouseTracker)
protected readonly mouseTracker: ApplicationShellMouseTracker;
// Style from core
const TRANSPARENT_OVERLAY_STYLE = 'theia-transparent-overlay';
@postConstruct()
protected init(): void {
this.frame = this.createWebView();
this.transparentOverlay = this.createTransparentOverlay();
this.node.appendChild(this.frame);
this.node.appendChild(this.transparentOverlay);
this.toDispose.push(this.mouseTracker.onMousedown(e => {
if (this.frame.style.display !== 'none') {
this.transparentOverlay.style.display = 'block';
}
}));
this.toDispose.push(this.mouseTracker.onMouseup(e => {
if (this.frame.style.display !== 'none') {
this.transparentOverlay.style.display = 'none';
}
}));
}
createWebView(): Electron.WebviewTag {
const webview = document.createElement('webview') as Electron.WebviewTag;
...
return webview;
}
createTransparentOverlay() {
const transparentOverlay = document.createElement('div');
transparentOverlay.classList.add(TRANSPARENT_OVERLAY_STYLE);
transparentOverlay.style.display = 'none';
return transparentOverlay;
}
检查编辑器是否可以保存
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
@inject(EditorManager)
protected readonly editorManager: EditorManager;
/**
* 检查编辑器是否可以保存
*/
checkEditorSaveable() {
let isDirty = false;
const trackedEditors: EditorWidget[] = this.editorManager.all;
for (let widget of trackedEditors) {
isDirty = widget.saveable.dirty;
if (isDirty) break;
}
return isDirty;
}
父进程 kill 后自动退出子进程
Theia 源码中有这么一个方法 checkParentAlive,在每个 fork 的子进程里面都会引用,作用就是定时检测父进程是否存活,不存活就自动退出子进程。
/**
* Exit the current process if the parent process is not alive.
* Relevant only for some OS, like Windows
*/
export function checkParentAlive(): void {
if (process.env[THEIA_PARENT_PID]) {
const parentPid = Number(process.env[THEIA_PARENT_PID]);
if (typeof parentPid === 'number' && !isNaN(parentPid)) {
setInterval(() => {
try {
// throws an exception if the main process doesn't exist anymore.
process.kill(parentPid, 0);
} catch {
process.exit();
}
}, 5000);
}
}
}
process.kill() 函数并不是杀死进程,它实际上只是信号发送者,就像 kill 系统调用,发送的信号可能会做其他事情而不是杀死目标进程,比如发送 0 可以来测试进程是否存在,如果进程存在则没影响,如果进程不存在则抛出错误。
Promise Deferred 模式
/**
* Simple implementation of the deferred pattern.
* An object that exposes a promise and functions to resolve and reject it.
*/
export class Deferred<T = void> {
state: 'resolved' | 'rejected' | 'unresolved' = 'unresolved';
resolve: (value: T | PromiseLike<T>) => void;
reject: (err?: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
promise = new Promise<T>((resolve, reject) => {
this.resolve = result => {
resolve(result);
if (this.state === 'unresolved') {
this.state = 'resolved';
}
};
this.reject = err => {
reject(err);
if (this.state === 'unresolved') {
this.state = 'rejected';
}
};
});
}
用法:
function asyncDoSomeing(flag, message) {
var deferred = new Deferred();
setTimeout(function () {
if (flag) {
deferred.resolve(message);
} else {
deferred.reject({ code: 400, message: '拒绝' });
}
}, 3000);
return deferred.promise;
}
DisposableCollection
export class DisposableCollection implements Disposable {
protected readonly disposables: Disposable[] = [];
protected readonly onDisposeEmitter = new Emitter<void>();
constructor(...toDispose: Disposable[]) {
toDispose.forEach(d => this.push(d));
}
/**
* This event is fired only once
* on first dispose of not empty collection.
*/
get onDispose(): Event<void> {
return this.onDisposeEmitter.event;
}
protected checkDisposed(): void {
if (this.disposed && !this.disposingElements) {
this.onDisposeEmitter.fire(undefined);
this.onDisposeEmitter.dispose();
}
}
get disposed(): boolean {
return this.disposables.length === 0;
}
private disposingElements = false;
dispose(): void {
if (this.disposed || this.disposingElements) {
return;
}
this.disposingElements = true;
while (!this.disposed) {
try {
this.disposables.pop()!.dispose();
} catch (e) {
console.error(e);
}
}
this.disposingElements = false;
this.checkDisposed();
}
push(disposable: Disposable): Disposable {
const disposables = this.disposables;
disposables.push(disposable);
const originalDispose = disposable.dispose.bind(disposable);
const toRemove = Disposable.create(() => {
const index = disposables.indexOf(disposable);
if (index !== -1) {
disposables.splice(index, 1);
}
this.checkDisposed();
});
disposable.dispose = () => {
toRemove.dispose();
disposable.dispose = originalDispose;
originalDispose();
};
return toRemove;
}
pushAll(disposables: Disposable[]): Disposable[] {
return disposables.map(disposable =>
this.push(disposable)
);
}
}
用法:
// 初始化 Disposable 对象收集器
const toDispose = new DisposableCollection();
// 收集待销毁的 Disposable 对象(通过 Disposable.create 方法生成或者实现 Disposable 接口的类
toDispose.push(Disposable.create(() => {
// ...
})
// 销毁
toDispose.dispose()
写文章不容易,也许写这些代码就几分钟的事,写一篇大家好接受的文章或许需要几天的酝酿,如果文章对您有帮助请我喝杯咖啡吧!