Eclipse Theia技术揭秘——脚手架源码分析

Eclipse Theia:云原生IDE框架的架构解析与实战指南 IDE(集成开发环境)是开发者日常编码的核心工具,从嵌入式开发常用的Arduino IDE,到集成AI助手如通义灵码的智能插件,不同场景对IDE的定制化需求日益凸显。传统桌面IDE难以满足云端协作和深度定制,而Eclipse Theia作为一个开源IDE框架,通过前后端分离架构与LSP/DAP协议标准,将编辑器、语言服务、调试功能模块化,让团队能够快速构建专属Web IDE。从Theia的定位讲起,解析其核心架构与扩展机制,并分享从零搭建云IDE的实战经验与避坑指南。无论你是想自研私有云IDE,还是探索AI 阅读详情

在之前的文章中,我们介绍了Theia的构建,其中用到了很多theia的命令,这些命令来自于@theia/cli这个库,本篇文章我们就对@theia/cli以及相关联的库进行分析。本篇文章是继构建桌面IDE,工程代码为Theia Blueprint,源码版本是1.28.0。

下载

首先我们在Github中下载Theia的源码。
在这里插入图片描述

目录结构

源码目录中我们主要关注dev-packages和packages两个包,dev-packages是开发工具包,packages下是Theia的核心依赖包,我们重点看一下dev-packages下的内容。

在这里插入图片描述

  • application-manager:应用工程管理器,提供 Frontend、Backend、Webpack 代码生成
  • application-package:应用 package.json 配置解析,管理 Application、Extensions
  • cli:是一个命令行工具,用于管理基于 Theia 的应用程序,为扩展和应用程序开发提供了有用的脚本和命令
  • ffmpeg:是一个Node Native 插件,用于动态链接到 Electronffmpeg.dll并获取包含的编解码器列表
  • localization-manager:用于为不同语言创建 Theia 和 Theia 扩展的本地化
  • ovsx-client:该包用于通过其 REST API@theia/ovsx-client进行交互。open-vsx该包允许客户端获取扩展及其元数据、搜索注册表,并包含必要的逻辑来根据提供的支持的 API 版本确定兼容性
  • private-eslint-plugin:对 Eclipse Theia 开发有用的@theia/eslint-plugin贡献规则。该插件通过静态分析帮助识别开发过程中的问题,包括代码质量、潜在问题和代码异常。
  • private-ext-scripts:是一个命令行工具,用于在 Theia 包中运行共享的 npm 脚本
  • private-re-exports:用于重新导出依赖项
  • request:发送代理请求的库

然后我们重点看一下cli这个库。

CLI分析

在package.json中可以看到bin字段注册的theia命令。

 "bin": {
    "theia": "./bin/theia"
  }

然后我们看一下目录

在这里插入图片描述
看一下bin/theia的内容。

#!/usr/bin/env node
require('../lib/theia')

他引用了编译后lib下的theia,也就是编译前src下的theia.ts。我们具体看一下theia.ts做了哪些内容。

在这里插入图片描述
我们可以看到这个文件中引用了@theia/application-manager、@theia/application-package、@theia/ffmpeg以及@theia/localization-manager库,并定义了一些函数,然后执行了theiaCli这个函数,我们针对theiaCli这个函数具体看一下。

在这里插入图片描述

通过yargs定义了很多命令。

  • start
  • clean
  • copy
  • generate
  • build
  • rebuild
  • rebuild:browser
  • rebuild:electron
  • check:hoisted
  • check:theia-version
  • check:dependencies
  • download:plugins
  • nls-localize
  • nls-extract
  • test
  • ffmpeg:replace
  • ffmpeg:check

我们根据工程中使用了theia clean、theia build、theia rebuild:electron、theia download:plugins这四个命令。

theia clean

我们从代码中可以看到theia clean最终调用了ApplicationPackageManager的clean方法,我们找到application-manager包下的application-package-manager.ts文件可以看到ApplicationPackageManager中定义的clean方法。

protected async remove(fsPath: string): Promise<void> {
        if (await fs.pathExists(fsPath)) {
            await fs.remove(fsPath);
        }
    }

    async clean(): Promise<void> {
        await Promise.all([
            this.remove(this.pck.lib()),
            this.remove(this.pck.srcGen()),
            this.remove(new WebpackGenerator(this.pck).genConfigPath)
        ]);
    }

可以看到clean删除了三个部分,分别是lib、src-gen、以及webpack的配置文件gen-webpack.config.js。其中this.pck是application-package下的application-package.ts的ApplicationPackage,我们可以看一下lib和srcGen方法的定义。

  path(...segments: string[]): string {
        return paths.resolve(this.projectPath, ...segments);
    }
    lib(...segments: string[]): string {
        return this.path('lib', ...segments);
    }
    srcGen(...segments: string[]): string {
        return this.path('src-gen', ...segments);
    }

getConfigPath就是WebpackGenerator中定义的。

get genConfigPath(): string {
        return this.pck.path('gen-webpack.config.js');
}
theia build

theia build命令会调用ApplicationPackageManager的build方法。看一下build方法的定义。

async prepare(): Promise<void> {
        if (this.pck.isElectron()) {
            await this.prepareElectron();
        }
}
async generate(options: GeneratorOptions = {}): Promise<void> {
        try {
            await this.prepare();
        } catch (error) {
            if (error instanceof AbortError) {
                console.warn(error.message);
                process.exit(1);
            }
            throw error;
        }
        await Promise.all([
            new WebpackGenerator(this.pck, options).generate(),
            new BackendGenerator(this.pck, options).generate(),
            new FrontendGenerator(this.pck, options).generate(),
        ]);
}
async build(args: string[] = [], options: GeneratorOptions = {}): Promise<void> {
        await this.generate(options);
        await this.copy();
        return this.__process.run('webpack', args);
}
protected async prepareElectron(): Promise<void> {
        let theiaElectron;
        try {
            theiaElectron = await import('@theia/electron');
        } catch (error) {
            if (error.code === 'ERR_MODULE_NOT_FOUND') {
                throw new AbortError('Please install @theia/electron as part of your Theia Electron application');
            }
            throw error;
        }
        const expectedRange = theiaElectron.electronRange;
        const appPackageJsonPath = this.pck.path('package.json');
        const appPackageJson = await fs.readJSON(appPackageJsonPath) as { devDependencies?: Record<string, string> };
        if (!appPackageJson.devDependencies) {
            appPackageJson.devDependencies = {};
        }
        const currentRange: string | undefined = appPackageJson.devDependencies.electron;
        if (!currentRange || semver.compare(semver.minVersion(currentRange), semver.minVersion(expectedRange)) < 0) {
            // Update the range with the recommended one and write it on disk.
            appPackageJson.devDependencies = this.insertAlphabetically(appPackageJson.devDependencies, 'electron', expectedRange);
            await fs.writeJSON(appPackageJsonPath, appPackageJson, { spaces: 2 });
            throw new AbortError('Updated dependencies, please run "install" again');
        }
        if (!theiaElectron.electronVersion || !semver.satisfies(theiaElectron.electronVersion, currentRange)) {
            throw new AbortError('Dependencies are out of sync, please run "install" again');
        }
        await ffmpeg.replaceFfmpeg();
        await ffmpeg.checkFfmpeg();
}
async copy(): Promise<void> {
        await fs.ensureDir(this.pck.lib());
        await fs.copy(this.pck.frontend('index.html'), this.pck.lib('index.html'));
}

我们看到build方法做了三件事。

  1. 调用generate函数,这个函数中最终会校验electron版本,调用WebpackGenerator生成gen-webpack.config.js,调用BackendGenerator生成src-gen/backend下main.js和server.js,调用FrontendGenerator生成src-gen/frontend下electron-main.js、index.html和index.js。
  2. 调用copy函数,将src-gen/frontend下的index.html复制到lib目录下
  3. 调用webpack进行编译
theia rebuild:electron

这个命令最终调用application-manager包下的rebuild.ts文件中rebuild方法。

export const DEFAULT_MODULES = [
    'node-pty',
    'nsfw',
    'native-keymap',
    'find-git-repositories',
    'drivelist',
];
/**
 * @param target What to rebuild for.
 * @param options
 */
export function rebuild(target: RebuildTarget, options: RebuildOptions = {}): void {
    const {
        modules = DEFAULT_MODULES,
        cacheRoot = process.cwd(),
        forceAbi,
    } = options;
    const cache = path.resolve(cacheRoot, '.browser_modules');
    const cacheExists = folderExists(cache);
    guardExit(async token => {
        if (target === 'electron' && !cacheExists) {
            process.exitCode = await rebuildElectronModules(cache, modules, forceAbi, token);
        } else if (target === 'browser' && cacheExists) {
            process.exitCode = await revertBrowserModules(cache, modules);
        } else {
            console.log(`native node modules are already rebuilt for ${target}`);
        }
    }).catch(errorOrSignal => {
        if (typeof errorOrSignal === 'string' && errorOrSignal in os.constants.signals) {
            process.kill(process.pid, errorOrSignal);
        } else {
            throw errorOrSignal;
        }
    });
}
async function rebuildElectronModules(browserModuleCache: string, modules: string[], forceAbi: NodeABI | undefined, token: ExitToken): Promise<number> {
    const modulesJsonPath = path.join(browserModuleCache, 'modules.json');
    const modulesJson: ModulesJson = await fs.access(modulesJsonPath).then(
        () => fs.readJson(modulesJsonPath),
        () => ({})
    );
    let success = true;
    // Backup already built browser modules.
    await Promise.all(modules.map(async module => {
        let modulePath;
        try {
            modulePath = require.resolve(`${module}/package.json`, {
                paths: [process.cwd()],
            });
        } catch (_) {
            console.debug(`Module not found: ${module}`);
            return; // Skip current module.
        }
        const src = path.dirname(modulePath);
        const dest = path.join(browserModuleCache, module);
        try {
            await fs.remove(dest);
            await fs.copy(src, dest, { overwrite: true });
            modulesJson[module] = {
                originalLocation: src,
            };
            console.debug(`Processed "${module}"`);
        } catch (error) {
            console.error(`Error while doing a backup for "${module}": ${error}`);
            success = false;
        }
    }));
    if (Object.keys(modulesJson).length === 0) {
        console.debug('No module to rebuild.');
        return 0;
    }
    // Update manifest tracking the backups' original locations.
    await fs.writeJson(modulesJsonPath, modulesJson, { spaces: 2 });
    // If we failed to process a module then exit now.
    if (!success) {
        return 1;
    }
    const todo = modules.map(m => {
        // electron-rebuild ignores the module namespace...
        const slash = m.indexOf('/');
        return m.startsWith('@') && slash !== -1
            ? m.substring(slash + 1)
            : m;
    });
    let exitCode: number | undefined;
    try {
        if (process.env.THEIA_REBUILD_NO_WORKAROUND) {
            exitCode = await runElectronRebuild(todo, forceAbi, token);
        } else {
            exitCode = await electronRebuildExtraModulesWorkaround(process.cwd(), todo, () => runElectronRebuild(todo, forceAbi, token), token);
        }
    } catch (error) {
        console.error(error);
    } finally {
        // If code is undefined or different from zero we need to revert back to the browser modules.
        if (exitCode !== 0) {
            await revertBrowserModules(browserModuleCache, modules);
        }
        return exitCode ?? 1;
    }
}
async function runElectronRebuild(modules: string[], forceAbi: NodeABI | undefined, token: ExitToken): Promise<number> {
    const todo = modules.join(',');
    return new Promise(async (resolve, reject) => {
        let command = `npx --no-install electron-rebuild -f -w=${todo} -o=${todo}`;
        if (forceAbi) {
            command += ` --force-abi ${forceAbi}`;
        }
        const electronRebuild = cp.spawn(command, {
            stdio: 'inherit',
            shell: true,
        });
        token.onSignal(signal => electronRebuild.kill(signal));
        electronRebuild.on('error', reject);
        electronRebuild.on('close', (code, signal) => {
            if (signal) {
                reject(new Error(`electron-rebuild exited with "${signal}"`));
            } else {
                resolve(code!);
            }
        });
    });
}

在rebuild方法中会先创建.browser_modules目录,然后调用rebuildElectronModules方法将’node-pty’, ‘nsfw’, ‘native-keymap’, ‘find-git-repositories’, ‘drivelist’这几个模块源码复制到.browser_modules并生成modules.json配置文件。最终调用runElectronRebuild方法,这个方法中执行了electron-rebuild命令,将Electron项目使用的Node.js版本重建上述配置的原生Node.js模块。

theia download:plugins

这个命令会调用cli包下download-plugins.ts文件下downloadPlugins方法。

export default async function downloadPlugins(options: DownloadPluginsOptions = {}): Promise<void> {
    const {
        packed = false,
        ignoreErrors = false,
        apiVersion = DEFAULT_SUPPORTED_API_VERSION,
        apiUrl = 'https://open-vsx.org/api',
        parallel = true,
        proxyUrl,
        proxyAuthorization,
        strictSsl
    } = options;

    requestService.configure({
        proxyUrl,
        proxyAuthorization,
        strictSSL: strictSsl
    });

    // Collect the list of failures to be appended at the end of the script.
    const failures: string[] = [];

    // Resolve the `package.json` at the current working directory.
    const pck = JSON.parse(await fs.readFile(path.resolve('package.json'), 'utf8'));

    // Resolve the directory for which to download the plugins.
    const pluginsDir = pck.theiaPluginsDir || 'plugins';

    // Excluded extension ids.
    const excludedIds = new Set<string>(pck.theiaPluginsExcludeIds || []);

    const parallelOrSequence = async (...tasks: Array<() => unknown>) => {
        if (parallel) {
            await Promise.all(tasks.map(task => task()));
        } else {
            for (const task of tasks) {
                await task();
            }
        }
    };

    // Downloader wrapper
    const downloadPlugin = (plugin: PluginDownload): Promise<void> => downloadPluginAsync(failures, plugin.id, plugin.downloadUrl, pluginsDir, packed, plugin.version);

    const downloader = async (plugins: PluginDownload[]) => {
        await parallelOrSequence(...plugins.map(plugin => () => downloadPlugin(plugin)));
    };

    await fs.mkdir(pluginsDir, { recursive: true });

    if (!pck.theiaPlugins) {
        console.log(chalk.red('error: missing mandatory \'theiaPlugins\' property.'));
        return;
    }
    try {
        console.warn('--- downloading plugins ---');
        // Download the raw plugins defined by the `theiaPlugins` property.
        // This will include both "normal" plugins as well as "extension packs".
        const pluginsToDownload = Object.entries(pck.theiaPlugins)
            .filter((entry: [string, unknown]): entry is [string, string] => typeof entry[1] === 'string')
            .map(([pluginId, url]) => ({ id: pluginId, downloadUrl: url }));
        await downloader(pluginsToDownload);

        const handleDependencyList = async (dependencies: Array<string | string[]>) => {
            const client = new OVSXClient({ apiVersion, apiUrl }, requestService);
            // De-duplicate extension ids to only download each once:
            const ids = new Set<string>(dependencies.flat());
            await parallelOrSequence(...Array.from(ids, id => async () => {
                try {
                    const extension = await client.getLatestCompatibleExtensionVersion(id);
                    const version = extension?.version;
                    const downloadUrl = extension?.files.download;
                    if (downloadUrl) {
                        await downloadPlugin({ id, downloadUrl, version });
                    } else {
                        failures.push(`No download url for extension pack ${id} (${version})`);
                    }
                } catch (err) {
                    failures.push(err.message);
                }
            }));
        };

        console.warn('--- collecting extension-packs ---');
        const extensionPacks = await collectExtensionPacks(pluginsDir, excludedIds);
        if (extensionPacks.size > 0) {
            console.warn(`--- resolving ${extensionPacks.size} extension-packs ---`);
            await handleDependencyList(Array.from(extensionPacks.values()));
        }

        console.warn('--- collecting extension dependencies ---');
        const pluginDependencies = await collectPluginDependencies(pluginsDir, excludedIds);
        if (pluginDependencies.length > 0) {
            console.warn(`--- resolving ${pluginDependencies.length} extension dependencies ---`);
            await handleDependencyList(pluginDependencies);
        }

    } finally {
        temp.cleanupSync();
    }
    for (const failure of failures) {
        console.error(failure);
    }
    if (!ignoreErrors && failures.length > 0) {
        throw new Error('Errors downloading some plugins. To make these errors non fatal, re-run with --ignore-errors');
    }
}
/**
 * Walk the plugin directory and collect available extension paths.
 * @param pluginDir the plugin directory.
 * @returns the list of all available extension paths.
 */
async function collectPackageJsonPaths(pluginDir: string): Promise<string[]> {
    const packageJsonPathList: string[] = [];
    const files = await fs.readdir(pluginDir);
    // Recursively fetch the list of extension `package.json` files.
    for (const file of files) {
        const filePath = path.join(pluginDir, file);
        if ((await fs.stat(filePath)).isDirectory()) {
            packageJsonPathList.push(...await collectPackageJsonPaths(filePath));
        } else if (path.basename(filePath) === 'package.json' && !path.dirname(filePath).includes('node_modules')) {
            packageJsonPathList.push(filePath);
        }
    }
    return packageJsonPathList;
}

/**
 * Get the mapping of extension-pack paths and their included plugin ids.
 * - If an extension-pack references an explicitly excluded `id` the `id` will be omitted.
 * @param pluginDir the plugin directory.
 * @param excludedIds the list of plugin ids to exclude.
 * @returns the mapping of extension-pack paths and their included plugin ids.
 */
async function collectExtensionPacks(pluginDir: string, excludedIds: Set<string>): Promise<Map<string, string[]>> {
    const extensionPackPaths = new Map<string, string[]>();
    const packageJsonPaths = await collectPackageJsonPaths(pluginDir);
    await Promise.all(packageJsonPaths.map(async packageJsonPath => {
        const json = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
        const extensionPack: unknown = json.extensionPack;
        if (Array.isArray(extensionPack)) {
            extensionPackPaths.set(packageJsonPath, extensionPack.filter(id => {
                if (excludedIds.has(id)) {
                    console.log(chalk.yellow(`'${id}' referred to by '${json.name}' (ext pack) is excluded because of 'theiaPluginsExcludeIds'`));
                    return false; // remove
                }
                return true; // keep
            }));
        }
    }));
    return extensionPackPaths;
}

/**
 * Get the mapping of  paths and their included plugin ids.
 * - If an extension-pack references an explicitly excluded `id` the `id` will be omitted.
 * @param pluginDir the plugin directory.
 * @param excludedIds the list of plugin ids to exclude.
 * @returns the mapping of extension-pack paths and their included plugin ids.
 */
async function collectPluginDependencies(pluginDir: string, excludedIds: Set<string>): Promise<string[]> {
    const dependencyIds: string[] = [];
    const packageJsonPaths = await collectPackageJsonPaths(pluginDir);
    await Promise.all(packageJsonPaths.map(async packageJsonPath => {
        const json = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
        const extensionDependencies: unknown = json.extensionDependencies;
        if (Array.isArray(extensionDependencies)) {
            for (const dependency of extensionDependencies) {
                if (excludedIds.has(dependency)) {
                    console.log(chalk.yellow(`'${dependency}' referred to by '${json.name}' is excluded because of 'theiaPluginsExcludeIds'`));
                } else {
                    dependencyIds.push(dependency);
                }
            }
        }
    }));
    return dependencyIds;
}

首先调用@theia/request包中的请求服务并配置,然后读取package.json,获取theiaPluginsDir、theiaPluginsExcludeIds和theiaPlugins的配置,根据theiaPlugins配置的插件列表将插件下载到theiaPluginsDir配置的目录下,然后遍历theiaPluginsDir目录下的插件,读取插件的package.json,获取extensionPack和extensionDependencies不包含theiaPluginsExcludeIds配置的插件,extensionPack是具有可以一起安装的扩展 ID 的数组。扩展的 id 始终是 p u b l i s h e r . {publisher}. publisher.{name}. 例如:vscode.csharp,extensionDependencies是具有此扩展所依赖的扩展 ID 的数组。扩展的 id 始终是 p u b l i s h e r . {publisher}. publisher.{name}. 例如:vscode.csharp。他们都是vscode插件的配置,具体可以查阅 vscode扩展清单 ,获取列表之后再下载对应的插件。

Theia Blueprint启动

上面脚手架的一些命令了解之后,我们看一下Theia Blueprint的启动过程。我们看一下package.json中的启动脚本。

"scripts": {
     "start": "electron scripts/theia-electron-main.js"
}

它调用electron命令执行了theia-electron-main.js的启动脚本,其中package.json中main配置也是main”: “scripts/theia-electron-main.js,package.json 中指定的脚本文件 main 是所有 Electron 应用的入口点。 这个文件控制 主程序 (main process),它运行在 Node.js 环境里,负责控制您应用的生命周期、显示原生界面、执行特殊操作并管理渲染器进程 (renderer processes)。electron具体可以查阅对应的文档。

// scripts/theia-electron-main.js
const path = require('path')
const os = require('os')

// Update to override the supported VS Code API version.
// process.env.VSCODE_API_VERSION = '1.50.0'

// Use a set of builtin plugins in our application.
process.env.THEIA_DEFAULT_PLUGINS = `local-dir:${path.resolve(__dirname, '..', 'plugins')}`

// Lookup inside the user's home folder for more plugins, and accept user-defined paths.
process.env.THEIA_PLUGINS = [
    process.env.THEIA_PLUGINS, `local-dir:${path.resolve(os.homedir(), '.theia', 'plugins')}`,
].filter(Boolean).join(',')

// Handover to the auto-generated electron application handler.
require('../src-gen/frontend/electron-main.js')

其中定义了一些环境变量,然后引入src-gen/frontend/electron-main.js文件。

// @ts-check

require('reflect-metadata');
require('@theia/electron/shared/@electron/remote/main').initialize();

// Useful for Electron/NW.js apps as GUI apps on macOS doesn't inherit the `$PATH` define
// in your dotfiles (.bashrc/.bash_profile/.zshrc/etc).
// https://github.com/electron/electron/issues/550#issuecomment-162037357
// https://github.com/eclipse-theia/theia/pull/3534#issuecomment-439689082
require('fix-path')();

// Workaround for https://github.com/electron/electron/issues/9225. Chrome has an issue where
// in certain locales (e.g. PL), image metrics are wrongly computed. We explicitly set the
// LC_NUMERIC to prevent this from happening (selects the numeric formatting category of the
// C locale, http://en.cppreference.com/w/cpp/locale/LC_categories).
if (process.env.LC_ALL) {
    process.env.LC_ALL = 'C';
}
process.env.LC_NUMERIC = 'C';

const { default: electronMainApplicationModule } = require('@theia/core/lib/electron-main/electron-main-application-module');
const { ElectronMainApplication, ElectronMainApplicationGlobals } = require('@theia/core/lib/electron-main/electron-main-application');
const { Container } = require('inversify');
const { resolve } = require('path');
const { app } = require('electron');

// Fix the window reloading issue, see: https://github.com/electron/electron/issues/22119
app.allowRendererProcessReuse = false;

const config = {
    "applicationName": "Theia Blueprint",
    "defaultTheme": "dark",
    "defaultIconTheme": "none",
    "electron": {
        "windowOptions": {}
    },
    "defaultLocale": "",
    "validatePreferencesSchema": true,
    "preferences": {
        "toolbar.showToolbar": true
    }
};
const isSingleInstance = false;

if (isSingleInstance && !app.requestSingleInstanceLock()) {
    // There is another instance running, exit now. The other instance will request focus.
    app.quit();
    return;
}

const container = new Container();
container.load(electronMainApplicationModule);
container.bind(ElectronMainApplicationGlobals).toConstantValue({
    THEIA_APP_PROJECT_PATH: resolve(__dirname, '..', '..'),
    THEIA_BACKEND_MAIN_PATH: resolve(__dirname, '..', 'backend', 'main.js'),
    THEIA_FRONTEND_HTML_PATH: resolve(__dirname, '..', '..', 'lib', 'index.html'),
});

function load(raw) {
    return Promise.resolve(raw.default).then(module =>
        container.load(module)
    );
}

async function start() {
    const application = container.get(ElectronMainApplication);
    await application.start(config);
}

module.exports = Promise.resolve()
    .then(function () { return Promise.resolve(require('theia-blueprint-updater/lib/electron-main/update/theia-updater-main-module')).then(load) })
    .then(function () { return Promise.resolve(require('theia-blueprint-product/lib/electron-main/theia-blueprint-main-module')).then(load) })
    .then(start).catch(reason => {
        console.error('Failed to start the electron application.');
        if (reason) {
            console.error(reason);
        }
    });

在这个文件中定义了一个IOC容器,然后加载了@theia/core下的electronMainApplicationModule,electronMainApplicationModule是一个ContainerModule,里面绑定了ElectronMainApplication等,绑定常量THEIA_APP_PROJECT_PATH、THEIA_BACKEND_MAIN_PATH、THEIA_FRONTEND_HTML_PATH。然后加载theia-blueprint-updater和theia-blueprint-product我们自定义的扩展,然后通过container.get(ElectronMainApplication)获取ElectronMainApplication实例,调用start方法并传入config。

我们之前看到electron-main.js是由application-manager下frontend-generator.ts中FrontendGenerator生成的,其中config是由application-package读取package.json生成提供的。

//application-manager/src/generator/frontend-generator.ts
const config = ${this.prettyStringify(this.pck.props.frontend.config)};
//application-package/src/application-package.ts
get props(): ApplicationProps {
        if (this._props) {
            return this._props;
        }
        const theia = this.pck.theia || {};

        if (this.options.appTarget) {
            theia.target = this.options.appTarget;
        }

        if (theia.target && !(theia.target in ApplicationProps.ApplicationTarget)) {
            const defaultTarget = ApplicationProps.ApplicationTarget.browser;
            console.warn(`Unknown application target '${theia.target}', '${defaultTarget}' to be used instead`);
            theia.target = defaultTarget;
        }

        return this._props = deepmerge(ApplicationProps.DEFAULT, theia);
    }

    protected _pck: NodePackage | undefined;
    get pck(): NodePackage {
        if (this._pck) {
            return this._pck;
        }
        return this._pck = readJsonFile(this.packagePath);
    }

可以看出config是读取package.json中theia字段获取的。

其中theia-blueprint-updater和theia-blueprint-product也是自动生成的。他主要由application-package提供的,他会遍历package.json的依赖包,收集package.json中theiaExtensions有electronMain的依赖。theiaExtensions是我们列出导出 DI 模块的 JavaScript 模块,这些模块定义了我们扩展的绑定。

//application-manager/src/generator/frontend-generator.ts
module.exports = Promise.resolve()${this.compileElectronMainModuleImports(electronMainModules)}
    .then(start).catch(reason => {
        console.error('Failed to start the electron application.');
        if (reason) {
            console.error(reason);
        }
    });
//application-package/src/application-package.ts
get electronMainModules(): Map<string, string> {
        if (!this._electronMainModules) {
            this._electronMainModules = this.computeModules('electronMain');
        }
        return this._electronMainModules;
    }

    protected computeModules<P extends keyof Extension, S extends keyof Extension = P>(primary: P, secondary?: S): Map<string, string> {
        const result = new Map<string, string>();
        let moduleIndex = 1;
        for (const extensionPackage of this.extensionPackages) {
            const extensions = extensionPackage.theiaExtensions;
            if (extensions) {
                for (const extension of extensions) {
                    const modulePath = extension[primary] || (secondary && extension[secondary]);
                    if (typeof modulePath === 'string') {
                        const extensionPath = paths.join(extensionPackage.name, modulePath).split(paths.sep).join('/');
                        result.set(`${primary}_${moduleIndex}`, extensionPath);
                        moduleIndex = moduleIndex + 1;
                    }
                }
            }
        }
        return result;
    }

然后我们看一下ElectronMainApplication的start方法做了哪些内容。这里提及一下electron分为主进程和渲染进程,主进程负责创建窗口并加载html,而html中的代码运行在渲染进程中。

//core/src/electron-main/electron-main-application.ts

async start(config: FrontendApplicationConfig): Promise<void> {
        this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native';
        this._config = config;
        this.hookApplicationEvents();
        const port = await this.startBackend();
        this._backendPort.resolve(port);
        await app.whenReady();
        await this.attachElectronSecurityToken(port);
        await this.startContributions();
        await this.launch({
            secondInstance: false,
            argv: this.processArgv.getProcessArgvWithoutBin(process.argv),
            cwd: process.cwd()
        });
}
protected async startBackend(): Promise<number> {
        // Check if we should run everything as one process.
        const noBackendFork = process.argv.indexOf('--no-cluster') !== -1;
        // We cannot use the `process.cwd()` as the application project path (the location of the `package.json` in other words)
        // in a bundled electron application because it depends on the way we start it. For instance, on OS X, these are a differences:
        // https://github.com/eclipse-theia/theia/issues/3297#issuecomment-439172274
        process.env.THEIA_APP_PROJECT_PATH = this.globals.THEIA_APP_PROJECT_PATH;
        // Set the electron version for both the dev and the production mode. (https://github.com/eclipse-theia/theia/issues/3254)
        // Otherwise, the forked backend processes will not know that they're serving the electron frontend.
        process.env.THEIA_ELECTRON_VERSION = process.versions.electron;
        if (noBackendFork) {
            process.env[ElectronSecurityToken] = JSON.stringify(this.electronSecurityToken);
            // The backend server main file is supposed to export a promise resolving with the port used by the http(s) server.
            const address: AddressInfo = await require(this.globals.THEIA_BACKEND_MAIN_PATH);
            return address.port;
        } else {
            const backendProcess = fork(
                this.globals.THEIA_BACKEND_MAIN_PATH,
                this.processArgv.getProcessArgvWithoutBin(),
                await this.getForkOptions(),
            );
            return new Promise((resolve, reject) => {
                // The backend server main file is also supposed to send the resolved http(s) server port via IPC.
                backendProcess.on('message', (address: AddressInfo) => {
                    resolve(address.port);
                });
                backendProcess.on('error', error => {
                    reject(error);
                });
                app.on('quit', () => {
                    // Only issue a kill signal if the backend process is running.
                    // eslint-disable-next-line no-null/no-null
                    if (backendProcess.exitCode === null && backendProcess.signalCode === null) {
                        try {
                            // If we forked the process for the clusters, we need to manually terminate it.
                            // See: https://github.com/eclipse-theia/theia/issues/835
                            process.kill(backendProcess.pid);
                        } catch (error) {
                            // See https://man7.org/linux/man-pages/man2/kill.2.html#ERRORS
                            if (error.code === 'ESRCH') {
                                return;
                            }
                            throw error;
                        }
                    }
                });
            });
        }
    }

async openDefaultWindow(): Promise<BrowserWindow> {
        const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.createWindow()]);
        electronWindow.loadURL(uri.withFragment(DEFAULT_WINDOW_HASH).toString(true));
        return electronWindow;
    }
 protected async createWindowUri(): Promise<URI> {
        return FileUri.create(this.globals.THEIA_FRONTEND_HTML_PATH)
            .withQuery(`port=${await this.backendPort}`);
    }

其中hookApplicationEvents中app模块监听应用的声明周期以及ipcMain监听渲染进程的事件。调用startBackend,然后fork一个子进程执行this.globals.THEIA_BACKEND_MAIN_PATH脚本,这个脚本就是在electron-main.js中绑定的常量,值为src-gen/backend/main.js。最后调用this.launch,这个方法最终调用了openDefaultWindow方法,可以看到默认调用了createWindow和createWindowUri,createWindowUri是在electron-main.js中定义的常量为lib/index.html,createWindow最终会new一个BrowserWindow窗口,然后加载uri。这样我们的应用就启动了。

我们看一下startBackend执行的脚本src-gen/backend/main.js的内容。

//src-gen/backend/main.js
// @ts-check
const { BackendApplicationConfigProvider } = require('@theia/core/lib/node/backend-application-config-provider');
const main = require('@theia/core/lib/node/main');

BackendApplicationConfigProvider.set({
    "singleInstance": false,
    "startupTimeout": -1,
    "resolveSystemPlugins": false
});

const serverModule = require('./server');
const serverAddress = main.start(serverModule());

serverAddress.then(({ port, address }) => {
    if (process && process.send) {
        process.send({ port, address });
    }
});

module.exports = serverAddress;

其中主要通过@theia/core/lib/node/main的模块调用start加载serverModule模块,我们看一下serverModule模块的内容。

//src-gen/backend/main.js
// @ts-check
require('reflect-metadata');

// Patch electron version if missing, see https://github.com/eclipse-theia/theia/pull/7361#pullrequestreview-377065146
if (typeof process.versions.electron === 'undefined' && typeof process.env.THEIA_ELECTRON_VERSION === 'string') {
    process.versions.electron = process.env.THEIA_ELECTRON_VERSION;
}

// Erase the ELECTRON_RUN_AS_NODE variable from the environment, else Electron apps started using Theia will pick it up.
if ('ELECTRON_RUN_AS_NODE' in process.env) {
    delete process.env.ELECTRON_RUN_AS_NODE;
}

const path = require('path');
const express = require('express');
const { Container } = require('inversify');
const { BackendApplication, BackendApplicationServer, CliManager } = require('@theia/core/lib/node');
const { backendApplicationModule } = require('@theia/core/lib/node/backend-application-module');
const { messagingBackendModule } = require('@theia/core/lib/node/messaging/messaging-backend-module');
const { loggerBackendModule } = require('@theia/core/lib/node/logger-backend-module');

const container = new Container();
container.load(backendApplicationModule);
container.load(messagingBackendModule);
container.load(loggerBackendModule);

function defaultServeStatic(app) {
    app.use(express.static(path.resolve(__dirname, '../../lib')))
}

function load(raw) {
    return Promise.resolve(raw.default).then(
        module => container.load(module)
    );
}

function start(port, host, argv = process.argv) {
    if (!container.isBound(BackendApplicationServer)) {
        container.bind(BackendApplicationServer).toConstantValue({ configure: defaultServeStatic });
    }
    return container.get(CliManager).initializeCli(argv).then(() => {
        return container.get(BackendApplication).start(port, host);
    });
}

module.exports = (port, host, argv) => Promise.resolve()
    .then(function () { return Promise.resolve(require('@theia/core/lib/node/i18n/i18n-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/core/lib/electron-node/keyboard/electron-backend-keyboard-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/core/lib/electron-node/token/electron-token-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/core/lib/electron-node/hosting/electron-backend-hosting-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/core/lib/electron-node/request/electron-backend-request-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/filesystem/lib/node/filesystem-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/filesystem/lib/node/download/file-download-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/workspace/lib/node/workspace-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/process/lib/common/process-common-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/process/lib/node/process-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/terminal/lib/node/terminal-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/task/lib/node/task-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/debug/lib/node/debug-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/external-terminal/lib/electron-node/external-terminal-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/file-search/lib/node/file-search-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/metrics/lib/node/metrics-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/mini-browser/lib/node/mini-browser-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/search-in-workspace/lib/node/search-in-workspace-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/plugin-ext/lib/plugin-ext-backend-electron-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/plugin-dev/lib/node-electron/plugin-dev-electron-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/plugin-ext-vscode/lib/node/plugin-vscode-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/vsx-registry/lib/node/vsx-registry-backend-module')).then(load) })
    .then(function () { return Promise.resolve(require('theia-blueprint-product/lib/node/theia-blueprint-backend-module')).then(load) })
    .then(() => start(port, host, argv)).catch(error => {
        console.error('Failed to start the backend application:');
        console.error(error);
        process.exitCode = 1;
        throw error;
    });

其中初始化了一个IOC容器,然后加载了@theia/core下backendApplicationModule模块,使用express托管了lib下的静态文件绑定到容器中,然后遍历package.json中依赖,加载依赖项package.json中theiaExtensions下配置backend和backendElectron的DI模块,然后调用BackendApplication的start方法启动。

//@theia/core/src/node/backend-application.ts

async start(aPort?: number, aHostname?: string): Promise<http.Server | https.Server> {
        const hostname = aHostname !== undefined ? aHostname : this.cliParams.hostname;
        const port = aPort !== undefined ? aPort : this.cliParams.port;

        const deferred = new Deferred<http.Server | https.Server>();
        let server: http.Server | https.Server;

        if (this.cliParams.ssl) {

            if (this.cliParams.cert === undefined) {
                throw new Error('Missing --cert option, see --help for usage');
            }

            if (this.cliParams.certkey === undefined) {
                throw new Error('Missing --certkey option, see --help for usage');
            }

            let key: Buffer;
            let cert: Buffer;
            try {
                key = await fs.readFile(this.cliParams.certkey as string);
            } catch (err) {
                console.error("Can't read certificate key");
                throw err;
            }

            try {
                cert = await fs.readFile(this.cliParams.cert as string);
            } catch (err) {
                console.error("Can't read certificate");
                throw err;
            }
            server = https.createServer({ key, cert }, this.app);
        } else {
            server = http.createServer(this.app);
        }

        server.on('error', error => {
            deferred.reject(error);
            /* The backend might run in a separate process,
             * so we defer `process.exit` to let time for logging in the parent process */
            setTimeout(process.exit, 0, 1);
        });

        server.listen(port, hostname, () => {
            const scheme = this.cliParams.ssl ? 'https' : 'http';
            console.info(`Theia app listening on ${scheme}://${hostname || 'localhost'}:${(server.address() as AddressInfo).port}.`);
            deferred.resolve(server);
        });

        /* Allow any number of websocket servers.  */
        server.setMaxListeners(0);

        for (const contribution of this.contributionsProvider.getContributions()) {
            if (contribution.onStart) {
                try {
                    await this.measure(contribution.constructor.name + '.onStart',
                        () => contribution.onStart!(server)
                    );
                } catch (error) {
                    console.error('Could not start contribution', error);
                }
            }
        }
        return this.stopwatch.startAsync('server', 'Finished starting backend application', () => deferred.promise);
    }

通过https.createServer/http.createServer来创建服务。然后再遍历contributionsProvider并调用其onStart方法,contributionsProvider收集了BackendApplicationContribution的实现类,接口BackendApplicationContribution有四个钩子方法initialize、configure、onStart、onStop,并在BackendApplication初始化后端生命周期过程中去调用,这样我们可以去注册BackendApplicationContribution在Theia 后端启动过程中去做一些自定义操作。

这样我们的服务子进程启动完成,然后我们看一下渲染进程加载的html。

打开lib/index.html,其中引入了bundle.js,bundle.js是在gen-webpack.config.js中以src-gen/frontend/index.js为入口打包生成的。我们看一下src-gen/frontend/index.js的代码。

// @ts-check

require('reflect-metadata');
require('setimmediate');
const { Container } = require('inversify');
const { FrontendApplicationConfigProvider } = require('@theia/core/lib/browser/frontend-application-config-provider');

FrontendApplicationConfigProvider.set({
    "applicationName": "Theia Blueprint",
    "defaultTheme": "dark",
    "defaultIconTheme": "none",
    "electron": {
        "windowOptions": {}
    },
    "defaultLocale": "",
    "validatePreferencesSchema": true,
    "preferences": {
        "toolbar.showToolbar": true
    }
});


self.MonacoEnvironment = {
    getWorkerUrl: function (moduleId, label) {
        return './editor.worker.js';
    }
}


const preloader = require('@theia/core/lib/browser/preloader');

// We need to fetch some data from the backend before the frontend starts (nls, os)
module.exports = preloader.preload().then(() => {
    const { FrontendApplication } = require('@theia/core/lib/browser');
    const { frontendApplicationModule } = require('@theia/core/lib/browser/frontend-application-module');
    const { messagingFrontendModule } = require('@theia/core/lib/electron-browser/messaging/electron-messaging-frontend-module');
    const { loggerFrontendModule } = require('@theia/core/lib/browser/logger-frontend-module');

    const container = new Container();
    container.load(frontendApplicationModule);
    container.load(messagingFrontendModule);
    container.load(loggerFrontendModule);

    return Promise.resolve()
    .then(function () { return Promise.resolve(require('@theia/core/lib/browser/i18n/i18n-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/core/lib/electron-browser/menu/electron-menu-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/core/lib/electron-browser/window/electron-window-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/core/lib/electron-browser/keyboard/electron-keyboard-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/core/lib/electron-browser/token/electron-token-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/core/lib/electron-browser/request/electron-browser-request-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/variable-resolver/lib/browser/variable-resolver-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/editor/lib/browser/editor-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/filesystem/lib/browser/filesystem-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/filesystem/lib/browser/download/file-download-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/filesystem/lib/electron-browser/file-dialog/electron-file-dialog-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/workspace/lib/browser/workspace-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/markers/lib/browser/problem/problem-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/outline-view/lib/browser/outline-view-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/monaco/lib/browser/monaco-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/bulk-edit/lib/browser/bulk-edit-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/callhierarchy/lib/browser/callhierarchy-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/console/lib/browser/console-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/output/lib/browser/output-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/process/lib/common/process-common-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/terminal/lib/browser/terminal-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/userstorage/lib/browser/user-storage-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/task/lib/browser/task-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/debug/lib/browser/debug-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/navigator/lib/browser/navigator-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/navigator/lib/electron-browser/electron-navigator-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/editor-preview/lib/browser/editor-preview-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/external-terminal/lib/electron-browser/external-terminal-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/file-search/lib/browser/file-search-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/preferences/lib/browser/preference-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/keymaps/lib/browser/keymaps-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/getting-started/lib/browser/getting-started-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/messages/lib/browser/messages-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/mini-browser/lib/browser/mini-browser-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/mini-browser/lib/electron-browser/environment/electron-mini-browser-environment-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/scm/lib/browser/scm-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/timeline/lib/browser/timeline-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/plugin-ext/lib/plugin-ext-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/plugin-ext/lib/plugin-ext-frontend-electron-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/plugin-dev/lib/browser/plugin-dev-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/plugin-ext-vscode/lib/browser/plugin-vscode-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/property-view/lib/browser/property-view-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/scm-extra/lib/browser/scm-extra-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/toolbar/lib/browser/toolbar-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/typehierarchy/lib/browser/typehierarchy-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('@theia/vsx-registry/lib/browser/vsx-registry-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('theia-blueprint-updater/lib/electron-browser/theia-updater-frontend-module')).then(load) })
    .then(function () { return Promise.resolve(require('theia-blueprint-product/lib/browser/theia-blueprint-frontend-module')).then(load) })
        .then(start).catch(reason => {
            console.error('Failed to start the frontend application.');
            if (reason) {
                console.error(reason);
            }
        });

    function load(jsModule) {
        return Promise.resolve(jsModule.default)
            .then(containerModule => container.load(containerModule));
    }

    function start() {
        (window['theia'] = window['theia'] || {}).container = container;
        return container.get(FrontendApplication).start();
    }
});

前端这部分创建了一个IOC容器,然后加载了@theia/core下browser中的DI容器模块,然后遍历package.json中依赖,加载依赖项package.json中theiaExtensions下配置frontend和frontendElectron的DI模块,然后调用FrontendApplication的start方法启动。

// @theia/core/src/browser/frontend-application.ts
 
async start(): Promise<void> {
        const startup = this.backendStopwatch.start('frontend');

        await this.measure('startContributions', () => this.startContributions(), 'Start frontend contributions', false);
        this.stateService.state = 'started_contributions';

        const host = await this.getHost();
        this.attachShell(host);
        this.attachTooltip(host);
        await animationFrame();
        this.stateService.state = 'attached_shell';

        await this.measure('initializeLayout', () => this.initializeLayout(), 'Initialize the workbench layout', false);
        this.stateService.state = 'initialized_layout';
        await this.fireOnDidInitializeLayout();

        await this.measure('revealShell', () => this.revealShell(host), 'Replace loading indicator with ready workbench UI (animation)', false);
        this.registerEventListeners();
        this.stateService.state = 'ready';

        startup.then(idToken => this.backendStopwatch.stop(idToken, 'Frontend application start', []));
    }
 /**
     * Initialize and start the frontend application contributions.
     */
    protected async startContributions(): Promise<void> {
        for (const contribution of this.contributions.getContributions()) {
            if (contribution.initialize) {
                try {
                    await this.measure(contribution.constructor.name + '.initialize',
                        () => contribution.initialize!()
                    );
                } catch (error) {
                    console.error('Could not initialize contribution', error);
                }
            }
        }

        for (const contribution of this.contributions.getContributions()) {
            if (contribution.configure) {
                try {
                    await this.measure(contribution.constructor.name + '.configure',
                        () => contribution.configure!(this)
                    );
                } catch (error) {
                    console.error('Could not configure contribution', error);
                }
            }
        }

        /**
         * FIXME:
         * - decouple commands & menus
         * - consider treat commands, keybindings and menus as frontend application contributions
         */
        await this.measure('commands.onStart',
            () => this.commands.onStart()
        );
        await this.measure('keybindings.onStart',
            () => this.keybindings.onStart()
        );
        await this.measure('menus.onStart',
            () => this.menus.onStart()
        );
        for (const contribution of this.contributions.getContributions()) {
            if (contribution.onStart) {
                try {
                    await this.measure(contribution.constructor.name + '.onStart',
                        () => contribution.onStart!(this)
                    );
                } catch (error) {
                    console.error('Could not start contribution', error);
                }
            }
        }
    }
  /**
     * Attach the application shell to the host element. If a startup indicator is present, the shell is
     * inserted before that indicator so it is not visible yet.
     */
    protected attachShell(host: HTMLElement): void {
        const ref = this.getStartupIndicator(host);
        Widget.attach(this.shell, host, ref);
    }

start方法主要做了这样几件事,1、初始化并启动frontend application contributions,和BackendApplicationContribution类似在前端通常用于打开和排列视图、注册侦听器、添加状态栏项或在应用程序启动时自定义应用程序的布局,2、调用@phosphor/widgets的Widget.attach方法,将ApplicationShell布局插入到document.body中class为theia-preload的节点前,3、初始化ApplicationShell的布局,4、隐藏启动动画,展示页面。

以上就是Theia Blueprint启动的过程。

自定义配置

在上面的分析过程中,我们发现package.json中有一个theia的配置,用于自定义theia的一些配置,通过使用application-package解析生成src-gen下配置的内容。

//application-package/src/application-package.ts

get props(): ApplicationProps {
        if (this._props) {
            return this._props;
        }
        const theia = this.pck.theia || {};

        if (this.options.appTarget) {
            theia.target = this.options.appTarget;
        }

        if (theia.target && !(theia.target in ApplicationProps.ApplicationTarget)) {
            const defaultTarget = ApplicationProps.ApplicationTarget.browser;
            console.warn(`Unknown application target '${theia.target}', '${defaultTarget}' to be used instead`);
            theia.target = defaultTarget;
        }

        return this._props = deepmerge(ApplicationProps.DEFAULT, theia);
}

protected _pck: NodePackage | undefined;
get pck(): NodePackage {
        if (this._pck) {
            return this._pck;
        }
        return this._pck = readJsonFile(this.packagePath);
}

我们配置的属性最终合并到ApplicationProps中,我们看一下定义:

export interface ApplicationProps extends NpmRegistryProps {

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    readonly [key: string]: any;

    /**
     * Whether the extension targets the browser or electron. Defaults to `browser`.
     */
    readonly target: ApplicationProps.Target;

    /**
     * Frontend related properties.
     */
    readonly frontend: {
        readonly config: FrontendApplicationConfig
    };

    /**
     * Backend specific properties.
     */
    readonly backend: {
        readonly config: BackendApplicationConfig
    };

    /**
     * Generator specific properties.
     */
    readonly generator: {
        readonly config: GeneratorConfig
    };
}
  • target:打包平台的类型,browser(浏览器)和electron(桌面端)
  • frontend:前端相关属性
  • backend:后端特定属性
  • generator:生成器特定属性
frontend配置

前端相关属性主要包括以下内容:

export const DEFAULT: FrontendApplicationConfig = {
        applicationName: 'Eclipse Theia',
        defaultTheme: 'dark',
        defaultIconTheme: 'none',
        electron: ElectronFrontendApplicationConfig.DEFAULT,
        defaultLocale: '',
        validatePreferencesSchema: true
};
  • applicationName:应用名称
  • defaultTheme:默认主题
  • defaultIconTheme: 默认文件图标主题
  • electron:electron配置,主要是BrowserWindow的相关配置
  • defaultLocale:默认区域语言
  • validatePreferencesSchema:是否在启动时验证首选项的JSON模式
backend配置
export const DEFAULT: BackendApplicationConfig = {
        singleInstance: false,
    };

electron模式下,如果为true则一次只允许运行应用程序的一个实例。

generator配置
export const DEFAULT: GeneratorConfig = {
        preloadTemplate: ''
};

preloadTemplate用于自定义启动页面模板的文件路径。

以上就是自定义配置的相关内容。

从已有 ALE 架构里找出 RFC Destination 和 System User,CUA 改造前最容易被忽略的一步 摘要: 在SAP多系统用户治理中,Central User Administration(CUA)的实施需谨慎处理RFC Destination和System User的复用问题。已配置ALE分发的系统应优先复用现有技术用户,而非新建,以避免破坏稳定的分发链路。通过SM59检查RFC Destination的System User,并确保其授权符合CUA要求,而非盲目替换或赋予过高权限。遵循SAP建议的命名规范(如CUA_<SID>_<Client>),同时兼顾历史遗留配置的兼容性, 阅读详情

相关推荐

锐捷路由器DNS Proxy保姆级配置教程:从零到实战,手把手教你搞定内网域名解析

本文提供锐捷路由器DNS Proxy功能的详细配置教程,从基础环境搭建到实战测试,手把手教你实现内网域名解析。涵盖上游DNS设置、内网域名映射、故障排查等关键步骤,帮助网络管理员快速解决内网访问难题,提升工作效率。

weixin_29284657的博客 554

Eclipse Theia技术揭秘——初识Theia

自从接触IDE这方面的技术以来,一直关注其他厂商是如何做IDE的,刚开始了解IDE是从微信、百度、支付宝等这些开发者工具知道这些开发者工具是基于electron或NW.js去构建的,但是从头开始写成本比较高。后来也看了一些其他的IDE,像、白鹭Egret Wing、快应用IDE、这类的是基于VSCode源码定制的,技术相对成熟,大部分功能现成的,工作量相对较少,于是就基于VSCode去定制了第一版的开发工具,详情可以看之前写的VSCode技术揭秘。

2207

10-142 6-4 查询厂商“D“生产的PC和便携式电脑的平均价格

分数 10全屏浏览题目切换布局作者 lhm单位 河北农业大学本题目要求编写SQL语句,查询厂商D生产的PC和便携式电脑的平均价格。

dqd66的博客 2748

Eclipse Theia 揭秘之技术架构篇

Cloud IDE 随着前端开发的发展更迭,前端日常开发工作变得愈发复杂愈发深入,同时前端工程中从项目初始化、编译、构建到发布、运维也变得细化而成熟。本地开发环境存在开发机性能要求高、开发环境配置复杂、依赖特定设备、复杂工程管理难等问题,Cloud IDE 很好的解决了这些问题。最近几年,Cloud IDE 在开发全流程领域扮演者越来越重的角色,国内外很多厂商都在做 Cloud IDE。 Coding:国内的云 IDE 产品 Cloud9:亚马逊为其云计算服务提供的 IDE Eclipse Theia.

lannister_awalys_pay的博客 3647

CMOS反相器版图设计全解析:PMOS/NMOS布局与VDD/VSS连接门道

版图设计是芯片从电路原理图迈向物理实现的必经桥梁,而CMOS反相器作为最基础的数字单元,恰好是理解这一过程的最佳切入点。在版图绘制中,PMOS与NMOS因导电类型和迁移率差异,需要在N阱与P衬底上分别规划,并呈现出不同的有源区尺寸。真正决定版图好坏的关键,往往在于VDD/VSS的完整连接——不仅要接通源漏,还要通过阱接触和衬底接触固定阱与衬底电位,防止寄生PN结正偏和闩锁效应。合理布局电源地线轨道、优化接触孔数量,能显著降低寄生电阻与电迁移风险。这些原理不仅适用于反相器,也是所有CMOS数字标准单元版图设计

weixin_29159441的博客 302

KBS投稿全流程拆解:从模板准备到条件接收的实战指南

本文详细拆解了KBS期刊投稿的全流程,从模板准备、投稿系统操作到返修策略和条件接收。重点介绍了Cover Letter撰写技巧、Latex模板选择、审稿周期管理以及返修文件的系统准备方法,帮助研究者高效完成KBS投稿并提升接收概率。

weixin_26938645的博客 703

eclipse宝刀未老

Theia 是一个高度可定制的、开源的、基于 Web 的集成开发环境(IDE)框架。它由 Eclipse Foundation 主导,旨在为云和本地环境提供现代化的、全功能的 IDE 解决方案。Theia 的核心目标是提供一个灵活的平台,开发者可以根据自己的需求构建和扩展 IDE。

svygh123的专栏 978

USB2.0基础知识总结

目录 1、协议层: 1.1、域分类 1.2 、USB传输方式: 1、协议层: 域和包定义:包是一次传输的单位,每个包由多个域组成; USB中是按照LSB进行字节传输; 1.1、域分类 1.1.1: 同步域(SYNC field) 所有的包都开始于同步域,在低速和全速通信中SYNC field用7个0和1个1来表示。 由于USB2.0使用了NRZI编码(0用翻转来表示,1用不翻转表示),所以00000001就是上图那样。 ...

qq_32938605的博客 4408

Eclipse Theia技术揭秘——自定义布局

以上介绍了ApplicationShell的组成和布局,那么我们要扩展一个toolbar或者simulator也就简单了,只需重写ApplicationShell的createLayout方法,添加自己定义的视图,然后使用inversify重新绑定即可。其实官方提供了一个@theia/toolbar的模块,也是按上述的方法去重写的。效果如图:});});} });};

1364

4、分布式发电系统中用于改善电能质量的有源、无源和混合滤波器

本文探讨了分布式发电系统中,尤其是光伏(PV)系统运行过程中引入的电能质量问题,并分析了有源滤波器、无源滤波器及混合滤波器在改善电能质量方面的应用。文章详细介绍了光伏系统的建模、最大功率点跟踪(MPPT)技术,以及谐波、电压波动等技术问题,同时对各类滤波器的拓扑结构和控制方法进行了系统性总结。通过仿真实例比较了不同滤波器在减少谐波方面的效果,为实际工程应用提供了参考依据。

transformer2023的博客 89

在 SAP S/4HANA 分析模型中添加 Measures,Calculated、Restricted、Currency Conversion 与 Unit Conversion 的建模思路

摘要: SAP S/4HANA分析查询建模中,度量(measure)的加工层级是关键难点,涉及聚合时机、币种/单位语义、过滤上下文等复杂因素。度量不仅是数字字段,还需处理计算口径(Calculated Measures)、上下文绑定(Restricted Measures)和单位转换等问题。Calculated Measures适合派生指标(如毛利率),需注意聚合前后计算差异;Restricted Measures通过固化过滤条件(如区分实际/计划金额)确保业务语义一致性。建模时应将业务逻辑下沉到分析模型层

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 1022

攻克《Rude-Java》实战难题:10大核心场景解决方案全解析

你是否在整合SSM框架时被配置文件搞得晕头转向?是否在实现JWT认证时频繁遭遇Token无效的诡异问题?是否在处理文件上传时反复出现"文件不存在"的异常提示?作为一个汇集了SSM、Spring Boot、Spring Security等主流技术栈的实战项目集合,《Rude-Java》为开发者提供了丰富的学习案例,但在实际运行和二次开发过程中,这些案例往往会暴露出各种"水土不服"的问题。 本文将聚...

gitblog_01130的博客 775

Spring Boot:如何快速搭建一个高性能微服务架构?

通过本文的详细步骤,我们成功搭建了一个高性能的微服务架构。Spring Boot 和 Spring Cloud 提供了强大的工具链,使得开发者能够快速实现服务注册与发现、负载均衡、熔断器与降级机制等功能。同时,通过数据库连接池、Redis 缓存和异步处理等优化手段,进一步提升了系统的性能和可靠性。希望本文能够帮助你快速入门 Spring Boot 微服务架构的搭建,并为实际项目提供参考。如果你有任何问题或建议,欢迎在评论区留言!

shrgegrb的博客 1013

跟我学Shiro目录贴

看到一个Shiro的整合贴,感谢原作者。 历经三个月左右时间,《跟我学Shiro》系列教程已经完结,暂时没有需要补充的内容,因此生成PDF版供大家下载。最近项目比较紧,没有时间解答一些疑问,暂时无法回复一些问题,很抱歉,不过可以加群(334194438/348194195)一起讨论问题。       点击下载《跟我学Shiro》教程PDF版。学习交流使用,请勿用于其他任何商业

楷楷的博客 503

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

拆解万套毕设合集:以冷链监控平台为例,从源码复现到创新改造

毕业设计是计算机专业学生综合运用所学知识的关键实践,其核心在于理解系统架构、掌握开发流程并实现业务逻辑。从技术原理上看,一个典型的Web应用通常遵循MVC或前后端分离架构,涉及数据库设计、API接口开发与前端交互。其技术价值在于将理论知识转化为解决实际问题的工程能力,尤其在物联网、大数据等热门领域,能够锻炼学生的全栈开发思维。在应用场景上,管理系统、电商平台和物联网监控系统是常见的毕设选题方向,它们要求开发者具备需求分析、模块设计与系统集成能力。本文以“冷链监控平台温控系统”这一物联网与Web开发结合的典型

chaoyv 683

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

Theia是由Eclipse基金会发起的开源项目,被称为Visual Studio Code(VS Code)的真正开源替代品

• 如果你只是想要一个开箱即用、插件丰富的桌面编辑器,请继续用 VS Code。 • 如果你需要“100 % 可控、可品牌定制、可同时跑在云端与桌面”的 IDE 产品,或者希望把开发环境嵌入到自己 SaaS/内网平台里,那么 Eclipse Theia 是目前唯一满足“开源 + 可白牌 + VS Code 级体验”的选择。

[Blog][Domain] programb.blog.youkuaiyun.com 1017

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

IDE(eclipse theia)

关于编辑器,自己之前也使用过多款产品,包括从写C开始使用的visual studio、到后面写网页应用使用webstrom、以及后来更轻量的vscode。

或许,有的梦可以被实现。 679

探索未来开发环境:Eclipse Theia 终极指南

你是否正在寻找一款功能强大、跨平台的云原生IDE?Eclipse Theia正是这样一个革命性的开发环境框架,它正在重新定义现代软件开发的体验!✨ ## 什么是Eclipse Theia? Eclipse Theia是一个基于TypeScript实现的云和桌面IDE框架,它提供了与VS Code相似的开发体验,同时具备更强的可定制性和扩展性。作为开源项目,Theia让开发者能够构建属于自己的专

gitblog_00045的博客 770

Eclipse Theia:Eclipse的继承者?

Theia在很多方面都已经追赶上Eclipse Desktop Platform。

优快云资讯 4259

Eclipse Theia调试与测试:完整的开发工作流指南

Eclipse Theia是一款基于TypeScript开发的现代化云桌面IDE框架,为开发者提供了完整的调试和测试解决方案。本文将详细介绍如何在Theia中设置调试环境、使用断点、监视变量以及运行自动化测试,帮助你构建高效的开发工作流。 ## 🎯 Eclipse Theia调试功能详解 Eclipse Theia提供了强大的调试功能,让你能够轻松定位和修复代码中的问题。调试是开发过程中不可

gitblog_00459的博客 741

【亲测免费】 探索未来开发的新境界:Eclipse Theia IDE框架

探索未来开发的新境界:Eclipse Theia IDE框架 【免费下载链接】theia 项目地址: https://gitcode.com/gh_mirrors/the/theia ...

gitblog_00834的博客 779

Eclipse Theia 开发环境搭建与调试指南

Eclipse Theia 开发环境搭建与调试指南 【免费下载链接】theia Eclipse Theia is a cloud & desktop IDE framework implemented in TypeScript. ...

gitblog_00767的博客 620

ubuntu搭建Eclipse Theia

Eclipse Theia 是一个开源的、可扩展的基于Web的集成开发环境(IDE)。它由Eclipse Foundation支持,旨在提供一个通用的工具平台,可以用于构建各种类型的云端IDE、桌面IDE或者嵌入式IDE。Ubuntu版本:Ubuntu 22.04.5 LTS。

lxiaopu的博客 1438

从create-vite源码看前端脚手架

create-vite源码解读,prompts用法,脚手架搭建

weixin_40756663的博客 996

Eclipse Theia 揭秘之启动流程篇

前言 在《Eclipse Theia 框架技术架构揭秘》一文中简单介绍了 Theia 框架整体的技术架构,接下来将通过系列文章从源码角度入手看一下 Theia 核心设计思路,本文从启动流程入手开始介绍 Theia 的运行原理。 基础 示例代码结构 根据Build your own IDE的文档创建示例工程,工程结构如下: ├── lib # 构建生成的 Bundle 包 ├── plugins # package.json 中 ..

lannister_awalys_pay的博客 3101

10分钟快速上手Eclipse Theia:构建你的第一个云端IDE

想要快速构建自己的云端IDE吗?Eclipse Theia这个开源框架能让你在10分钟内搭建起专业的云端开发环境!🚀 作为一款基于现代Web技术的云原生IDE框架,Theia提供了与VS Code相似的开发体验,同时支持浏览器和桌面端部署。 ## 什么是Eclipse Theia? Eclipse Theia是一个**可扩展的云端IDE框架**,让你能够开发功能完整的多语言云端和桌面IDE工

gitblog_00844的博客 1022
上一篇: Eclipse Theia技术揭秘——构建桌面IDE
下一篇: Eclipse Theia技术揭秘——自定义布局
全栈大师兄
博客等级 码龄11年 448粉丝 175原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值