vscode插件原理浅析与实战

是这样的,想请问一下,vscode插件原理浅析与实战
最新回答
落花ぴ满天飞

2026-10-21 12:48:54

背景

作为一位前端同学肯定对vscode不陌生,相信每位同学电脑上也都有五花八门的个性化配置,那么我们是借助什么东西做到的呢?那就是它丰富的插件生态。本次将讲述插件基本原理并从一个简单的case了解如何制作一个的vscode插件

是什么实现了vscodeElectron

vscode底层通过electron开发实现,electron的核心构成分别是:chromium、nodejs、native-api

Chromium(ui视图):通过web技术栈编写实现ui界面,其与chrome的区别是开放开源、无需安装可直接使用(可以简单理解chromium是beta体验版chrome,新特性会优先在chromium中体验并在稳定后更新至chrome中)。

Nodejs(操作桌面文件系统):通过node-gyp编译,主要用来操作文件系统和调用本地网络。

Native-API(操作系统纬度api):使用Nodejs-C++Addon调用操作系统API(Nodejs-C++Addon插件是一种动态链接库,采用C/C++语言编写,可以通过require()将插件加载进NodeJS中进行使用),可以理解是对Nodejs接口的能力拓展。

Electron多进程:

主进程(main):每一个Electron应用只会启动一个主进程。

渲染进程(render):主进程会通过Chromium的api创建任意多个web页面,每一个工作区(workbench)对应一个进程,同时是BrowserWindow实例,由于chromeium(chrome)也是多进程的,所以每个页面都单独运行在各自的渲染进程中。

例:

//主进程const{ipcMain}=require('electron');//主进程响应事件ipcMain.on('main_msg',(event,arg)=>{console.log(arg);//pingevent.reply('renderer-msg-reply','pong');})

//渲染进程(子进程)const{ipcRenderer}=require('electron');//渲染进程响应事件ipcRenderer.on('renderer-msg-reply',(event,arg)=>{console.log(arg);//pong})//触发主进程响应事件ipcRenderer.send('main_msg','ping');

对于vscode还会有一些其他的进程,比如:

插件进程(Extension):fork渲染进程,每个插件都运行在一个NodeJS宿主环境中,即插件间共享进程

Debug进程:一个特殊的插件进程。

Search进程:搜索是密集型任务,单独占用一个进程。

。。。

通俗意义上,electron就是给你搞了一个Chrome浏览器的壳子,只是比传统网页多了一个访问桌面文件的功能。

vscode插件加载基本原理

https://github.com/microsoft/vscode/tree/main

插件的结构├──extensions----------------------------------vscode内置插件├──src│├──main.js--------------------------------入口文件│├──bootstrap-fork.js----------------------衍生子进程(渲染进程)│├──vs││└──workbench-------------------------工作台││├──base│││├──browser----------------------浏览器api,可操作dom│││├──common-----------------------公共js代码│││├──node-------------------------nodejsapi││├──code│││├──electron-browser-------------electron渲染进程│││├──electron-main----------------electron主进程

插件加载过程初始化插件服务

在插件初始化构造函数中通过_initialize初始化插件服务。

//src/vs/workbench/services/extensions/electron-browser/extensionService.ts//通过监听生命周期函数,创建ExtensionHostManagerexportclassExtensionServiceextendsAbstractExtensionServiceimplementsIExtensionService{constructor(){this._lifecycleService.when(LifecyclePhase.Ready).then(()=>{//rescheduletoensurethisrunsafterrestoringviewlets,panels,andeditorsrunWhenIdle(()=>{this._initialize();//初始化插件服务},50/*maxdelay*/);});}}//src/vs/workbench/services/extensions/common/abstractExtensionService.ts//启动初始化插件服务方法protectedasync_initialize():Promise<void>{perf.mark('code/willLoadExtensions');this._startExtensionHosts(true,[]);//...}private_startExtensionHosts(isInitialStart:boolean,initialActivationEvents:string[]):void{//创建插件进程,分别为LocalProcessExtensionHost(本地插件,如个人插件)、RemoteExtensionHost(远程插件,如WSLRemote)、WebWorkerExtensionHost(webworker进程)constextensionHosts=this._createExtensionHosts(isInitialStart);extensionHosts.forEach((extensionHost)=>{//创建ExtensionHostManagerconstprocessManager:IExtensionHostManager=createExtensionHostManager(this._instantiationService,extensionHost,isInitialStart,initialActivationEvents,this._acquireInternalAPI());processManager.onDidExit(([code,signal])=>this._onExtensionHostCrashOrExit(processManager,code,signal));processManager.onDidChangeResponsiveState((responsiveState)=>{this._onDidChangeResponsiveChange.fire({isResponsive:responsiveState===ResponsiveState.Responsive});});this._extensionHostManagers.push(processManager);});}

fork渲染进程

fork渲染进程,并加载extensionHostProcess。由于vscode考虑插件可能会影响启动性能和IDE自身的稳定性,所以通过进程隔离来解决这个问题,插件进程fork渲染进程,保证每个插件都运行在一个nodejs宿主环境中,不影响IDE及其启动时间。

//src/vs/workbench/services/extensions/common/extensionHostManager.ts//启动fork渲染进程classExtensionHostManagerextendsDisposable{constructor(){this._proxy=this._extensionHost.start()!.then();}}

//src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.tsclassLocalProcessExtensionHostimplementsIExtensionHost{publicstart():Promise<IMessagePassingProtocol>|null{//...constopts={env:objects.mixin(objects.deepClone(process.env),{//加载插件进程,指明插件进程入口AMD_ENTRYPOINT:'vs/workbench/services/extensions/node/extensionHostProcess',}),}//衍生子进程(渲染进程)this._extensionHostProcess=fork(getPathFromAmdModule(require,'bootstrap-fork'),['--type=extensionHost'],opts);}}

初始化插件激活逻辑//src/vs/workbench/services/extensions/node/extensionHostProcess.tsimport{startExtensionHostProcess}from"vs/workbench/services/extensions/node/extensionHostProcessSetup";startExtensionHostProcess().catch((err)=>console.log(err));//src/vs/workbench/services/extensions/node/extensionHostProcessSetup.tsexportasyncfunctionstartExtensionHostProcess():Promise<void>{constextensionHostMain=newExtensionHostMain(renderer.protocol,initData,hostUtils,uriTransformer);}//src/vs/workbench/services/extensions/common/extensionHostMain.tsexportclassExtensionHostMain{constructor(){//必须在创建extensionService之后再调用initialize,因为initialize本身会依赖extensionService的实例this._extensionService=instaService.invokeFunction(accessor=>accessor.get(IExtHostExtensionService));this._extensionService.initialize();}}

插件激活//src/vs/workbench/api/node/extHost.services.tsimport{ExtHostExtensionService}from'vs/workbench/api/node/extHostExtensionService';//注册插件服务registerSingleton(IExtHostExtensionService,ExtHostExtensionService);

继承AbstractExtHostExtensionService

//src/vs/workbench/api/node/extHostExtensionService.tsexportclassExtHostExtensionServiceextendsAbstractExtHostExtensionService{//...}

//src/vs/workbench/api/common/extHostExtensionService.tsabstractclassAbstractExtHostExtensionServiceextendsDisposable{constructor(){this._activator=this._register(newExtensionsActivator());}//根据activationEvent事件名激活插件,如onCommandprivate_activateByEvent(activationEvent:string,startup:boolean):Promise<void>{returnthis._activator.activateByEvent(activationEvent,startup);}}

加载流程

简单实战

背景:实现选择指定目录右键自动生成lynx页面基本目录结构的插件。

目标拆解:

选择自定义目录,添加右键点击菜单

输入lynx页面名称

按照模版生成对应文件

环境准备

nodejs

vscode

安装Yeoman和VSCodeExtensionGenerator

//渲染进程(子进程)const{ipcRenderer}=require('electron');//渲染进程响应事件ipcRenderer.on('renderer-msg-reply',(event,arg)=>{console.log(arg);//pong})//触发主进程响应事件ipcRenderer.send('main_msg','ping');0

初始化项目工程

//渲染进程(子进程)const{ipcRenderer}=require('electron');//渲染进程响应事件ipcRenderer.on('renderer-msg-reply',(event,arg)=>{console.log(arg);//pong})//触发主进程响应事件ipcRenderer.send('main_msg','ping');1

具体实现//渲染进程(子进程)const{ipcRenderer}=require('electron');//渲染进程响应事件ipcRenderer.on('renderer-msg-reply',(event,arg)=>{console.log(arg);//pong})//触发主进程响应事件ipcRenderer.send('main_msg','ping');2

main:指定了插件的入口函数。

activationEvents:指定触发事件,当指定事件发生时才触发插件执行。需额外关注*这个特殊的插件类型,因为他在初始化完成后就会触发插件执行,并不需要任何自定义触发事件。

contributes:描述插件的拓展点,用于定义插件要扩展vscode哪部分功能,如commands命令面板、menus资源管理面板等。

声明指令

初始化插件项目成功后会看到上图的目录结构,其中我们需要重点关注src目录和package.json文件,其中src目录下的extension.ts文件为入口文件,包含activate和deactivate分别作为插件启动和插件卸载时的生命周期函数,可以将逻辑直接写在两个函数内也可抽象后在其中调用。

同时我们希望插件在适当的时机启动activate或关闭deactivate,vscode也给我们提供了多种onXXX的事件作为多种执行时机的入口方法。那么我们该如何使用这些事件呢?

事件列表

//渲染进程(子进程)const{ipcRenderer}=require('electron');//渲染进程响应事件ipcRenderer.on('renderer-msg-reply',(event,arg)=>{console.log(arg);//pong})//触发主进程响应事件ipcRenderer.send('main_msg','ping');3

如何使用这些事件呢?我们以onCommand为例。首先需要在package.json文件中注册activationEvents和commands。

//渲染进程(子进程)const{ipcRenderer}=require('electron');//渲染进程响应事件ipcRenderer.on('renderer-msg-reply',(event,arg)=>{console.log(arg);//pong})//触发主进程响应事件ipcRenderer.send('main_msg','ping');4

然后在extension.ts文件的activate方法中编写自定义逻辑。

//渲染进程(子进程)const{ipcRenderer}=require('electron');//渲染进程响应事件ipcRenderer.on('renderer-msg-reply',(event,arg)=>{console.log(arg);//pong})//触发主进程响应事件ipcRenderer.send('main_msg','ping');5

添加目录右键点击事件

//渲染进程(子进程)const{ipcRenderer}=require('electron');//渲染进程响应事件ipcRenderer.on('renderer-msg-reply',(event,arg)=>{console.log(arg);//pong})//触发主进程响应事件ipcRenderer.send('main_msg','ping');6

唤起组件名称输入面板//渲染进程(子进程)const{ipcRenderer}=require('electron');//渲染进程响应事件ipcRenderer.on('renderer-msg-reply',(event,arg)=>{console.log(arg);//pong})//触发主进程响应事件ipcRenderer.send('main_msg','ping');7

//渲染进程(子进程)const{ipcRenderer}=require('electron');//渲染进程响应事件ipcRenderer.on('renderer-msg-reply',(event,arg)=>{console.log(arg);//pong})//触发主进程响应事件ipcRenderer.send('main_msg','ping');8

根据输入面板创建模版文件//渲染进程(子进程)const{ipcRenderer}=require('electron');//渲染进程响应事件ipcRenderer.on('renderer-msg-reply',(event,arg)=>{console.log(arg);//pong})//触发主进程响应事件ipcRenderer.send('main_msg','ping');9

可优化点

增加模版类型

通过下载模版替代写入字符串文本

End

??谢谢支持

以上便是本次分享的全部内容,希望对你有所帮助^_^

喜欢的话别忘了?分享、点赞、收藏?三连哦~。

欢迎关注公众号?ELab团队?收获大厂一手好文章~

我们来自字节跳动,是旗下大力教育前端部门,负责字节跳动教育全线产品前端开发工作。

我们围绕产品品质提升、开发效率、创意与前沿技术等方向沉淀与传播专业知识及案例,为业界贡献经验价值。包括但不限于性能监控、组件库、多端技术、Serverless、可视化搭建、音视频、人工智能、产品设计与营销等内容。

欢迎感兴趣的同学在评论区或使用内推码内推到作者部门拍砖哦?

字节跳动校/社招投递链接:

https://job.toutiao.com/s/FCbeMRg

内推码:GDUVRJJ

原文:https://juejin.cn/post/7099838116789387295