写点什么

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

作者:龙之幽谷
  • 2022 年 9 月 28 日
    北京
  • 本文字数:28746 字

    阅读完需:约 94 分钟

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

在之前的文章中,我们介绍了 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 noderequire('../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 始终是{name}. 例如:vscode.csharp,extensionDependencies 是具有此扩展所依赖的扩展 ID 的数组。扩展的 id 始终是{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.jsconst 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-439689082require('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/22119app.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.tsconst config = ${this.prettyStringify(this.pck.props.frontend.config)};
复制代码


//application-package/src/application-package.tsget 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.tsmodule.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.tsget 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-checkconst { 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-checkrequire('reflect-metadata');
// Patch electron version if missing, see https://github.com/eclipse-theia/theia/pull/7361#pullrequestreview-377065146if (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 用于自定义启动页面模板的文件路径。


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


看完觉得对您有所帮助别忘记关注呦


发布于: 刚刚阅读数: 5
用户头像

龙之幽谷

关注

还未添加个人签名 2018.02.21 加入

还未添加个人简介

评论

发布
暂无评论
Eclipse Theia技术揭秘——脚手架源码分析_开发工具_龙之幽谷_InfoQ写作社区