
这一篇读的是 microsoft/vscode。VS Code 很容易被当成“编辑器项目”来泛读:先看编辑区、再看文件树、再看命令面板。这个顺序会把人带进大量 UI 和贡献点细节,最后只记住目录名。
更有效的一条线,是从桌面应用启动开始,一直追到插件被激活:Electron main 进程怎样准备环境,窗口怎样拿到 native configuration,renderer 怎样加载 Workbench,Workbench 怎样合并服务注册,Extension Host 怎样被 main 进程启动,最后 activation event 怎样走到插件的 activate(context)。
本文源码基线:本地目录 sources/vscode;分支 main;提交 1f98b39208918cace8d36e2a4f20b7b9282508f1;提交时间 2026-05-31T00:04:28+02:00;提交主题 Avoid leaving detached DOM elements in Getting Started (#319128)。本地根 package.json 声明 name = code-oss-dev、version = 1.123.0、入口 main = ./out/main.js;根 product.json 声明 OSS 产品名为 Code - OSS。
本文由 AI 辅助整理和改写,可能存在遗漏、理解偏差或版本差异。关键实现请以本文固定的提交、对应源码文件和官方文档为准,自行复核后再用于工程判断。
从 src/main.ts 开始,但不要停在入口文件

src/main.ts 看起来像入口,实际更像 Electron 进程的校准层。它会处理 portable mode、CLI args、Electron command-line switches、sandbox/GPU sandbox、user data path、crash reporter、vscode-webview 和 vscode-file scheme,然后等待 Electron app.once('ready')。
真正进入业务 main bundle 之前,它还会设置 NLS config 和 code cache path,再通过 bootstrapESM() 动态导入 ./vs/code/electron-main/main.js。
这里的重点是:VS Code 的启动不是“加载一个 JS app”。它先把进程级行为、安全 scheme、缓存、NLS 和崩溃上报固定下来,然后才进入 CodeMain。如果从这里开始读,很多后面的窗口、webview、extension host 行为会更容易定位到源头。
Main 进程先有服务,再有窗口
src/vs/code/electron-main/main.ts 里的 CodeMain.startup() 会创建 main 进程的 ServiceCollection,再处理单实例、lockfile、logger、shutdown cleanup,最后创建 CodeApplication(...).startup()。
createServices() 里能看到一批窗口之前就存在的服务:EnvironmentMainService、FileService、StateMainService、UserDataProfilesMainService、ConfigurationService、LifecycleMainService、IProtocolMainService。这些东西属于 main 进程级底座,窗口只是后面被这些服务打开和管理的 native shell。
到了 CodeApplication,它先配置 Electron session 权限,再注册监听器。Webview origin 和核心 window origin 的权限是分开处理的:webview 只允许特定能力,其他 origin 默认拒绝。这层权限配置直接落在插件生态的安全边界上。
CodeApplication.startup() 还会创建 IPC server、启动 shared process、初始化服务和 IPC channels、处理 protocol URL,然后打开第一个窗口。IExtensionHostStarter 也是在这个阶段被注册到 main 进程服务和 IPC channel 里的。后面本地 extension host 的实际进程创建,会回到这里。
窗口不是空壳,关键在 native configuration

窗口打开的主线在 src/vs/platform/windows/electron-main/windowsMainService.ts。WindowsMainService.open() 会先把请求分类成 folders、workspaces、files、empty windows、backup windows、diff/merge/wait marker 等路径,然后才进入窗口加载。
真正进入窗口加载时,openInBrowserWindow() 会组装 INativeWindowConfiguration。这个对象很重,里面有 CLI args、machine ID、appRoot、execPath、codeCachePath、backup path、profiles、home/tmp/userData dirs、remote authority、workspace、filesToOpen、NLS、logs、product、performance marks、OS、color/accessibility state、policy data、CSS modules、window session 等字段。
这一步解释了很多调试问题:为什么某个 profile 没生效,为什么 remote window 行为不同,为什么 backup 或 window reuse 会影响打开结果。VS Code 的 renderer 依赖 main 进程构造好的窗口配置启动,而不是靠全局变量猜环境。
Renderer 侧入口在 src/vs/code/electron-browser/workbench/workbench.ts。它从 preload globals 里 resolve window configuration,设置 NLS、base URL、开发态 CSS import maps,再动态 import vs/workbench/workbench.desktop.main.js。加载完成后,才调用 desktop workbench 的 main(configuration)。
Workbench 不是普通前端 SPA

src/vs/workbench/electron-browser/desktop.main.ts 里的 DesktopMain.open() 会先初始化桌面侧服务,同时等待 DOM ready,然后创建 new Workbench(...) 并调用 workbench.startup()。
这里最容易误读的是服务注册方式。VS Code 没有把所有服务都直接写进 DesktopMain。文件里反复强调应通过 registerSingleton() 注册服务,因为桌面端、Web 端、共享 workbench 都要进入统一 registry。
src/vs/workbench/browser/workbench.ts 的 initServices() 会遍历 getSingletonServiceDescriptors(),把前面通过 registerSingleton() 注册的服务放进 collection,再创建 InstantiationService。这就是 Workbench 的基本组织方式:服务 token、实现类、lazy/eager 实例化、contribution import side effects,共同组成一张运行时服务图。
所以 src/vs/workbench/workbench.desktop.main.ts 不能当普通 barrel file 看。它的大量 import 都带副作用:注册服务、贡献点、actions、views、extension API participants。和 extension host 相关的 nativeExtensionService、extensionHostStarter、extensionHost.contribution,也是通过这条 bundle import 链进入 Workbench。
插件不直接跑在 renderer 里

NativeExtensionService 是 desktop Workbench 里的关键对象。它会创建 extension scanner、extension host factory、extension host kind picker,并作为 IExtensionService 的 eager singleton 进入工作台。
插件会跑在哪里,不是简单由“本地安装”决定。NativeExtensionHostKindPicker 会结合 extensionKind、本地/远端安装位置、remote authority、web worker host 能力来判断:
- •
ui且本地安装,通常跑 local process。 - •
workspace且远端安装,跑 remote host。 - •
workspace但没有 remote,会回到 local process。 - •
web且本地安装且启用 web worker,可能跑 local web worker。
这也是 VS Code 源码最有价值的地方之一:它把插件运行位置收敛成可解释的策略,避免让判断散落在各处的 if/else 里。
本地进程 host 的启动链路在 localProcessExtensionHost.ts。renderer 侧先决定需要 extension host,准备环境变量、debug port、inspect 参数、MessagePort 和握手 nonce;但实际创建进程的是 main 进程里的 extensionHostStarter.ts。它会创建 WindowUtilityProcess,类型是 extensionHost,entry point 是 vs/workbench/api/node/extensionHostProcess。
换句话说,本地 extension host 的边界是:renderer 负责发起和握手,main 进程负责创建 utility process,extension host process 再通过 MessagePort/RPC 接回 renderer。
RPC 才是插件 API 的真实边界
src/vs/workbench/services/extensions/common/rpcProtocol.ts 里的 RPCProtocol 用 proxy identifier 把 $method 调用封成跨进程 RPC。getProxy(identifier) 会创建远端 proxy,JS Proxy 拦截 $ 开头的方法名,再调用 _remoteCall(rpcId, name, args)。
接口地图在 src/vs/workbench/api/common/extHost.protocol.ts。里面的 MainContext 和 ExtHostContext 定义了两边能互相调用的对象,例如 MainThreadExtensionService 和 ExtHostExtensionService。
mainThreadExtensionService.ts 展示了这条桥怎么接起来:main-thread customer 从 extHostContext.getProxy(ExtHostContext.ExtHostExtensionService) 拿到 ext host proxy,再把 $startExtensionHost、$activateByEvent、$activate 等调用转成远端调用。
这能纠正一个常见误解:插件 API 不是 renderer 里的一堆直接函数调用。真实边界是一组 MainThread/ExtHost 双边对象和 RPC proxy。理解这层,才能理解为什么 VS Code 可以把插件放到 local process、web worker 或 remote host,而上层 API 仍然保持相对一致。
Activation 不是 require(main).activate() 这么简单

插件激活要分 renderer side 和 extension host side 看。
Renderer side 在 extensionHostManager.ts。它会设置 extension registry,启动 extension host,并在 activateByEvent() 时检查扩展集合里是否包含对应 activation event,再调用 ext host proxy。
Extension host side 在 extHostExtensionService.ts 和 Node 版 extHostExtensionService.ts。它先等待 workspace 初始化和 ready barrier,再根据事件或 extension id 进入 _activateExtension()。真正执行之前,还要判断 ESM/CJS、加载模块、创建冻结的 ExtensionContext,包括 global/workspace state、secrets、subscriptions、extensionUri/path、storage/log URI、extensionMode、extensionRuntime 等。
最后才会检查 extensionModule.activate 是否是函数,并调用 activate(context)。成功后还会上报 code loading、activate call、activate resolved 等 timing;失败路径也会归因到对应 extension。
extensionsRegistry.ts 和 extensionDescriptionRegistry.ts 则解释了 manifest 的另一半:contributes schema、activationEvents schema、extension point 注册、activation event 到 extension description 的索引。也就是说,插件会先被注册和索引,再由事件触发激活,不是运行时临时扫一遍 package.json。
最后的判断:VS Code 的核心是可扩展平台骨架
读完这条链路,我对 VS Code 源码的判断是:它最值得学习的不是“编辑器 UI 怎么写”,而是一个大型桌面工具平台怎样把边界拆清楚。
Electron main 进程负责单实例、进程级配置、IPC channel、安全 session 和 native service。窗口层负责路径分类和 native configuration。Renderer bootstrap 负责把配置带进 Workbench。Workbench 用 service registry、DI 和 import side effects 组织服务与贡献点。Extension service 决定插件运行位置。Local extension host 通过 main 进程 utility process 启动。RPCProtocol 把 MainThread/ExtHost 双边接口接起来。Activation runtime 再把 manifest、activation event、loader、context、timing 和错误处理串成完整生命周期。
如果继续读 VS Code,建议不要从目录百科开始。按这条顺序走:src/main.ts、CodeMain、CodeApplication、WindowsMainService、renderer workbench bootstrap、DesktopMain、Workbench.startup()、workbench.desktop.main.ts、NativeExtensionService、localProcessExtensionHost.ts、extensionHostProcess.ts、extHost.protocol.ts、extHostExtensionService.ts。
VS Code 的项目价值,在于它把“桌面应用、服务容器、插件宿主、远程能力和 API 边界”做成了一套可持续扩展的骨架。源码阅读最该带走的,也正是这套骨架如何启动、连接、隔离和激活。
夜雨聆风