一文带你探秘 Web IDE 及其功能拓展

本文会浅析 web IDE 及其相关生态,以及如何在 web 端使用 MocacoEditor 实现 git blame 、代码高亮、校验、语法提示和代码调试功能,帮你更好的理解 Web IDE 这个 toD 产品形态对实际研发流程的助力与赋能。

Web IDE 浅谈

What:什么是 Web IDE

首先,我们需要先解释什么是 IDE (integrated development environment):

An integrated development environment (IDE ) is a software application that provides comprehensive facilities for software development. An IDE normally consists of at least a source-code editor , build automation tools, and a debugger . Some IDEs, such as IntelliJ IDEA, Eclipse and Lazarus contain the necessary compiler, interpreter or both; others, such as SharpDevelop, NetBeans do not.

------ wikipedia

作为一名开发工程师,我们在日常工作中离不开对 IDE 的使用,如今的 IDE 软件相关生态繁荣而活跃,各种玲琅满目语法插件以及 AI 辅助(like: copilot)等工具极大的提升了我们的工作效率。

而 WebIDE 就是可以运行在浏览器中的 IDE ,用户无需下载本地 IDE 软件,打开浏览器就可以写代码。这极大的增强了编码的便捷性,提升了开发者体验。

分类

VS Code 的成功基本奠定了如今市面上 Web IDE 的格局。 传统的 IDE 需要自己来维护整个 IDE 的 editor / 语言分析 / 调试协议 ...,然而 LSP & DAP 打破了这一僵局。目前主流的 Web IDE 技术栈都是与 VS Code 一致:

  • 使用了 Monaco Editor 进行编码编辑,这提供了良好的用户体验和强大的功能。

  • 采用了 Language Server Protocol (LSP) 进行语法分析,这一做法使得语法分析的实现与 IDE 解耦,使 IDE 仅需要实现协议即可使用现有的 language server。

  • 使用了 Debug Adapter Protocol (DAP) 进行调试,进一步提高了编程效率。

  • VS Code 拥有一个繁荣的扩展市场,许多 Web IDE 都尽可能支持 VS Code 的扩展机制,以充分利用开源生态系统。这些扩展的支持程度直接与对 VS Code API 的实现程度成正比。

所以当前业界主要有以下三种类型:

  • 直接使用 VS Code 源码

  • 基于 Theia IDE 二次开发

  • 自研,但架构同 Theia

首先登场的鼎鼎大名的 Github 出品的(据说跳票了很久Codespaces, 它直接使用 VS Code 源码:

个人体验从打开流畅度来说很舒适,控制台输入也完全没有卡顿感

Codespaces

然后我们再来看看 Theia 的特点:

  • VS Code 和 Theia 都使用 Monaco Editor 作为编辑器
  • Theia 也支持 VS Code 相关扩展,但由于 VS Code 的扩展系统提供的 API 非常多,Theia 还没有完全支持,这会带来一些麻烦
  • Theia 被设计为更加模块化和灵活,可进行高度自定义
  • Bug 修复和新功能更新较慢

🌟 综上所述:作为业务研发我们只关注 VS Code 即可满足大部分需求

Why:为什么我们需要它?

应用场景

  • 在线编程: 如果考察候选人代码能力只能让候选人投屏演示进行编码,互动性差,Web IDE 解决了这个痛点
  • 远程开发: 远程开发如果需要在新主机上重新配置一套环境实在是太麻烦啦,Web IDE 可以在部分项目做到开箱即用
  • 微服务 / FaaS 开发: 传统开发方式的环境部署效率低,而且可能存在不能单点调试的问题
  • 需要复杂环境配置的开发: 通常新人入职后,需要安装各种研发工具并进行系统配置,才能开始上手开发,而使用 Web IDE 大大缩短了这个时间

VS code 架构设计

先来看看无敌的Monaco Editor吧!

The Monaco Editor is the code editor that powers VS Code . A good page describing the code editor's features is here. It is licensed under the MIT License and supports Edge, Chrome, Firefox, Safari and Opera. The Monaco editor is not supported in mobile browsers or mobile web frameworks. Find more information at the Monaco Editor repo.

VS code 底层也是通过 Monaco Editor + 各种 UI 组件实现的, 各项能力拓展依托于各有特色的插件,而插件的能力往往也是通过 LSP 或 DAP 与 Language Server 完成通讯的。

LSP

语言服务器协议是一种被用于编辑器或集成开发环境与支持,比如自动补全,定义跳转,查找所有引用等语言特性的语言服务器之间的一种协议,例如实现 lint 能力和特定语法提示能力。

DAP

  • 一套协议抽象层,供开发工具(IDE)和调试器(Debugger)通信。将原本 IDE 与调试器之间的关联抽象出了 Debugger Adapter Protocol 一层,使得不同的编辑器能够较为便捷地复用现有的调试器,debug 功能包括:

    • source-, function-, conditional-, and inline breakpoints,
    • variable values shown in hovers or inlined in the source,
    • multi-process and multi-thread support,
    • navigating through complex data structures,
    • watch expressions,
    • You can use the debug console to do interactive evaluation with autocomplete (aka REPL),
    • log points.

如果没有 LSP ,如果我们想对多语言提供 debug 服务,则需要设计不同语言的调试 Debugger(如左图):

但是每种语言都要实现一次的话太过麻烦,所以后来出现了一个适配层协议:DAP(如右图),屏蔽了不同协议的区别,提供统一的协议接口给客户端用,我们避免了很多不必要的重复开发。我们只需要开发一套逻辑即可,而不用关心一些底层逻辑的实现。

来点花活

Git blame

如果发现某段代码的逻辑有点问题,不确定之前的技术背景时,最快的解决方法就是找到这一块代码究竟是谁写的在什么时间写的,如果可以找到对应同学,那么直接沟通就是最快的解决路径。即使因为人员调动无法联系到当时变动的负责人,那么根据当时提交时的版本和描述也可以还原过去的变动历史背景。

这些代码行数的相关附属属性,为普通的代码编辑器带来了便捷的管理数据展示:

Git blame: Show what revision and author last modified each line of a file

变更逻辑识别

那么如果我们想在 Monaco Editor 中实现一个简单的 git blame 能力需要怎么做呢?

直接去搭建一个 git 服务当前可以做到,不过成本就有点太高了,而且我们暂时也不需要 git 相关其他更加复杂的能力。

综合决策下我们可以采用更简单的方法:Monaco Diff + Monaco Decoration 来说实现我们需要的能力。我们只需要在每一次提交后根据代码变更后的 diff 数据来计算各行的 blame 数据即可。

通过 monaco-diff 获取代码当前的所有变更

使用 Monaco 原生 api diffEditor.getLineChanges() 获取change存在 bug ,遂使用 monaco-diff 代替

javascript 复制代码
import { diff } from 'monaco-diff';
const changes = diff(originalData.split('\n'), modifiedData.split('\n'));

//打印 changes 可以获得下面的结果:

[
    {
        "originalStartLineNumber": 2,
        "originalEndLineNumber": 2,
        "modifiedStartLineNumber": 1,
        "modifiedEndLineNumber": 0
    },
    {
        "originalStartLineNumber": 6,
        "originalEndLineNumber": 6,
        "modifiedStartLineNumber": 5,
        "modifiedEndLineNumber": 5,
        "charChanges": [
            {
                "originalStartLineNumber": 0,
                "originalStartColumn": 0,
                "originalEndLineNumber": 0,
                "originalEndColumn": 0,
                "modifiedStartLineNumber": 5,
                "modifiedStartColumn": 6,
                "modifiedEndLineNumber": 5,
                "modifiedEndColumn": 8
            }
        ]
    },
    {
        "originalStartLineNumber": 9,
        "originalEndLineNumber": 0,
        "modifiedStartLineNumber": 9,
        "modifiedEndLineNumber": 9
    }
]

根据不同的变更进行相应的操作

javascript 复制代码
// 根据不同的数据,能解析为不同的修改类型
function getChangeType(change) {
  if (change.originalEndLineNumber === 0) {           // 新增行
    
  } else if (change.modifiedEndLineNumber === 0) {    // 删除行
    
  } else {                                            // 其他的都是修改
    
  }
}

信息展示

Monaco 编辑器的装饰器可以实现在编辑器中高亮显示文本、语法错误、搜索结果等功能,我们可以基于这个属性给用户所在的行添加 blame 信息。

  1. 监听用户光标所在的位置:onDidChangeCursorPosition

  2. 给此行添加一个装饰器:lineDecoration

  3. 书写装饰器的样式等

  4. 监听用户操作,根据变更逻辑识别中获得的信息执行以下操作

    1. 当用户新增某行时,新增对应行的数据,行变更人为当前用户,同时更新时间和版本号
    2. 当用户删除某行时,将存储的对应行 blame 信息也删除
    3. 当用户修改某行时,修改对应行数据,将对应的行变更人改为当前用户,同时更新时间和版本号
  5. 正式提交 某次修改后,把这次修改对应版本号的描述也存储至 blame 数据结构中

php 复制代码
inner.onDidChangeCursorPosition(({ position }) => {
  const { lineNumber } = position;
  lastDeco.current = inner.deltaDecorations(
    [...lastDeco.current!],
    [
      {
        range: new monaco.Range(lineNumber, 1, lineNumber, Number.MAX_SAFE_INTEGER),
        options: {
          after: {
            content: author[lineNumber-1],
            inlineClassName: 'inlinestyle',
            cursorStops: 3,
          },
        },
      },
    ],
  );
});

代码高亮、校验、语法提示

常规语言

对于 Monaco Editor 来说 JS / TS / CSS / HTML 等相关能力是内置的了,不需要额外的 lsp 服务,其他的例如 python、java、rust、c# 等就需要额外的插件+ lsp 服务

这种算是比较简单的场景,如果有相关诉求可以点击参考学习搭建。

数据序列化格式

比如 JSON 和 YAML,我们就需要自己制定相关规则来约束用户输入的格式。

JSON Schema

JSON Schema其实是对json数据格式的描述和规范,是对JSON格式一种约束,更明确地定义数据的类型和结构。

比如说我们有这样一个 schema:

json 复制代码
{"type":"object","properties":{"title":{"type":"string"}}}

通过校验,可以检查出 a 符合规范,而 b 不符合:

css 复制代码
// success
var a = {title: '123'}

// error
var b = {title: 123}

在 Monaco Editor 中,我们可以使用setDiagnosticsOptions这个 API 来使用 JSON Schema 来约束我们希望定制化的 JSON 数据结构,如果想自己试试的话可以看看:demo

如果我们想做到自定义实现特定的 YAML 格式校验,我们需要借助第三方库的能力,其核心能力还是使用了JSON Schema 和 yaml-language-server,可以 参考以下几步:

  1. 接入monaco-yaml
  2. 通过接口获取我们需要的 raw schema 数据
  3. 把 raw schema 转换为 IDE 可以读取的 scheam.json
  4. 执行纠错、语法提示 & 补全功能
  5. 通过 editor.onDidChangeMarkers 监听错误情况,阻止不规范 Conf 提交并进行错误提示

同样,YAML 校验同样也有 demo可以供我们学习和测试

自定义语言

这个就相对比较复杂了,我们需要自己实现很多东西,大致流程如下:

  1. 注册新语言register
  2. 设定语法高亮器setMonarchTokensProvider
  3. 注册代码补全器registerCompletionItemProvider
  4. 注册 hover 提示器registerHoverProvider
  5. 实现语法校验样式setModelMarkers
  6. 通过 ANTLR 或手写递归下降解析器来构建词法和语法解析器

Debug

在代码量级比较大的时候,代码调试功能是用户保证代码健壮性的最佳选择。 而在本地 IDE 进行代码调试时又没有办法获取平台方注入的 context,调试也不能到达理想的效果。

如果可以在 webIDE 上实现完整的调试能力,则会大大提升用户调试的便利度和获取调试结果准确性。

前端核心组件

Code / name Structure of the module(s) Newly added upstream-downstream interaction
Debug button - 按钮 - 如果本段代码可以被调试,则展示此按钮
  • 获取代码上下文内容并将其发送到调试插件
  • 点击按钮开始调试流程 | | debug-plugin | - 悬停提示框
  • 断点 | - 使用 Monaco API 创建悬停提示和 断点
  • 管理 websocket 连接到服务器并交换调试信息
  • 提供 VariablesZone 和 OutputsZone 的数据来源
  • 提供一些处理函数handlePause, handleContinue ...... )和给 debugToolbar 使用的当前代码执行状态 (暂停、执行中 .......) | | VariablesZone | - 树结构组件 | - 可以单击箭头展开或收起树形结构 | | OutputsZone | - 文本显示组件 | - 如果信息太长,我们可以选择删减一些历史输出 | | debugToolbar | - 拖动手柄
  • 暂停
  • 继续
  • Step Over
  • Step Into
  • 重新启动
  • 停止 | - 使用调试插件中的函数并控制全部调试过程 |

前后端交互

服务核心流程梳理:

  1. 通过 socket.io 创建 websocket 服务,监听 path 为 /dap ,必要参数为需要调试的代码内容变量上下文

  2. 使用 net.createServer() 创建 tcp server ,获得 tcp 的 Address(host + port)

  3. 将需要调试的代码临时存储在本地 (eg. /tmp/debug-1uJMG6/rule.py),变量也需要存储

  4. 使用 spawn 开始一个新的 python adapter 子进程

    1. 在 start_adapter.py 中执行 main 函数
    2. 使用 debugpy 建立连接,传入对应的 rule_path 和全局变量 ,并且使 python code 运行在之前生成的 host 和 port 中
    3. 在创建成功后添加第一个断点,使代码看起来停在第 1 行,方便后续调试,在线程中执行 runpy.run_path()
    4. 在调试结束执行清理函数,结束调试线程,并 unlink rule_path 和 globals_path
  5. 监听 data、exit、error 事件,并通过 websocket 和客户端实时通讯

Debug Service 层

在 Debug Service 层,使用 Debug Adapter Protocol 进行统一的数据传输,这个的好处是:

  1. 基于统一的消息层抽象,后续可以快速扩展其他语言的debugger
  2. 基于 vscode 的标准协议生态,能够快速搭建基于 DAP 的 Adapter 的服务

总结

关于 web IDE 还有很多其他的能力拓展可以实践,比如多文件、多文件代码调试,代码格式化等很多能力,在本文就不一一展开为大家介绍了,感兴趣的话大家可以从官方 demo开始 Monaco Editor 搭建第一步!

参考阅读

相关推荐
Jiaberrr3 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy3 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白3 小时前
web基础+http协议+httpd详细配置
前端·网络协议·http
前端小趴菜、3 小时前
Web Worker 简单使用
前端
web_learning_3213 小时前
信息收集常用指令
前端·搜索引擎
tabzzz3 小时前
Webpack 概念速通:从入门到掌握构建工具的精髓
前端·webpack
200不是二百4 小时前
Vuex详解
前端·javascript·vue.js
滔滔不绝tao4 小时前
自动化测试常用函数
前端·css·html5
码爸4 小时前
flink doris批量sink
java·前端·flink
深情废杨杨4 小时前
前端vue-父传子
前端·javascript·vue.js