VSCode 插件开发经验小结

news2025/4/7 9:27:07

理解 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 还有一个很大特点就是多进程。主要的有以下两个进程:

  • 主进程:

    • 1、Electron 中运行 package.json 中的 main 脚本的进程被称为主进程,即 main.js 就是运行在主进程。

    • 2、一个 electron 应用有且只有一个主进程。

    • 3、只有主进程可以直接进行 GUI 相关的原生 API 操作。

  • 渲染进程:

    • 1、运行在 Chromium 的 web 页面姑且叫渲染进程,即运行 index.html 的环境就是渲染进程。

    • 2、一个 electron 应用可以有多个渲染进程。

    • 3、渲染进程在引入 Node.js 模块的前提下,可以在页面中和操作系统进行一些底层交互(如 fs 模块)。

    • 了解了 vscode 的底层设计,下面我们就以真实的需求(创建模板)来一步步探索 vscode 扩展开发。

  •  

                                  需求分析

    在 vscode 活动栏提供视图容器,透出创建模板入口,点击后打开可视化界面,进行简单配置后完成模板创建(注册模板信息到模板平台并生成对应的模板文件)。

    要实现以上功能,需要先提炼出几个和 vscode 相关功能:

  • 通过 vscode 指令系统,注册一个命令到菜单栏。

  • 创建一个用于配置的 web 页面。

  • 完成配置后上传配置信息并创建文件。

  • 完成配置后关闭 web 页面。

  •  

                                        逻辑实现

      注册指令

    要实现以上功能,需要先提炼出几个和 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>
  `;
}

由上面的代码可见,针对一个命令 / 函数,如果涉及到 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',
    },
  };
  进程通信

当我们实现 Webview 后,下一步是拉取数据,然后渲染到本地项目对应的路径中,可见这一步需要操作系统 api 的支持,我们需要使用 node 进程来做这件事。

那么问题来了,UI 是通过 html 字符串传给 vscode 进程的,他们之间是如何通信的呢。

开发 vscode 扩展最核心(恶心)的事情就是通信,单向的数据流导致不仅是 webview 和插件 node 进程通信复杂,即使在同一个 react 项目中的两个不同页面(webview)也是不能直接进行数据交互的。

流程如图:

vscode 在通信这里,只为我们提供了最简单粗糙的通信方法 —— acquirevscodeApi,这个对象里面有且仅有以下几个可以和插件通信的 API。

插件发送消息:

window.addEventListener('message', (event) => {
    const message = event.data;
    console.log(message);
});

WebView 给插件发消息:

window.addEventListener('message', (event) => {
    const message = event.data;
    console.log(message);
});

WebView 给插件发消息:

export const vscode = acquirevscodeApi();
vscode.postMessage('xxx');

插件接收消息:

通信封装

基于以上的进程通信方式,如果所有通信逻辑都通过 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 端:

需求实现

基于以上,视图层、逻辑层、通信层的框架就大致完成了,接下来就是基于需求本身实现视图(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

  • 如有侵权,请联系 LD546307@163.com 删除

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1886105.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

maketrans()方法——创建字符映射的转换表

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 语法参考 maketrans()方法用于创建字符映射的转换表&#xff0c;对于接受两个参数的最简单的调用方式&#xff0c;第一个参数是字符串&#xff0c;表…

【Kubernetes】加入节点Node及问题

命令 分别再node节点机器上&#xff0c;执行如下命令&#xff1a; kubeadm join [master机器ip:端口] --token [master机器初始化生成的token] --discovery-token-ca-cent-hash [master机器初始化生成的hash]问题 由于清屏没有记住token和hash的时候&#xff1a; 1&#xff…

外贸企业选择什么网络?

随着全球化的深入发展&#xff0c;越来越多的国内企业将市场拓展到海外。为了确保外贸业务的顺利进行&#xff0c;企业需要建立一个稳定、安全且高速的网络。那么&#xff0c;外贸企业应该选择哪种网络呢&#xff1f;本文将为您详细介绍。 外贸企业应选择什么网络&#xff1f; …

axios的底层ajax,XMLHttpRequest原理解释及使用方法

定义 ajax全称asychronous JavaScript and XML 意思是异步的 JavaScript和xml&#xff0c; 也就是通过javascript创建XMLHttpRequest &#xff08;xhr&#xff09;对象与服务器进行通信 步骤 创建实例对象&#xff0c;初始请求方法和url&#xff0c;设置监听器监听请求完成…

【Rust入门教程】安装Rust

文章目录 前言Rust简介Rust的安装更新与卸载rust更新卸载 总结 前言 在当今的编程世界中&#xff0c;Rust语言以其独特的安全性和高效性吸引了大量开发者的关注。Rust是一种系统编程语言&#xff0c;专注于速度、内存安全和并行性。它具有现代化的特性&#xff0c;同时提供了低…

超简洁Django个人博客系统(适合初学者)

一、环境介绍 Django4.2.13Markdown3.3.4PyMySQL1.1.1Python3.8PyCharm 2023.1.2 (Professional Edition) 二、功能简介 用户登录 通过在pycharm终端执行以下命令创建超级管理员。python manage.py create createsuperuser 创建完成后再通过新建的超级管理员账号进行登录 …

The First项目报告:NvirWorld与区块链游戏的未来

根据官方公告&#xff0c;The Fisrt现货区将于2024年7月2日16:00上架NVIR/USDT交易对&#xff0c;NVIR是NvirWorld平台的原生代币。作为一个去中心化解决方案&#xff0c;NvirWorld为开发者提供了一个简化且适应性强的环境&#xff0c;旨在通过优化的扩展解决方案来降低交易成本…

windows USB设备驱动开发-开发USB 设备端驱动

USB 设备是通过单个端口连接到计算机的外设&#xff0c;例如鼠标设备和键盘。 USB 客户端驱动程序是计算机上安装的软件&#xff0c;该软件与硬件通信以使设备正常运行。 如果设备属于 Microsoft 支持的设备类&#xff0c;Windows 会为该设备加载 Microsoft 提供的 USB 驱动程序…

如何离线安装ctags

首先下载一个ctags源码包&#xff0c;ctags-master.zip&#xff0c;拷贝到ubuntu20中&#xff0c; #unzip ctags-master.zip 找到README文件&#xff0c;找到install的描述&#xff1a; 运行ctags解压后的目录下的autogen.sh 发现缺少autoreconf, autoconf, automake 等一些…

问题集锦1

01.inner中使用JwtTokenUtil.getUserCode() 前端调用上传&#xff08;java&#xff09;&#xff0c;上传使用加购 Overridepublic Boolean insertShoppingCart(InsertShoppingCartParamsDto dto) {// 通过userCode,itemCode和supplierCode来判断当前加购人添加到购物车的商品是…

iminuit,一个神奇的 Python 库!

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 大家好&#xff0c;今天为大家分享一个神奇的 Python 库 - iminuit。 Github地址&#xff1a;https://github.com/scikit-hep/iminuit 在科学计算和数据分析领域&#xff0c;参数估计和最优化是非常重要的任务。…

【Windows】Visual Studio Installer下载缓慢解决办法

【Windows】Visual Studio Installer下载缓慢解决办法 1.背景2.分析3.结果 1.背景 使用visual studio在线安装包进行IDE安装&#xff0c;发现下载几乎停滞&#xff0c;网速几乎为零。 经过排查并不是因为实际网络带宽导致。 这里涉及DNS知识&#xff1b; DNS&#xff08;Dom…

Python学习速成必备知识,(20道练习题)!

基础题练习 1、打印出1-100之间的所有偶数&#xff1a; for num in range(1, 101):if num % 2 0:print(num) 2、打印出用户输入的字符串的长度&#xff1a; string input("请输入一个字符串&#xff1a;")print("字符串的长度为&#xff1a;", len(str…

【第二周】基础语法学习

目录 前言初始化项目文件介绍基本介绍JSWXMLWXSS 常见组件基础组件视图容器match-mediamovable-area/viewpage-containerscroll-viewswiper 表单组件自定义组件 模板语法数据绑定单向数据绑定双向数据绑定 列表渲染条件渲染模板引用 事件系统事件类型事件绑定阻止冒泡互斥事件事…

8617 阶乘数字和

这是一个关于计算阶乘结果所有位上的数字之和的问题。我们可以通过以下步骤来解决这个问题&#xff1a; 1. 首先&#xff0c;我们需要一个函数来计算阶乘。由于n的范围可以达到50&#xff0c;阶乘的结果可能非常大&#xff0c;所以我们需要使用一个可以处理大整数的数据类型&a…

[Information Sciences 2023]用于假新闻检测的相似性感知多模态提示学习

推荐的一个视频&#xff1a;p-tuning P-tunning直接使用连续空间搜索 做法就是直接将在自然语言中存在的词直接替换成可以直接训练的输入向量。本身的Pretrained LLMs 可以Fine-Tuning也可以不做。 这篇论文也解释了为什么很少在其他领域结合知识图谱的原因&#xff1a;就是因…

理解MySQL核心技术:触发器功能特点与应用案例解析

触发器&#xff08;Trigger&#xff09;是MySQL中一个重要的功能&#xff0c;它能够在特定的数据表操作发生时自动执行预定义的SQL语句&#xff0c;从而实现在数据库层面的自动化操作和数据维护。在这篇文章中&#xff0c;我们将进一步了解MySQL触发器的相关知识&#xff0c;包…

渲染100农场如何渲染全景图?渲染100邀请码1a12

全景图的制作需要渲染&#xff0c;以国内知名的渲染农场—渲染100为例&#xff0c;我来说下操作过程。 1、进入渲染100官网&#xff0c;点击右上角注册按钮完成注册&#xff0c;记得邀请码一栏填1a12&#xff0c;有30元礼包和2张免费渲染券。 渲染100官网&#xff1a;http://…

【C语言】extern 关键字

在C语言中&#xff0c;extern关键字用于声明一个变量或函数是定义在另一个文件中的。它使得在多个文件之间共享变量或函数成为可能。extern关键字常见于大型项目中&#xff0c;通常用于声明全局变量或函数&#xff0c;这些变量或函数的定义位于其他文件中。 基本用法 变量声明…