VSCode插件开发经验小结
VSCode (Visual Studio Code) 是微软开发的一款免费、开源的代码编辑器。它基于 Electron 框架构建,提供了丰富的开发者工具,支持多种编程语言,可以进行代码调试、版本控制、智能提示等功能,是很多开发者日常使用的工具。
理解 vscode,我们首先要谈的是 Electron。
Electron 的核心技术主要包括以下几个方面:
-
Chromium: Electron 使用了 Chromium 浏览器作为其渲染引擎。Chromium是 Google Chrome 的开源版本,负责处理和渲染应用程序的用户界面,包括 HTML、CSS 和 JavaScript。这使得开发者可以利用Web开发技术来构建应用的界面。
-
Node.js: Electron 集成了 Node.js,使得开发者可以在应用程序的主进程(后台)中运行 JavaScript 代码。Node.js 提供了对文件系统、网络、进程等系统级 API 的访问,增强了应用程序的功能和交互性。
-
Native API: Electron 提供了一套 API,允许主进程和渲染进程之间进行通信,以及调用操作系统级别的功能。这些 API 包括 ipcRenderer 和 ipcMain(用于进程间通信)、webContents(用于控制页面内容)等。
Electron 还有一个很大特点就是多进程。主要的有以下两个进程:
-
主进程
-
Electron 中运行 package.json 中的 main 脚本的进程被称为主进程,即 main.js 就是运行在主进程。
-
一个 electron 应用有且只有一个主进程。
-
只有主进程可以直接进行 GUI 相关的原生 API 操作。
-
渲染进程
-
运行在 Chromium 的 web 页面姑且叫渲染进程,即运行 index.html 的环境就是渲染进程。
-
一个 electron 应用可以有多个渲染进程。
-
渲染进程在引入 Node.js 模块的前提下,可以在页面中和操作系统进行一些底层交互(如 fs 模块)。
综上来看:在 Electron 应用中,web 页面可以通过渲染进程将消息转发到主进程中,进而调用操作系统的 native api。相比普通 web 应用,可开发扩展的能力更加灵活、丰富。
了解了 vscode 的底层设计,下面我们就以真实的需求(创建模板)来一步步探索 vscode 扩展开发。
![]()
需求分析
在 vscode 活动栏提供视图容器,透出创建模板入口,点击后打开可视化界面,进行简单配置后完成模板创建(注册模板信息到模板平台并生成对应的模板文件)。
要实现以上功能,需要先提炼出几个和 vscode 相关功能:
-
通过 vscode 指令系统,注册一个命令到菜单栏。
-
创建一个用于配置的 web 页面。
-
完成配置后上传配置信息并创建文件。
-
完成配置后关闭 web 页面。
![]()
逻辑实现
▐ 注册指令
初始化一个插件项目后,暴露在最外面的文件中包含 activate 和 deactvate 两个方法,这俩方法属于 vscode 插件的生命周期,最终会被 export 出去给 vscode 主动调用。而 onXXX 等事件是声明在插件 package.json 文件中的 Activation Events。声明这些 Activation Events 后,vscode 就会在适当的时机回调插件中的 activate函数。vscode 之所以这么设计,是为了节省资源开销,只在必要的时候才激活你的插件。
// package.json
"activationEvents": [
"onCommand:dinamicx.createTemplate",
...
],
"commands": [
{
"command": "dinamicx.createTemplate",
"title": "DX: 创建模板"
},
...
],
"menus": {
"view/title": [
{
"command": "dinamicx.createTemplate",
"group": "navigation@0",
"when": "view == dinamicx.views.main"
}
...
]
}
也可以在插件激活时注册命令:
import { createTemplate } from './commands/createTemplate';
export function activate(context: vscode.ExtensionContext) {
// 注册命令
vscode.commands.registerCommand('dinamicx.createTemplate', (info: any) => {
createTemplate(context, info.path);
})
...
}
上面这段代码的含义是将dinamicx.createTemplate
这个命令和函数绑定,具体的逻辑部分应该在createTemplate
这个方法中实现。
▐ 创建WebView
如果要创建一个页面,可以使用 vscode 提供的
api——vscode.window.createWebviewPanel:
export function createTemplate(
context: vscode.ExtensionContext,
dirPath: string,
) {
const panel = vscode.window.createWebviewPanel(
'createTemplate', // viewType
'创建模板页面', // 视图标题
vscode.ViewColumn.One, // 显示在编辑器的哪个部位
// 启用JS,默认禁用 // webview被隐藏时保持状态,避免被重置
{ enableScripts: true, retainContextWhenHidden: true },
);
...
const htmlContent = this.getHtmlContent(panel.webview, htmlPath);
panel.webview.html = htmlContent;
panel.reveal();
return panel;
}
具体渲染的页面可以通过 html 属性指定,但是 html 属性接收的参数是字符串!那么我们无法使用 vue/react 进行编码,只能写模板字符串了吗?
当然不是!我们可以先编写 react 代码,再打包成 js,套在 index.html 模板中 return 出来,问题就迎刃而解。处理这件事情的就是getHtmlContent
:
function getHtmlContent(webview, htmlPath) {
/*
各种资源的绝对路径
const getHTMLDependencies = () => (`
<!-- Dependencies -->
<script src="${highlightJs}"></script>
<script src="${reactJs}"></script>
<script src="${reactDomJs}"></script>
<script src="${antdJs}"></script>
`);
*/
const { getHTMLLinks, getHTMLDependencies } = useWebviewBasic(context);
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
${getHTMLLinks()}
</head>
<style>
body {
background-color: transparent !important;
}
</style>
<body>
<div id="root"></div>
${getHTMLDependencies()}
<!-- Main -->
<script src="vscode-resource:${htmlPath}"></script>
#{endOfBody}
</body>
</html>
`;
}
vscode-resource: 出于安全考虑,Webview 默认无法直接访问本地资源,它在一个孤立的上下文中运行。它只允许通过绝对路径访问特定的本地文件。
由上面的代码可见,针对一个命令/函数,如果涉及到 webview,只关注渲染代码(即 SPA 的 js 文件),不关心具体页面实现,所以可以将编写 UI 相关的逻辑,提炼到 node 主进程之外。
▐ React 和 Webpack
对于 vscode 插件来讲,UI 是独立的,所以我们可以像创建 react 项目一样来完成页面部分的代码。
const Template: React.FC = () => {
const [loading, setLoading] = useState(false);
...
return (
<Spin spinning={loading} tip={loadingText}>
<div className="template">
...
</div>
</Spin>
);
};
ReactDOM.render(<Template />, document.getElementById('root'));
在打包方面,刚才提到了我们要根据不同命令加载不同的页面组件,即不同的 js,所以打包的 entry 是多入口的;为了不重复引入公共库,将 react、antd 等库 external,选择通过 cdn 的方式引入。
const config = {
mode: env.production ? 'production' : 'development',
entry: {
template: createPageEntry('page-template'),
layout: createPageEntry('page-layout'),
view: createPageEntry('view-idl'),
...
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, '../dist/webview'),
},
...
externals: {
'react': 'root React',
'react-dom': 'root ReactDOM',
'antd': 'antd',
},
};
▐ 进程通信
vscode 在通信这里,只为我们提供了最简单粗糙的通信方法 —— acquirevscodeApi,这个对象里面有且仅有以下几个可以和插件通信的 API。
插件发送消息:
panel.webview.postMessage; // 支持发送任意被JSON化的数据
WebView 接收消息:
window.addEventListener('message', (event) => {
const message = event.data;
console.log(message);
});
WebView 给插件发消息:
export const vscode = acquirevscodeApi();
vscode.postMessage('xxx');
插件接收消息:
panel.webview.onDidReceiveMessage(
(message) => {
console.log('插件收到的消息:', message);
},
undefined,
context.subscriptions
);
通信封装
基于以上的进程通信方式,如果所有通信逻辑都通过 message 事件监听,那怎么知道某一处该接收哪些消息,该如何发送一个具有唯一标识的消息?
vscode 本身没有提供类似的功能,不过可以自己封装。
流程如图:
Webview端:
export abstract class App<> {
// private readonly _api: vscodeApi;
// 单向通信
protected sendCommand<TCommand extends IpcCommandType<any>>(
command: TCommand,
params: IpcMessageParams<TCommand>
): void {
const id = nextIpcId();
this.postMessage({ id: id, method: command.method, params: params });
}
// 双向通信
protected async sendCommandWithCompletion<
TCommand extends IpcCommandType<any>,
TCompletion extends IpcNotificationType<any>
>(
command: TCommand,
params: IpcMessageParams<TCommand>,
completion: TCompletion
): Promise<IpcMessageParams<TCompletion>> {
const id = nextIpcId();
const promise = new Promise<IpcMessageParams<TCompletion>>(
(resolve, reject) => {
let timeout: ReturnType<typeof setTimeout> | undefined;
const disposables = [
DOM.on(window, 'message', (e: MessageEvent<IpcMessage>) => {
onIpc(completion, e.data, (params) => {
if (e.data.completionId === id) {
disposables.forEach((d) => d.dispose());
queueMicrotask(() => resolve(params));
}
});
}),
{
dispose: function () {
if (timeout != null) {
clearTimeout(timeout);
timeout = undefined;
}
},
},
];
timeout = setTimeout(() => {
timeout = undefined;
disposables.forEach((d) => d.dispose());
debugger;
reject(
new Error(
`Timed out waiting for completion of ${completion.method}`
)
);
}, 600000);
}
);
this.postMessage({
id: id,
method: command.method,
params: params,
completionId: id,
});
return promise;
}
private postMessage(e: IpcMessage) {
this._api.postMessage(e);
}
}
Node端:
parent.webview.onDidReceiveMessage(this.onMessageReceivedCore, this),
onMessageReceivedCore(e: IpcMessage) {
if (e == null) return;
switch (e.method) {
case ExecuteCommandType.method:
onIpc(ExecuteCommandType, e, params => {
if (params.args != null) {
void executeCommand(params.command as Commands, ...params.args);
} else {
void executeCommand(params.command as Commands);
}
});
break;
default:
this.provider.onMessageReceived?.(e);
break;
}
}
// commands.ts
import { commands } from 'vscode';
export function executeCommand<U = any>(command: Commands): Thenable<U>;
export function executeCommand<T = unknown, U = any>(command: Commands, arg: T): Thenable<U>;
export function executeCommand<T extends [...unknown[]] = [], U = any>(command: Commands, ...args: T): Thenable<U>;
export function executeCommand<T extends [...unknown[]] = [], U = any>(command: Commands, ...args: T): Thenable<U> {
return commands.executeCommand<U>(command, ...args);
}
基于以上,视图层、逻辑层、通信层的框架就大致完成了,接下来就是基于需求本身实现视图(react)和逻辑(node)的实现了。
希望此文能帮助大家快速对 vscode 插件开发有一定了解。后续会再介绍基于 vscode 的 DX 插件和使用建议、以及提高 vscode 开发效率的配置分享~
参考资料
-
Introduction | Electron (electronjs.org):
https://www.electronjs.org/docs/latest/?spm=ata.21736010.0.0.317e4797PUtlD0
-
Webview API | Visual Studio Code Extension API:
https://code.visualstudio.com/api/extension-guides/webview?spm=ata.21736010.0.0.317e4797PUtlD0
团队介绍
我们是淘天集团 - 终端体验平台团队,立足于淘宝体验平台及集团移动中台定位,致力于无线端到端前沿技术探索,深入终端厂商到原生系统技术挖掘,打造集团先进且行业领先的终端基础设施及配套服务,涵盖多端性能体验、终端技术服务、原生技术研发、用户增长触达等关键领域的工作,为阿里巴巴数百款活跃App提供研发与性能支撑,即是集团终端技术生态的基石团队之一,也是淘天双11核心支撑团队之一!
本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
vivo 互联网自研代码评审 VCR 落地实践
作者:vivo 互联网效能平台团队- Chi Wei 本文介绍了vivo工程效能团队基于 Gitlab、Gerrit等开源工具搭建的VCR平台,代码评审idea插件开发及开发过程中遇到的挑战、困难,并分享了相应的应对策略和优化方案。 代码评审是软件质量保证一种活动,由一个或者多个人对一个程序的部分或者全部源代码进阅读理解。一般来说分为作者和评审者两种角色,作者方提供代码逻辑的介绍和代码,评审者则对提供的代码基于设计,功能性和非功能性等方面认知进行阅读并提出问题。常见的评审组织形式是有同行评审(Peer Review)和小组检查 (Team Inspection)两种方式。 在代码评审中,评审的目的在通过代码的评审发现潜在的问题,同时分享和表达是代码评审的重要收获,我们知道人相同在不同的文化下生产力是不同的,代码评审是一个工具,工具受文化的影响的同时也影响着文化,最终朝着我们希望的责任共担、持续改进的方向发展。 一、代码评审演进 随着互联网的发展,开发人员也越来越重视代码评审带来的代码的代码质量提高以及代码评审间接带来的分享及人员备份效果,已经不满足于只是简单的发现当前问题解决问题记录问...
- 下一篇
GaussDB关键技术原理:高性能(三)
GaussDB关键技术原理:高性能(二)从查询处理综述对GaussDB的高性能技术进行了解读,本篇将从查询重写RBO、物理优化CBO、分布式优化器、布式执行框架、轻量全局事务管理GTM-lite等五方面对高性能关键技术进行分享。 目录 3 高性能关键技术 3.1 查询重写RBO 3.2 物理优化CBO 3.3 分布式优化器 3.4 分布式执行框架 3.5 轻量全局事务管理GTM-lite 3 高性能关键技术 内容概要:本章节介绍GaussDB中实现的高性能关键技术,内容涉及优化器、执行器、分布式数据库、存储引擎等多个方面。 目的:通过对GaussDB数据库关键高性能技术的学习,能够让读者更加清晰的理解数据库内核哪些优化是性能关键点同时也为类似的应用系统实现提供方法论和最佳实践。 3.1 查询重写RBO 在数据库里RBO基于规则的优化一般指查询重写技术,按照一系列关系代数表达式的等价规则,对查询的关系代数表达式进行等价转换,从逻辑上减少执行的总量从而提高查询执行效率,例如,通过条件的推导得出非必要的表扫描、避免非必要的计算表示等。 查询重写RBO优化是非常重要的一种逻辑优化手段,通常应用...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Mario游戏-低调大师作品
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题