前言
很多人第一次看 VS Code,都会先看到它的界面:侧边栏、编辑器、终端、面板,看起来像是一个复杂一点的前端应用。
但真正深入之后会发现,VS Code 并不是一个普通的 React App。它是一个基于 Electron 的桌面 IDE 架构,背后有 main、preload、renderer、extensionHost 等多个进程协作,也有命令系统、Service、RPC、插件隔离等一整套设计。
为了更好地理解这些设计,我基于 React + Vite + Electron 写了一个 mini-vscode 项目。这个系列不是为了复刻 VS Code 的全部功能,而是希望通过一个更小的实现,把 VS Code 最核心的架构骨架讲清楚。
mini-vscode:github.com/zenoskongfu…
这个系列最想给谁看?
这个系列主要是对自己学习 vscode 架构的一个总结, 也是自己的一个技术沉淀,也给想学习 vscode 架构的人一个领路的作用。基于对 vscode 的学习, 写了一个 mini-vscode 项目。mini-vscode 的技术栈是 React + vite + Electron
这个系列首先是我学习 Vscode 架构过程的一次总结,也是一次技术沉淀。他背后有一套非常清晰的桌面 IDE 架构:多进程隔离、服务化能力、命令系统、插件宿主、RPC 通信、编辑器模型等等
为了更好地理解这些设计,我基于 React+ Vite + Electron 写了一个 mini-vscode 项目,它不是为了完成复刻 VS code 的所有功能,而是希望用一个更小、更容易读懂的项目,把 VSCode 的核心架构骨架拆开讲清楚。
所以这个系列既是我自己的学习记录,也希望能给想学习 VS Code 架构的人提供一条更容易入门的路径
希望第一篇读完后,读者记住一句什么话?
VS Code 不是一个 React App,而是一个桌面的多进程 IDE 架构
很多人第一次看 VSCode 或者 mini-vscode,可能会先看到页面、组件、编辑器区域,于是很容易把它理解成一个 React 页面
但实际上,VS Code 并不是运行在浏览器里的普通 Web App。它是一个基于 Electron 的桌面程序,既有浏览器渲染页面的能力,也有 NodeJs 操作本地系统的能力
也就是说,它的复杂度不在于页面怎么化,而在于一个桌面 IDE 如何安全稳定、可拓展地组织本地文件、终端、插件、编辑器和各种系统能力
如果只是写界面,为什么还需要 Electron main 进程?
如果 mini-vscode 只是展示一个静态页面,那确实只需要 React 就够了。但一个 IDE 远远不止是页面这么简单。
它需要打开本地文件,读取目录结构,保存文件内容,监听文件变化,启动终端,执行命令,管理窗口,甚至和 Git、调试器、语言服务等系统能力交互。这些能力都不是普通浏览器页面能够直接完成的。
Electron 的 main 进程就承担了这部分职责。它运行在 NodeJs 环境中,拥有访问本地系统的能力,是整个应用中真正接近操作系统一层。
Renderer 负责接main 和用户交互,main 负责本地能力和系统资源。两者通过 IPC 通信写作,才组成了一个真正可用的桌面 IDE
preload 到底在保护什么?为什么 renderer 不能直接访问 Node?
preload 保护的的底层能力的边界
在 Electron 里,renderer 负责渲染页面,如果让 renderer 直接访问 NodeJs,那么页面的代码就可以直接读写本地的文件,执行系统命令、访问任意底层 API。这样虽然方便,但非常危险。
因为 renderer 层本质上仍然是一个浏览器环境,它可能加载复杂的前端代码,也可能收到 XSS 等问题影响,一旦 renderer 可以直接使用 Node 能力,任何前端层面的漏洞都可能变成本地系统级别的风险
更合理的做法是:不让 renderer 直接接触 Node,而是通过 preload 做一层受控暴露。
preload 使用 contextBridge 把少量经过设计的能力挂到 window.electronAPI 上,renderer 只能调用这些被允许的 API,比如读取文件,创建文件、保存内容,而不能随意调用 NodeJs 的全部能力
这就是 preload 的意义:它不是为了增加复杂度,而是为了让底层能力被有边界有规划地使用
为什么 VS Code 要把“能力”做成 Service,而不是组件里直接调用?
vscode 的 service 有两种,一个处于 renderer 层,一个处于 main 层。
renderer 层
从代码角度上来看,这里的 service 层包装着对 preload 暴露的 API 的封装:即window.electronAPI.fs.createFile。从设计上看,这样的好处是,renderer 层的调用能力,只需要面对能力的抽象就可以了,不需要沉浸在具体的 API 中。增加了代码的可读性和维护行
main 层
在 mini-vscode 中,service 是理解架构的关键
他可以分成两个层面来看:renderer 层的 Service,和 main 层的 Service。他们不再同一个进程里,但设计思想是一致的:都是把具体的能力封装成更清晰的业务接口。
在 renderer 层,Service 面向的是工作台逻辑。比如文件服务、编辑器服务、命令服务等。组件不需要直接关心底层调用的细节,而是调用更接近业务语义的 Service 方法。
这样做的好处是,组件只需要对面我要创建文件,我要打开编辑器,我要执行命令这样的抽象能力,而不需要沉浸在 IPC,reload API,数据转换这些细节里。
在 main 层,也会有一组按能力划分的 service,他们负责封装对 Nodejs API、本地文件系统,终端进程等底层能力的调用。
这样 preload 暴露能力的时候,也不是简单地把 NodeAPI 原样暴露出去,而是暴露经过业务整理后的能力接口。
所以 Service 的价值在于:它把能做什么和具体怎么做分开了。组件面对的能力抽象,底层实现可以被替换、拓展和维护
为什么命令系统要用 command id 做中间层?
这是 vscode 中一个非常聪明的设计
在一个 IDE 中,同一个功能可能会被很多地方触发。比如保存文件,可以通过快捷键触发,可以通过菜单触发,可以通过命令面板触发,也可以被插件触发。
如果这些入口都直接调用具体的实现,系统很快就会变得混乱:快捷键要知道保存逻辑在哪里,菜单也要知道,插件还要知道另一套调用方式。这样模块之间会产生大量耦合
VS Code 的做法是引入 command id 作为中间层。功能是闲着注册一个命令,比如 workbench.action.files.save其他触发者不需要知道这个功能具体由谁实现,只需要知道这个命令 ID
绥中所有入口都会收敛到:
executeCommand(commandId, ...args)
这样依赖,快捷键、菜单、命令面板、拆那都可以通过同一种方式触发能力。
命令系统的本质,就是把谁触发与谁实现解偶。她让不同模块之间可以互相调用能力,但又不需要彼此直接依赖
为什么插件不能直接操作界面,而要通过 extensionHost 和 RPC?
因为插件是不可信的
如果插件可以直接访问 Renderer 里的 React 组件,DOM、Service,甚至直接操作本地文件系统,那么一个插件写坏了,就可能影响整个编辑器的稳定性和安全性
所以 VS Code 把插件运行在独立的 extensionHost 进程里
插件想执行命令、注册能力、访问编辑器状态,都不能直接改主界面,而是通过一层 RPC 通信告诉 renderer:
我想注册一个命令,我想执行一个命令
我想获取当前编辑器信息
我想提供某种语言能力
renderer 再决定怎么处理
这背后的思想是:插件可以拓展能力,但不能侵入主程序
这也是 VS Code 插件系统可以稳定运行的关键
为什么编辑器内容不能只放在 React state 里?
因为编辑器不是普通的表单
普通 React 页面里,一个输入框的内容放在 useState 里没有问题。但代码编辑器要复杂得多,它需要处理:
- 光标位置
- 选区
- undo/redo
- 滚动位置
- 语法高亮
- 多文件切换
- dirty 状态
- 跳转定义
- 诊断信息
这些状态如果全部塞进 React State,组件每次 render 都可能影响编辑器实例,甚至导致内容、光标、历史记录丢失。
所以 mini-vscode 里真正承载编辑器内容的是 Monaco Model。每个文件路径对应一个独立的 model,文件切换时复用 model,而不是重新创建一个编辑器内容。
React 在这里更像是投影视图,负责把前端状态展示出来。真正的编辑器状态,应该由 EidtorService 和 Monaco model 维护。
这也是 VSCode 架构里一个很重要的思想:UI 组件不应该成为核心状态的唯一来源,核心状态应该沉到服务层和模型层
所以 mini-vscode 真正在复刻 VS Code 的哪套骨架?
mini-vscode 复刻的不是 VS Code 的全部功能,而是它最核心的架构骨架
它真正想表达的是这几件事:
第一:VS Code 是一个多进程桌面程序
main / preload / renderer / extensionHost各有明确的边界,不是一个简单的浏览器页面。
第二:renderer 才是工作台的核心
编辑器、命令系统、Service、状态流转,大部分都发生在 renderer 这一侧
第三:能力要通过 Service 暴露
组件不直接碰底层 API,而是面对抽象能力,这样的系统才能继续拓展。
第四:所有的操作都尽量收敛到命令系统中
快捷键、菜单、命令面板、插件最终都通过 command id 找到对应能力
第五:插件运行在隔离的环境里
extensionHost 通过 RPC 和 renderer 通信,既保证了拓展能力,也保护主程序稳定性
所以 mini-vscode 的价值不在于“功能有多少”,而是在于它把 VS Code 最重要的设计思想拆成了一个可以阅读,可以调试、可以学习的小模型
总结
所以,mini-vscode 第一篇想先回答一个最基础的问题:VS Code 为什么不是一个普通 React App?
答案在于,它不只是一个页面,而是一个桌面 IDE。React 只负责渲染界面,真正支撑编辑器运行的,是多进程模型、受控的本地能力、Service 抽象、命令系统和插件隔离机制。
只有先理解这一点,后面再看 VS Code 的进程模型、命令系统、插件系统、编辑器模型和调试架构,才不会觉得这些设计是“为了复杂而复杂”。它们本质上都是为了让一个桌面 IDE 能够安全、稳定、可扩展地运行。