构建IDE产品的方案和选型

IDE即集成开发环境(integrated development environment),在开发者成果输出中扮演这重要而不可或缺的角色,是开发者生产力的辅助性工具,伴随开发者对成果输出的整个过程——原型、编码、调试、测试、集成、交付等。

序言

对前端领域而言,最广为人知的IDE工具非vscode莫属了,免费、开源、可扩展是vscode最中重要的三个标签,而且加之微软厂牌在开发工具领域独特的优势地位的加持,使得vscode成为前端开发领域的佼佼者,乃至扩展至整个软件研发领域都占有一席之地。

vscode

vscode从2011年为WebIDE探索以monaca浏览器编辑器起家,在azure云中发展,在微软拥抱开源的政策之后与electron(当时的atom)结合而成为Visual Studio Code占领了桌面端,2020年vscode团队重回初心,将vscode的支持从桌面端拉回浏览器,成为一款真正意义的WebIDE。

vscode在2022年的统计数据中,有超过2000万人使用,其插件市场托管了超过34K款能力各异的插件,是当之无愧的IDE界最强生态系统。

vscode的成功引来了众多瞩目的目光,尤其是对于致力于构建跨平台研发工具的企业,从vscode的成功中看到了独立构建的曙光,一款高效、快捷、多端、低成本的IDE解决方案也成长起来,其中最受瞩目的莫过于theia

theia

theia托管于eclipse基金会,和鼎鼎大名的eclipse是一母之后。发布于2014年的Eclipse Che for java,并经过了多次的重构优化,在2017年开源发布更名为theia,紧接着在2019年发布了稳定的1.0版本;theia的与众不同在于,它并不是一款和vscode直接竞争的IDE工具,而是一套IDE构建的框架和解决方案,可以让任何的个人或者企业可以基于theia完成自由开发工具的研发流程,提供满足差异性市场要求的IDE产品。

theia充分借鉴了vscode成功的要素,那就是架构分离:将产品拆分为内核(core)和插件(plug-in),由内核完成对操作系统的隔离,由插件完成对功能的定义。还不仅于此,巧妇难为无米之炊,theia一开始就定位于借助vscode庞大的插件市场生态,使得大量的vscode插件不必任何改动就可以摇身一变成为theia插件,这实在是天大的好处,给IDE开发者打开了一扇“通过拼凑构建IDE的方案”。为达成这一点,theia在内核中支持了LSP(语言服务协议)、DAP(调试适配协议),甚至扩展点的定义和名称都一字不差的照搬了过来。有了这一套东西,基于theia构建IDE工具也就变成不那么天方夜谭的事情。

opensumi

相对于国外市场对IDE工具、框架的万众瞩目和异军突起,与theia、vscode成长伴随的同时,国内的厂商也在慢慢的磨砺手中的刀剑,2019年阿里巴巴对发发布了一款名为KAITIAN(开天)的技术,展示了其IDE构建的能力;在支付宝小程序的开发工具上,既是KAITIAN扩展性和定制性能力的投射;KAITIAN的磨砺延宕至2022年,3月不起眼的一天,KAITIAN摇身一变为opensumi对外做了开源发布。

opensumitheia同属IDE构建框架的阵营,为企业、个人构建定制化研发工具提供了行之有效的解决方案,且都和vscode有着千丝万缕的联系,本文将基于opensumitheiavscode的能力范畴,详尽深入的探寻其在IDE构建领域的相似与不同,为IDE解决方案的选型提供一些参考。

代码工程

IDE工具的构建是复杂性工程,在编码层面降低架构复杂度,提供可读性和可维护性也就成为了重中之重,对使用typescript语言而不是javascript语言的选择是为了达成这一目标的重要决策,typescript的类型化、静态化的能力使得语言没有失去灵活性的同时提高了健壮性、稳定性和可读性,在vscode的2021年周年总结中,创始人就曾着重的讨论迁移到typescript对vscode的多次重构的变化而起到的重要作用,这也就不难理解opensumitheia纷纷以typescript为主要编码语言的原因了。

依赖注入

typescript将面向对象的思维引入了前端编程,继承、多态的存在提高了代码复用的效率,也降低了维护的成本。但也正如其他面向对象语言所面临的情况,在应用中管理大量对象,对工厂模式或者单例模式的使用,其中类型(对象)依赖的管理成为高复杂度的任务,在这一点上,三者有着各自不同的解决方案。

依赖注入是三者架构实现的核心,使得开发者不再关心那些对象实例化的细节,对象由“容器”实例化,在注册器中进行创建和管理,高效的解藕了对象的定义和使用。

opensumi中依赖注入借助于@opensumi/di完成。@opensumi/di是基于 Angular DI 的设计自行实现的一个依赖注入的工具,通过使用注解(装饰器)在编译时完成依赖声明的生成,在运行时通过依赖声明来装配。

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Dog { //声明类型:狗
bark(): void;
}

@Injectable()
class Zoo {
@Autowired('Animal')
dog: Dog //注入类型:狗

zooTalk() {
dog.bark(); // 调用
}
}

在运行时,从容器中获取Zoo时,容器会自动将Dog装配到Zoo并返回Zoo的一个可用实例。

1
2
3
4
5
injector.addProviders(Dog)
injector.addProviders(Zoo)

const zoo = injector.get(Zoo);
zoo.zooTalk();

@opensumi/di支持ClassProvider(用于class声明有构造函数的类型) 、TypeProvider(用于注入function时)、ValueProvider(用于注入值类型),FactoryProvider(用于注入工厂函数)。在opensumi的代码当中,随处可见存在大量的依赖注入调用,对于扩展点(Contribution)的实现广泛采用了@Injectable来注解。

1
2
3
4
5
@Injectable()
@Domain(ClientAppContribution, CommandContribution)
export class StatusBarContribution extends WithEventBus implements ClientAppContribution, CommandContribution {
//...
}

theia中依赖注入是通过Inversify.Js来完成的,Inversify.Js是为了因应前端引入面向对象(OO)编程后编程复杂度增加的情况而应运而生,比@opensumi/di更加老牌而稳定。Inversify.Js同样使用typescript注解来完成依赖的定义和注入,只是命名稍有不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Dog { //声明类型:狗
bark(): void;
}

@injectable()
class Zoo {
@inject(Dog)
dog: Dog //注入类型:狗

zooTalk() {
dog.bark(); // 调用
}
}

如上的示例和opensumi/di几无不同,最大的区别或许在两者对容器的使用上。

1
2
3
4
5
6
var container = new Container();
container.bind<Dog>(Dog).to(Dog);
container.bind<Zoo>(Zoo).to(Zoo);

const zoo = container.get<Zoo>(Zoo);
zoo.zooTalk();

theia中整个应用都是由Inversify.Js装配而成的,主体有两个容器(Container),分别是frontend(对应于前端)和backtend(对应与后端),除此之外plug-in进程包含一个Inversify.Js容器。在编译时通过dev-packages/application-manager/src/generator来生成对应的初始化代码。

1
2
3
4
5
6
7
const { Container } = require('inversify');
// ...

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

vscode中,依赖注入未借助与任何三方的方案,而是有核心团队基于ts装饰器(Decorator)做了编码实现,vscode中对每一种服务(Services)都做了装饰器的实现,在编码时可以通过注解使用。

1
2
3
4
5
6
7
8
class MyClass {
constructor(
@IAuthService private readonly authService: IAuthService,
@IStorageService private readonly storageService: IStorageService,
) {
authService
}
}

上文中的@IAuthService@IStorageService两个注解即是使用存储和授权两个服务(Services)的注解用法的示例,MyClass并不用关心authServicestorageService的实例化过程,拿来即用即可。

综上所述,依赖注入是构建IDE工具必不可少的一环,而在三者的解决方案中都提供了对等的实现满足了各自工程研发过程中依赖管理的要求。对于opensumi和theia此方案是近似的,而相较与vscode却有显著的不同。

一方面,vscode的初衷并不是让开发者去扩展服务,而是使用服务,所以显然缺失了扩展性的考虑,而opensumi和theia则在起始之初就考虑了扩展性的要求,支持开发则任意的插拔和管理依赖,甚至替换官方的实现,而这在vscode中是无法实现的。

另一方面,vscode对服务的封装也代表了设计良好的分层,开发者对任意功能服务的调用都无法脱离vscode的管控,就变相隔离了运行时和运行容器,开发者通过不可替换、不可变更的接口使用操作系统,对vscode运行时编程大大提高了vscode本本身的移植能力和健壮性。而opensumi和theia则把这些问题丢给IDE的开发者去考虑。

编辑器

IDE中最核心的能力非编辑能力莫属,从严格意义上来说,IDE是Editors的增强,不同的IDE只是在Editor和IDE的终极形态之间寻找的平衡。

5a3353cf9478d62ef777f86e0611690c.png

monaco是vscode团队在创建指出提供的web端的Editor编辑器库,最早在浏览器中使用了代码的编辑、语法高亮、自动补全、DIff、代码片段补全等IDE中常见的编辑特性,将monaco打造成WebIDE也是vscode团队的初心,所以在设计之处就考虑到了IDE集成的场景和可能性。

monaco也是为opensumi、theia和vscode提供代码编辑功能的主要功臣,虽然三者设计各异、思想不同,但都是借助于monaco并在其上添砖加瓦来构建IDE工具。

theia通过包@theia/monaco集成了monaco,@theia/monaco将monaco的扩展点包装为service,并交给Inversify.Js容器托管以便于在需要使用的地方由容器注入。

1
2
3
4
@injectable()
export class MonacoCommandService implements ICommandService, Disposable {
//...
}

vscode和opensumi都是以类似的方案封装了monaco的使用,就不再赘述;monaco是三者迥异的实现当中为数不多的“常数”,这也表明了monaco以高明的适应性和丰富的功能扩展性,成为主流跨端IDE方案中不变的风景。

依赖注入和编辑器是IDE构建的核心,依赖注入代表了架构方案、稳定性、扩展性和健壮性,而编辑器则是核心能力。

插件开发

vscode、opensumi和theia支持相同而有异的插件扩展体系。作为对核心功能的扩展和补充,插件体系承担着重要的作用。为了最大程度的减小插件加载、运行对IDE窗体的的影响,三者都采用了最早由vscode使用的Extension Host结构:插件运行于独立进程,不会因插件错误、奔溃、挂起而影响主窗体的交互和功能。

2f20175d88f379069668fb6a2c3d875c.png

vscode插件(给既有工具扩展特性)

由于opensumi和theia对vscode插件的兼容,使得vscode插件开发规范成为三者事实上的统一插件开发规范,对于满足要求的vscode插件将无需适配地应用于opensumi和theia。以下简要的说明vscode插件开发的关键步骤。

扩展点定义

插件开发即是对扩展点和运行时开发,vscode支持包括语法(grammars)、指令(command)、配置(configuration)等在内的众多扩展点(contribution-points)。插件对扩展点的扩展声明在插件工程的描述文件-package.json中,并以属性contributes来指定。

1
2
3
4
5
6
7
8
9
10
{
"contributes": {
"commands": [
{
"command": "extension.sayHello",
"title": "Hello World"
}
]
}
}

如以上,即是对command指令的扩展,代表在ctrl+shift+p打开的指令面板中可以直接访问使用的指令。

扩展实现

package.json在声明扩展点的同时,也必须声明扩展点的实现入口,即main属性。在众多的vscode插件开发模板中这一值被设置为./out/extension.js./src/extension.ts在编译构建后的输出文件。

在插件声明周期的合适阶段,对扩展点功能编码,即可完成插件功能的实现。

1
2
3
4
5
export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('extension.helloWorld', () => {
//...
})
}

以上简要概述了vscode插件开发的两个重要的责任,对于简单如”hello,world”或者复杂如“gitlen”的插件而言,其原理是完全相同的,只是所使用的扩展点以及基于扩展点所完成的任务相异。

theia插件

theia通过plugin-ext-vscode包支持vscode插件的集成和使用,在运行时上theia和vscode的插件接口是兼容的(并非100%兼容,可以参阅:https://eclipse-theia.github.io/vscode-theia-comparator/status.html),这也就意味着编译为vsix插件包的vscode插件可以从市场下载后应用于theia,而实际上theia构建也是如此进行的,theia编译构建期间会读取package.json中声明的插件列表,然后从openvsix站点中下载并解压插件包。

30bb0d00a2712ec190b8b81fbdefec10.png

theia在vscode的插件体系之外,还有两种扩展机制,Theia Plugin 和 Theia Extension。

  • vscode插件支持在编译时集成以及在运行时安装,但只能使用vscode提供的扩展点
  • Theia Extension只能在编译时集成,是IDE工具的一部分,通过Theia Plugin可以完整访问theia的所有模块
  • Theia Plugin 和vscode插件类似,支持在运行时安装,但可以使用专属与theia的接口或者操纵IDE工具的前端界面

Theia Extension

Theia Extension是IDE的一部分,只能在编译构建时集成,可以完整访问和修改IDE运行时的所有特性。theia中所有模块都是以Theia Extension的形式提供的,比如@theia/navigator@theia/filesystem等,通过定义良好的依赖注入管理,形成了Theia Extension可插拔的结构,在IDE的构建过程中可以自由的选择或者新增Theia Extension。

Theia Extension 以theiaExtensions属性定义,在backend和frontend中通过依赖注入扩展IDE的能力。

1
2
3
4
5
6
7
8
{
"theiaExtensions": [
{
"backend": "lib/node/plugin-vscode-backend-module",
"frontend": "lib/browser/plugin-vscode-frontend-module"
}
]
}

对于vscode无法实现的复杂视图,Theia Extension是优势方案。

Theia Plugin

Theia Plugin是对vscode插件扩展能力不足的补充,Theia Plugin可以在运行时安装。

Theia Plugin的开发和vscode插件的开发类似,在package.json中通过theiaPlugin属性定义插件实现,既可在插件中调用theia提供的扩展API。

1
2
3
4
5
{
"theiaPlugin": {
"backend": "lib/theia-hello-world-plugin-backend-plugin.js"
}
}

相对vcode插件的定义和扩展方法,Theia Plugin不需要使用依赖注入,更加简洁;除了支持常见的quickpick、语言、语言、配置等,还支持对UI界面的定制和调整,更加强大。

1
2
3
4
import * as theia from '@theia/plugin';
export function start() {
theia.commands.registerCommand //...
}

opensumi插件

opensumi宣称和vscode插件具有100%的兼容性,支持完整的vscode插件API,所有vscode插件都可以分毫不动的情况下在opensumi中安装。

同时,opensumi也提供了除vscode插件扩展体系之外的扩展方案。

1
2
3
4
5
6
7
{
"main":"",
"contributes":{ }
"browserMain":"",
"workerMain":""
"sumiContributes":{}
}

opensumi插件扩展即包含了vscode插件完整的声明和使用方式,如maincontributes的配置;也做了升级和扩展,如main中引入的逻辑可以使用大量sumi包暴露的接口,还新增了browserMainworkerMainsumiContributes(在opensumi文档中提供了sumiContributes的描述,但是代码中使用的是kaitianContributes)三个扩展属性,以支持对Browser 环境(扩展左、右、底部面板,以及 Toolbar 等位置)、Worker 环境的扩展(扩展轻量级的任务)。

总结

上文讨论了在opensumi 、vscode、theia三种IDE方案中所提供的扩展能力范围、场景和实现差异,基于这些信息可以得出几个结论:

  • vscode插件是事实上的插件扩展规范,vscode插件丰富的生态是构建IDE不得不考虑重要资源
  • theia弥补了vscode插件能力上的缺失和不足,支持运行时安装的插件(plug-in)扩展前端表现和交互,同时增加了更多的底层暴露。编译时的theia插件有最高权利。
  • opensumi 完全参考了vscode插件的扩展体系,且进行了1:1的实现,增强支持对Browser 环境(前端表现和交互,但范围受限),Worker 环境(只有opensumi有此环境)的扩展。缺失了编译时的插件体系。

多态运行

vscode作为完整的集成开发环境,适应开发者对多种运行环境的要求的能力也在慢慢的迭代中逐步的提供出来,最早的版本通过electron适配到了mac、windows、linux等三大主流的平台,2020年又将vscode拉回浏览器,通过File System Access API 彻底的摆脱了对操作系统的依赖,而成为Web应用;除此之外,vscode的运行时环境还可以运行于vm虚拟机、remote服务器、docker容器、WSL之中,在差异性的环境支持方面做的无所不在。

8e788c61c65e4be5db88acc52209634c.png

这一切都得益于vscode设计巧妙的架构,系统接口层、表现层、扩展层等各司其职、互不干扰;通过对接口编程的思维解藕和了操作系统的联系,使得接口在web、nodejs、electron等多种环境中都有了对等实现,让运行于这些千奇百怪的运行时变得易于操作;通过分离表现层和业务层的关系,接口调用、websocket、http(s)、ipc通信都可以是它们沟通的手段,也就可以做到拆分IDE的独立装状态,拆分业务层到remote服务器、docker容器甚至WSL之中。

站在巨人的肩膀上的opensumi和theia在这一点上有巨大的优势,吸取了vscode多态运行的思路和方案,在设计之初就考虑到了架构的分层和隔离,对运行时环境的解藕也变为了重中之重。

opensumi和theia步了vscode的后尘,差异性的环境支持方面分毫不差的集成了前辈的衣钵,任何基于opensumi和theia的IDE解决方案也能共享这一果实。

总结

对技术的了解可以辅助选型决策,而选型又不得不回归到场景的描述之中。

什么时候做插件?

插件的意义是对即成环境的扩展,插件的限制也是无法打破环境约束。vscode对扩展点的范围有限制,但也可以基本满足常见的对IDE工具扩展的需求。至少有以下的范围之内,毋须多做思考,vscode插件既是最好的选择。

  • 改变IDE工具的颜色或者主题
  • 在给定的范围内扩展IDE的工作台,仅限于Title Bar、Activity Bar、Side Bar、Panel、Editor Group、Status Bar
  • 通过webview加载和渲染自定义的内容
  • 增加语言支持,如语法高亮补全等,使用LSP
  • 增加特定平台的调试断点支持等,使用DAP

6d9fa57f73549263136062c0a392a3c9.png

这里要重点说明的是,vscode对扩展IDE的工作台有明确的范围,所有允许调整的部分都如上图所示。如果产品预期对UI的调整超出上图的范围,插件将无法解决产品的问题,需要升级选择去构建IDE!

什么时候做IDE?

对产品功能的预期超出了插件所能覆盖的范围,就进入到构建IDE产品的范畴,在这一层面自由度会大大提升,至少在以下的几个方面,IDE的方案会更适合:

  • 特定的研发场景,比如小程序开发、公司的LDP开发等市面上的开发工具无法涉猎的领域;
  • 定制的服务整合,比如IDE开发者工具和公司内研发辅助系统如效能、TS、质量卡点的深度整合;
  • 增强研发体验,比如通过知识智能辅助研发的交互体验,编码质量的统计度量的数据体验;

vscode在设计之初就严格约束的适用场景,所以在这一领域基于vscode的实现可能会面临较高的成本对其代码定制,而opensumi和theia是此场景下的最优选择:

d20657aa53a094ddbac4f80da2a0a33f.png

上图是基于opensumi的淘宝开发者工具。

87d90e3d5a7e400baac287dd1aa6b99d.png

上图是基于theia的coffeeeditor开发者工具。

opensumi和theia都具有很强的定制能力,适用场景上会有很大的交集。theia在对vscode插件的兼容性上会稍逊与opensumi,但是在开源生态方面比opensumi起步更早,影响力和用户基数更有优势。再加之,在2021年研发中心的LDP产品已经深度使用了theia做IDE底座,使得theia在恒生的领域里更有光彩。

什么时候做云IDE?

云IDE是脱离了环境依赖的IDE的最终形态,一个浏览器,即可获取代码、编辑、运行调试程序及访问云端的资源,开发者可以脱离设备的约束,随时随地的获取研发环境。

云IDE在PAAS厂商如华为云、阿里云中有丰富的应用,可以实现在线一键应用构建,低成本的访问厂商提供的SAAS服务,高效的完成从研发到交付的流程。

云IDE是开发环境的理想形态,至少在以下几个场景云IDE具有更高的适用性:

  • 复杂的研发环境。容器化的IDE环境可以在云端一键拉起,对于复杂度高的研发环境搭建成本也几乎为0;研发依赖环境的复杂性也可以同步解决
  • 配置库安全。IDE所关联的配置库在远端,由网络交付到浏览器端,开发者毋须获取完整的代码即可启动研发,还可以对研发行为实施管控,避免代码外泄
  • 成本敏感硬件。云IDE将远端的硬件服务化,多开发者可以共享硬件,比如内存、CPU、外接设备等,最大限度提高利用率。

opensumi和theia甚至vscode都是云IDE的优势方案,架构相似的三者在处理云IDE部署的情况上都能满足要求,具体需求根据定制性的要求来做方案选择。