Visual Studio Code
(VS Code)
近年来获得了爆炸式增长,成为广大开发者工具库中的必备神器。它作为一个开源项目,也吸引了无数第三方开发者和终端用户,成为顶尖开源项目之一。它在功能
上做到
了够用,体验上做到了好用,更在拥有海量插件的情况下做到了简洁流畅,实属难能可贵。
我是 VS Code 用户,同时也为它开发插件,插件市场里的众多Java插件基本都是我们团队的作品,所以我在日常工作中观察到不少 VS Code 在工程方面的亮点,下面就来逐一探讨。
你知道 VS Code的开发团队人数只有二十出头吗?难以相信吧,大家都觉得 VS Code 无所不能,如此强大的工具那么几个人怎么做得出来。实际上功能丰富是个美好的错觉,因为大部分针对特定编程语言和技术的功能都是第三方插件提供的,VS Code 的核心始终非常精简,这很考验产品团队的拿捏能力:做多了,臃肿,人手也不够;做少了,太弱,没人用。
他们团队选择了专注于核心功能的开发,为用户提供简洁流畅的体验,并将该思路贯穿在产品开发的每个环节
。在我看来,这就是第一个亮点。
第一个亮点同时也是一个难点,因为“简洁”说到底是产品的“形态”,更关键的其实是前置问题——产品的定位,它到底解决什么问题。该问题如果从用户的角度来看,可以转换为以下几个点——我们为什么需要一个新的工具?它到底是代码编辑器
(Editor)
还是集成开发环境
(IDE)
?让我们来看看项目负责人怎么说:
视频截图 - Erich阐述了VS Code的定位:编辑器+代码理解+调试
它阐述了 VS Code 的定位:
编辑器+代码理解+调试
。这是一个非常节制而平衡的选择,专注于开发者“最常用”的功能,同时在产品的形式上力求简洁高效。从结果来看,这个定位是相当成功的。
在这个定位的指导下,这二十多位工程师搞出了 VS Code。相对较小的功能集,使得开发者们能在代码质量上精益求精,最终用户们也得到了一个性能优异的工具,这是 VS Code 从一众编辑器中脱颖而出的重要原因。关于精益求精,大家可以参考这篇博文,它记录了 VS Code 重新实现 Text Buffer 的过程,同时也分享了思路历程。正因为产品定位以及团队职责上的高度节制,团队成员才能把时间花在这类问题上,写出经得起考验的代码。
与此同时,较小的团队也使得团队成员做到了行为层面的整齐划一,这点在社区互动上体现得尤为明显,大家可以去 GitHub 上看他们的 Issues,超出产品定位范畴的请求和反馈基本都被婉拒或者转交到第三方插件项目,可以说是很专注了。
看到这里,似乎一切都好,但问题来了,码农千千万,你用 Node 我用 Go,你搞前端我弄后台,VS Code 如何满这些五花八门的需求呢?机智的你已经抢答了——海量插件。那么接下来我们来深究一下 VS Code 是如何经营一个庞大的插件生态的。
通过插件来扩展功能的做法已经是司空见惯了,但如何保证插件和原生功能一样优秀呢?历史告诉我们:不能保证。大家可以参考 Eclipse,插件模型可以说是做得非常彻底了,功能层面也是无所不能,但存在几个烦人的问题:不稳定、难用、慢,所以不少用户转投 IntelliJ 的怀抱。可谓
成也插件,败也插件
。
问题的本质在于信息不对称,它导致不同团队写出来的代码,无论是思路还是质量,都不一致。最终,用户得到了一个又乱又卡的产品。所以要让插件在稳定性、速度和体验的层面都做到和原生功能统一,只能是一个美好的愿望。
来看看其他 IDE 是怎么做的,Visual Studio 自己搞定所有功能,并且做到优秀,让别人无事可做,这也成就了其“宇宙第一IDE”的美名;IntelliJ 与之相仿,开箱即用,插件可有可无。这么看起来,自己搞定所有的事情是个好办法,但大家是否知道,Visual Studio 背后有上千人的工程团队,显然,这不是 VS Code 这二十几号人能搞定的。他们选择了让大家来做插件,那怎么解决 Eclipse 所遇到的问题呢?
这里分享一个小知识——Eclipse 核心部分的开发者就是早期的 VS Code 团队。嗯,所以他们没有两次踏入同一条河流。
与 Eclipse 不同,VS Code 选择了把插件关进盒子里
。
这样做首先解决的问题就是稳定性,这个问题对于 VS Code 来说尤为重要。都知道 VS Code 基于 Electron,实质上是个 node.js 环境,单线程,任何代码崩了都是灾难性后果。所以 VS Code 干脆不信任任何人,把插件们放到单独的进程里,任你折腾,主程序妥妥的。
VS Code 团队的这一决策不是没有原因的,正如前面提到的,团队里很多人其实是 Eclipse 的旧部,自然对 Eclipse 的插件模型有深入的思考。Eclipse 的设计目标之一就是把组件化推向极致,所以很多核心功能都是用插件的形式来实现的。遗憾的是,Eclipse 的插件运行在主进程中,任何插件性能不佳或者不稳定,都直接影响到 Eclipse,最终结果是大家抱怨 Eclipse 臃肿、慢、不稳定。VS Code 基于进程做到了物理级别的隔离,成功解决了该问题。实际上进程级别的隔离也带出了另一个话题,那就是界面与业务逻辑的隔离。
“不稳定”之后的问题是“难用”,具体来说就是混乱的界面和流程,究其原因就是插件之间的界面语言的“不一致”,它导致学习曲线异常陡峭,并且在面临问题时没有统一的解决路径。VS Code 的做法是根本不给插件们“发明”新界面的机会。
如上图,插件们被关在 Extension Host 进程里,而UI则在主进程里,所以插件们天然没法直接在用户界面上做手脚。VS Code 统管所有用户交互入口,制定交互的标准,所有用户的操作被转化为各种请求发送给插件,插件能做的就是响应这些请求,专注于业务逻辑。但从始至终,插件都不能“决定”或者“影响”界面元素如何被渲染
(颜色、字体等,一概不行)
,至于弹对话框什么的,就更是天方夜谭了。
VS Code 对于用户界面的把控可以说是谨慎到变态,做过插件的人都懂的,感兴趣的同学可以去深挖一下 TreeView 的历史,会有更直观的体会。乍一看,第三方开发者被卡得死死的,这样不是限制了大家的创造力吗?我想说这个做法跟这个团队的背景密切相关,换一拨人很有可能会失败。他们之所以能成功,是因为该团队在开发工具领域深耕多年,他们把经验转换为观点,最终落实到了 VS Code 的界面元素以及交互语言上,从结果来看,广受欢迎。
界面和业务逻辑的彻底隔离,使得所有插件有了一致的行为,用户就得到了整齐划一的体验。不仅如此,这种接口和行为层面的一致性,
最终转化成了另一个“伟大”的功能——Remote Development
,我们稍后讨论。接下来我们要聊的是 VS Code 另一个创举——Language Server Protocol。
前文提到了 VS Code 定位中的两个特色:代码理解和调试,绝大部分都由第三方插件来实现,中间的桥梁就是两大协议——Language Server Protocol
(LSP)
和 Debug Adapter Protocol
(DAP)
。两者从设计的角度来看高度相似,我们着重看一下最火的 LSP。首先,为什么需要LSP?
全栈开发早已成为这个时代的主流,软件从业者们也越来越不被某个特定的语言或者技术所局限,这也对我们手里的金刚钻提出了新的挑战。举个栗子,我用 TypeScript 和 node.js 做前端,同时用 Java 写后台,偶尔也用 Python 做一些数据分析,那么我很有可能需要若干工具的组合,这样做的问题就在于需要在工具间频繁切换,无论从系统资源消耗和用户体验的角度来看,都是低效的。
那么有没有一种工具能在同一个工作区里把三个语言都搞定呢?没错,就是 VS Code——支持多语言的开发环境,而多语言支持的基础就是 Language Server Protocol
(LSP)
。该协议在短短几年内取得了空前的成功,到目前为止,已经有来自微软等大厂以及社区的一百个实现,基本覆盖了所有主流编程语言。同时,它也被其他开发工具所采纳,比如 Atom、Vim、Sublime、Emacs、Visual Studio 和 Eclipse,从另一个角度证明了它的优秀。更难能可贵的是,该协议还做到了轻量和快速,可以说是 VS Code 的杀手级特性了,同时也是微软最重要的 IP 之一。。。哇塞,又强大又轻巧,怎么看都是个骗局啊,那我们就来看看它到底怎么做到的。
先来说说
设计
(Design)
,大而全是很常见的问题。如果让我来设计这么一个用来支持所有编程语言的东西,第一反应很可能是搞个涵盖所有语言特性的
超集
。微软就有过这样的尝试,比如 Roslyn——一个语言中立的编译器,C# 和VB.NET 的编译器都是基于它做的。大家都知道 C# 在语言特性层面是非常丰富的,Roslyn 能撑起 C# 足以说明它的强大。那么问题来了,为啥它没有在社区得到广泛应用呢?我想根本原因是“强大”所带来的副作用:复杂、主观
(Opinionated)
。光是语法树就已经很复杂了,其他各种特性以及他们之间的关系更是让人望而却步,这样一个庞然大物,普通开发者是不会轻易去碰的。
相较之下,LSP 显然把小巧作为设计目标之一,它选择做最小子集,贯彻了团队一贯节制的作风。它关心的是用户在编辑代码时最经常处理的物理实体
(比如文件、目录)
和状态
(光标位置)
。它根本没有试图去理解语言的特性,编译也不是它所关心的问题,所以自然不会涉及语法树一类的复杂概念。它也不是一步到位的,而是随着 VS Code 功能的迭代而逐步发展的。所以它自诞生至今依然保持着小巧的身材,易懂,实现门槛也很低,迅速在社区得到了广泛的支持,各种语言的 Language Server
(LS)
遍地开花。
小归小,功能可不能少,所以抽象就非常关键了。
LSP 最重要的概念是动作和位置
,LSP的大部分请求都是在表达”在指定位置执行规定动作“。举个栗子,用户把鼠标悬停在某个类名上方,查看相关的定义和文档。这时 VS Code 会发送一个'textDocument/hover'请求给 LS,这个请求里最关键的信息就是当前的文档和光标的位置。LS 收到请求之后,经过一系列内部计算
(识别出光标位置所对应的符号,并找出相关文档)
,找出相关的信息,然后发回给 VS Code 显示给用户看。这样一来一回的交互,在 LSP 里被抽象成请求
(Request)
和回复
(Response)
,LSP 同时也规定了它们的规格
(Schema)
。在开发者看来,概念非常少,交互形式也很简单,实现起来非常轻松。
看到这里,大家应该对 LSP 有了更进一步的理解,它本质上是胶水,把 VS Code 和各种语言的 LS 粘在一起。但它不是普通的胶水,而是非常有品位的胶水,这品位就体现在细节上。
首先这是一个基于文本的协议,文本降低了理解和调试的难度。参考 HTTP 和 REST 的成功,很难想象如果这是一个二进制协议会是什么局面,甚至同样是文本协议的 SOAP 也早已作古,足以说明“简单”在打造开发者生态里的重要性。
其次这是一个基于 JSON 的协议,JSON 可以说是最易读的结构化数据格式了,大家看看各个代码仓库里的配置未见都是啥格式就知道这是个多么正确的决定了,现在还有人在新项目里用 XML 吗?又一次——“简单”。
再次,这是一个基于 JSONRPC 的协议,由于 JSON 的流行,各大语言都对它有极好的支持,所以开发者根本不需要处理序列化、反序列化一类的问题,这是实现层面的“简单”。
从这些细节可以看出,VS Code 团队对当今技术趋势的把握是相当精准的,他们决策充分考虑到了“简单”,牢牢抓住了社区开发者的心。所以重要的事情说三遍: