vscode 作为一款网红 IDE,其丰富的插件让人叹为观止,通过 vscode 提供的插件机制,我们几乎可以自定义 vscode 的所有细节。事实上很多 vscode 的核心功能也是通过插件实现的。
本文我们将从以下三个方面详述 vscode 的插件机制:
插件的基本结构
插件执行环境
插件的运行流程
阅读本文后续内容,需要对 vscode 的插件开发有基本的了解。关于 vscode 的插件开发可参考 vscode 的官方教程 。
1. 插件基本结构
vscode 的官方教程 中有详细的插件开发文档;并且在 github 上提供了丰富插件案例, 从 UI 定制到代码自动补全都有可借鉴性很高的 demo。本文并不打算详诉插件开发的细节,我们更多的关注 vscode 是如何设计和实现这套插件架构的。
我们首先从一个插件项目的 package.json
来了解其基本结构。 package.json
中 main 指定了插件的入口函数,而 contributes 和 activationEvents 分别描述的插件的扩展点和触发事件。如下面代码所示:
"main": "./out/extension.js","contributes": {
"commands": [ {
"command": "extension.helloWorld", "title": "Hello World" } ]}"activationEvents": [ "onCommand:extension.helloWorld"]
扩展点和触发事件是两个比较重要的概念,我们先简单解释下,后续讲到插件注册时再详细描述。
contributes(扩展点)用于定义插件要扩展 vscode 哪部分功能;vscode 暴露出多个扩展点,包括 commands (命令面板)、configuration (配置模板)等
activationEvents(触发事件)用于定义插件何时执行,当指定的事件发生时插件才会执行
有一类特殊的插件的 activationEvents
为通配符 *
,这类插件称为 EagerExtensions
,它们会在插件环境初始化完成后自动执行,而不需要其他事件触发。
2. 插件执行环境
vscode 的第三方插件的质量往往难以保证,因此需要设计一套隔离机制,将核心功能和第三方插件的执行环境隔离,保证第三方插件挂了,vscode 的核心功能依然可用。由于不想增加理解成本,本文所讲的 vscode 插件机制是指纯 web 版 vscode,和基于 electron 的桌面版 vscode 原理类似。
插件环境初始化入口的在 vs/workbench/services/extensions/browser/extensionService.ts
的构造函数中, _initialize
函数中的第一步就是初始化插件执行环境。
protected async _initialize(): Promise<void> {
perf.mark('willLoadExtensions'); this._startExtensionHostProcess(true, []); //启动插件worker进程,建立rpc通道,注入api this.whenInstalledExtensionsRegistered().then(() => perf.mark('didLoadExtensions')); await this._scanAndHandleExtensions(); this._releaseBarrier();}
2.1 环境隔离与通信
(1)worker 进程创建
插件环境初始化的第一步就是创建一个 webworker 用于运行插件逻辑,防止不可信的插件影响到 vscode 核心功能。通过调用 vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter.ts
的 start
方法创建一个新的webworker作为插件的执行环境。
const url = getWorkerBootstrapUrl(require.toUrl('../worker/extensionHostWorkerMain.js'), 'WorkerExtensionHost');const worker = new Worker(url, { name: 'WorkerExtensionHost' });
(2)主进程与 worker 信道建立
webWorkerExtensionHostStarter.ts
的 start
方法在创建 webworker 后就封装一个 IMessagePassingProtocol
接口返回。如下代码所示:
start(){
..... const protocol: IMessagePassingProtocol = {
onMessage: emitter.event, send: vsbuf => {
const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteO