随着前端开发的发展更迭,前端日常开发工作变得愈发复杂愈发深入,同时前端工程中从项目初始化、编译、构建到发布、运维也变得细化而成熟。本地开发环境存在开发机性能要求高、开发环境配置复杂、依赖特定设备、复杂工程管理难等问题,Cloud IDE 很好的解决了这些问题。最近几年,Cloud IDE 在开发全流程领域扮演者越来越重的角色,国内外很多厂商都在做 Cloud IDE。
Cloud IDE 主要包含 Client、Server、Container Pool 三部分。
三者之间最典型的架构如下图所示:
Eclipse Theia 是一个可扩展的平台,基于现代 Web 技术(TypeScript, CSS 和 HTML)实现,用于开发成熟的、多语言的云计算和桌面类的理想产品。
Theia 为开发浏览器和桌面 IDE 提供了可扩展的平台,主要特性:
目前 Theia 社区已经有一些产品基于 Theia 构建:
根据 Stack Overflow 2019 年 Developer Survey Results 中流行的开发者工具排行榜,VS Code 无疑是最流行的开发者工具。
VS Code 提供了开箱即用的产品,借助强大的插件生态实现下载各种增强功能的 VS Code Extensions,例如对各种语言的支持;而 Theia 是一个构建 IDE 和工具的框架,当然 Theia 也提供了多种启动方法,下文将详细介绍。
对比指标 | VS Code Product | VS Code Project | Eclipse Theia |
可用性概念 | ✔✔ | ✔✔ | ✔✔ |
技术栈和架构 | ✔✔ | ✔✔ | ✔✔ |
可扩展性和适应性 | ? | ✔ | ✔✔ |
可用的扩展和技术 | ✔✔ | ✔ | ? |
在线/离线功能 | ✔ | ✔ | ✔✔ |
开源 | ✖ | ✔✔ | ✔✔ |
生态系统和厂商无关性 | ? | ✔ | ✔✔ |
可用性概念:VS Code 主要关注代码,在用户界面中占用的功能非常少,它的可用性概念是围绕使用键盘,而不是鼠标。许多特性只能通过CLI或命令面板获得。Theia 和 VS Code 使用相同的代码编辑器(Monaco)、相同的窗口管理和非常相似的命令面板,甚至大多数默认的快捷方式都是相同的。
技术栈和架构:VS Code 和 Eclipse Theia 都基于 Web 技术,包括 TypeScript,HTML5,CSS 以及 Node.js。实际上 Theia 复用了 VS Code 大量的技术,例如结合 Monaco code editor 和 language server protocol (LSP) 的方式,通过 LSP 允许以标准化的方式将代码编辑器的 UI 特性与底层逻辑解耦。
可扩展性和适应性:VS Code 通过用户可安装的 VS Code Extensions 提供了一种扩展机制,提供一套内置的 API 用于扩展 VS Code。Eclipse Theia 提供了一个类似的扩展机制,但称之为 Theia Plugins。Theia 实现了相同的API,所以许多 VS Code 扩展也可以在 Theia中 使用(参见《如何在 Theia 中安装 VS Code 扩展》的文章)。简而言之,VS Code Extensions 和 Theia Plugins 在概念上几乎是一样的。通过内置的 API,可以很容易开发一个VS Code 拓展,拓展运行在特定的进程中可以很好的保证产品稳定性,以及允许在运行时安装。这种扩展模式促进了 VS Code 生态,但是缺点是只能基于公开的 API 进行拓展。虽然涵盖了许多用例,例如添加命令,视图或扩展代码编辑,由于无法预见,因此很难进行其他一些修改。Theia 官方说插件克服了 VS Code 拓展的一些限制,提供了更强大的扩展机制。Theia 作为一个平台,完全由 Theia 扩展组成,这些扩展通过依赖注入进行连接,这使得 Theia 非常灵活,你可以删除任何你不喜欢的东西,调整或扩展平台本身所触及的一切。后面我们再专门探究一下 Theia 拓展的原理。
可用的扩展和技术:Eclipse Theia 支持 VS Code 扩展 API,也可以在 Theia 中使用VS代码扩展,不过有两个限制条件:Theia 还没有覆盖 VS Code 定义的全部 API,存在部分不可用的情况,API 覆盖率可以参考 vscode-theia-comparator;VS Code 插件市场的使用条款限制插件,推荐从项目主页下载。
在线与离线功能:最开始 VS Code 和 Theia 的区别就是在线和离线的区别,Theia 的核心架构明确允许将产品部署为桌面应用程序,并通过浏览器在云中访问。目前 VS Code 和 Theia 都支持在线和离线功能。
开源:VS Code Project 是开源的,但是 VS Code 产品不是开源的,VS Code Project 基于 MIT 协议;Theia 是基于 Eclipse Public License (EPL) 协议。最大的限制是只允许 VS Code 产品连接 VS Code 拓展市场,其他产品不能直接使用 VS Code 拓展市场的服务。
生态系统和三方库独立性:Theia 复用了 VS Code 生态的大量代码,所以整体代码数量上相对少一些。整体上讲相比 Theia,VS Code 生态更强大,Theia 的优势在于由 Eclipse 基金会托管,众多 Eclipse 成员公司参与 Eclipse Theia 的贡献。
整体上来说,如果我们想开发一个侧重代码的工具让更多的人在现有的 IDE 中使用,VS Code 拓展或许是更好的选择;如果是需要提供比编辑器更多功能的定制 IDE,使用 Theia 是更好的选择。
上面我们说了 VS Code 和 Theia 的区别,相信大家即使没有看过 VS Code 源代码,也通过 VS Code 产品包体验过 VS Code 的功能,那么怎么安装并启动 Theia 呢?Theia 提供了多种启动方式:
1.基于自定义 package.json 构建包启动
由于 Theia 及其扩展是 Node.js 包,而 Theia 应用程序是包的集合,因此启动 Theia 及所选扩展的一个非常简单的方法就是创建一个 package.json。
参考文档:Build your own IDE
2.基于预配置的 Docker Image 构建
3.从源代码克隆、构建和运行
参考文档:How to build Theia and the example applications
4.基于 Eclipse Che(托管运行时和工作区) 构建
如果你不想下载、部署或编译任何东西,而只想以托管的方式尝试 Eclipse Theia,那么您可以使用 Eclipse Che 来实现这一点。Eclipse Che 提供了一个工作空间服务器,即服务器,它可以承载一个或多个开发人员的开发环境。从版本 Eclipse Che 7 开始,Eclipse Theia 是 Eclipse Che 的默认 IDE (在 Che 中称为编辑器)。
参考文档:Eclipse Che vs. Eclipse Theia、How to install/run/try Eclipse Che
对于新手而言,推荐第一种方法方式去了解启动一个 Theia Application 工程,整体过程类似于一个普通的 Node 工程,本文不做赘述。
Theia 被设计为一个可以在本地运行的桌面应用程序,也可以在浏览器和远程服务器之间工作。为了支持这两种工作方式,Theia 运行在两个独立的进程中,它们被称之为前端和后端,相互之间通过 WebSockets 上的 JSON-RPC 消息或 HTTP 上的 REST APIs 来通信。对于 Electron 而言,前端和后端都在本地运行,而在远程上下文中,后端运行在远程服务器上。前端和后端进行都有它们各自的依赖注入 (DI) 容器以方便开发者进行扩展。
前端
前端部分负责客户端的UI呈现。在浏览器中,它只是简单地在渲染循环中运行,而在 Electron 中,它运行在 Electron BrowserWindow 中,BrowserWindow 是包含 Electron 和 Node.js APIs 的浏览器窗口。因此,任何前端代码都可以把浏览器而不是 Node.js 作为一个运行平台。
启动前端进程将首先加载所有扩展包的 DI 模块,然后获取一个 FrontendApplication 的实例并在上面调用 start()。
const { Container } = require('inversify');
// 应用前端部分配置
FrontendApplicationConfigProvider.set({
"applicationName": "Theia"
});
// IoC 容器
const container = new Container();
container.load(frontendApplicationModule);
container.load(messagingFrontendModule);
container.load(loggerFrontendModule);
function load(raw) {
return Promise.resolve(raw.default).then(module =>
container.load(module)
)
}
function start() {
(window['theia'] = window['theia'] || {}).container = container;
const themeService = ThemeService.get();
themeService.loadUserTheme();
const application = container.get(FrontendApplication);
return application.start();
}
module.exports = Promise.resolve()
.then(function () { return Promise.resolve(require('@theia/core/lib/browser/menu/browser-menu-module')).then(load) })
...
.then(start).catch(reason => {
console.error('Failed to start the frontend application.');
if (reason) {
console.error(reason);
}
});
后端
后端进程运行在Node.js上。我们使用express作为HTTP服务器,它可以不使用任何需要浏览器平台的代码(DOM API)。
启动后端应用程序将首先加载所有扩展包的DI模块,然后获取一个 BackendApplication 的实例并在上面调用 start(portNumber)。 默认情况下后端的 Express 服务器也为前端提供代码。
require('reflect-metadata');
const path = require('path');
const express = require('express');
const { Container } = require('inversify');
const { BackendApplication, 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 load(raw) {
return Promise.resolve(raw.default).then(module =>
container.load(module)
)
}
function start(port, host, argv) {
if (argv === undefined) {
argv = process.argv;
}
const cliManager = container.get(CliManager);
return cliManager.initializeCli(argv).then(function () {
const application = container.get(BackendApplication);
application.use(express.static(path.join(__dirname, '../../lib')));
application.use(express.static(path.join(__dirname, '../../lib/index.html')));
return application.start(port, host);
});
}
module.exports = (port, host, argv) => Promise.resolve()
.then(function () { return Promise.resolve(require('@theia/process/lib/node/process-backend-module')).then(load) })
...
.then(() => start(port, host, argv)).catch(reason => {
console.error('Failed to start the backend application.');
if (reason) {
console.error(reason);
}
throw reason;
});
按平台进行区分
在扩展包的根目录下,包含如下子目录层级,按不同的平台进行区分:
Theia 支持三种部署模式。
1.Web Client, Remote Back-end (Cloud IDE)
前端是从远程服务器提供给本地浏览器的,并连接到远程后端。
2.Native Front-End, Local Back-end
基于Electron,IDE可以在本地运行前端和后端。
3.Native Front-End, Remote Back-end
基于Electron,只有前端会在本地运行,并连接到远程后端。
下图说明了主要组件及其连接方式:
依赖的组件:
模块名 | 版本 | 开源协议 |
electron | 1.6.2 | MIT |
express | 4.15.2 | MIT |
inversify | 3.1.0 | MIT |
monaco-editor-core | 0.8.2 | MIT |
monaco-editor | 0.8.3 | MIT |
monaco-languageclient | 0.0.1-alpha.2 | MIT |
ws | 2.2.0 | MIT |
reconnecting-websocket | 3.0.3 | MIT |
@phosphor/application | 0.1.5 | BSD-3-Clause |
@phosphor/algorithm | 0.1.1 | BSD-3-Clause |
@phosphor/domutils | 0.1.2 | BSD-3-Clause |
@phosphor/messaging | 0.1.2 | BSD-3-Clause |
@phosphor/signaling | 0.1.2 | BSD-3-Clause |
@phosphor/virtualdom | 0.1.1 | BSD-3-Clause |
@phosphor/widgets | 0.1.7 | BSD-3-Clause |
reflect-metadata | 0.1.10 | Apache-2.0 |
vscode-ws-jsonrpc | 0.0.1-alpha.1 | MIT |
vscode-languageserver | 3.2.0 | MIT |
Theia 由扩展包构成,前端应用程序和主后端应用程序均包含多个扩展。 一个 npm 软件包可以公开一个或多个扩展,这些扩展可以被前端和主后端应用程序使用。一个扩展包就是一个 npm 包,在这个 npm 包中公开了用于创建 DI 容器的多个 DI 模块 (ContainerModule) 。通过在应用程序的 package.json 中添加 npm 包的依赖项来使用扩展包,扩展包能够在运行时安装和卸载,这将触发重新编译和重启。通过 DI 模块,扩展包能提供从类型到具体实现的绑定,即提供服务和功能。
依赖注入(DI)
DI 在 Theia 中是一个非常重要的部分,Theia 使用DI框架 Inversify.js 来连接不同的组件。DI 在创建时注入组件(作为构造函数的参数),从而将组件从依赖项中彻底解耦出来。DI 容器根据你在启动时通过所谓的容器模块提供的配置项来进行创建。
例如,Navigator 小部件需要访问 FileSystem 用来在树形结构中显示文件夹和文件,但是 FileSystem 接口的实现对 Navigator 来说并不重要,它可以大胆地假设与 FileSystem 接口一致的对象已经准备好并可以使用了。在 Theia 中,FileSystem 的实现仅仅是一个发送 JSON-RPC 消息到后端的代理,它需要一个特殊的配置和处理程序。Navigator不需要关心这些细节,因为它将获取一个被注入的 FileSystem 的实例。此外,这种结构的解耦和使用,允许扩展包在需要时能提供非常具体的功能实现,例如这里提到的 FileSystem,而不需要接触到 FileSystem 接口的任何实现。
Services
Service 只是一个提供给其它组件使用的绑定。一个扩展包可以公开 SelectionService,这样其它扩展包就可以获得一个注入的实例并使用它。
Contribution-Points
如果一个扩展包想要提供 hook 由其它扩展包来实现其中的功能,那么它应该定义一个 contribution-point。一个 contribution-point 就是一个可以被其它扩展包实现的接口。扩展包可以在需要时将它委托给其它部分。例如,OpenerService 定义了一个 contribution-point,允许其它扩展包注册 OpenHandler,你可以查看 这里 的代码。Theia 已经提供了大量的 Contribution Points 列表,查看已存在的 Contribution Points 的一个好方法是查找 bindContributionProvider 的引用。
Contribution Providers
一个 Contribution Provider 基本上是 Contributions 的容器,其中的 Contributions 是绑定类型的实例,要将类型绑定到 Contribution Provider,你可以这样做:
// messageing-module.ts
export const messagingModule = new ContainerModule(bind => {
bind(BackendApplicationContribution).to(MessagingContribution);
bindContributionProvider(bind, ConnectionHandler)
});
最后一行将一个 ContributionProvider 绑定到一个包含所有 ConnectionHandler 绑定实例的对象上。像这样使用:
// messageing-module.ts
constructor(@inject(ContributionProvider) @named(ConnectionHandler) protected readonly handlers: ContributionProvider) {
}
这里我们注入了一个 ContributionProvider,它的 name 值是 ConnectionHandler,这个值之前是由 bindContributionProvider 绑定的。这使得任何人都可以绑定 ConnectionHandler,现在,当 messageingModule启动时,所有的 ConnectionHandlers 都将被初始化。
Theia Services 的依赖注入机制借鉴于 VS Code,Contributions 机制借鉴于 Eclipse。从整体设计上看 Theia 的可拓展性更强大,后面我们再详细分析 Theia 的拓展机制原理。