百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

还不会 vscode 插件开发?vscode 任务栏插件

bigegpt 2024-09-11 01:00 3 浏览

vscode 插件开发。对于大部分前端同学,相信都用过不少 vscode 插件,比如我们耳熟能详的美化插件 prettier,又或是类 web gpt 的插件 code-gpt,一个好的 vscode 插件在我们的开发流程中,可以起到显著的提效,对开发的流畅度又或是上限提高都是非常重要的。

因为工作内容的关系,很多前端同学并不会自己去开发一个 vscode 插件,更多的是和 web 环境打交道。但事实上,不论是从技术提升上,还是个人发展上,我都非常建议大家学习 & 具备 vscode 插件开发的能力。

从技术提升上, vscode 插件不同于 web 环境,运行于 node 运行时下,对于一个前端服务领域相关知识的有着显著帮助;而从个人发展上,作为前端基建的重要一环,不管是面试大厂业务团队,还是架构团队,掌握 vscode 插件开发都是一个不错的加分项。例如下面字节 web-infra 的一条职位要求中,就明确要求了需要熟悉 vscode 插件开发。

但 vscode 插件开发 API, 分支都有一定的学习成本,加上文档全英文,对一些新入门的前端同学上手的确不够友好。出于这个背景,所以我打算出一些关于 vscode 插件实战相关的技术系列文章,帮助大家更快上手 & 具备 vscode 插件开发的基本能力,


今天这一节我将为大家介绍 vscode 插件的一种常见载体 vscode 任务栏插件。

vscode 任务栏插件是一种集成到 vscode 左侧任务栏的插件形式,通过在任务栏注册 webview 来实现自己插件能力的常驻化,在这种模式下,用户可以通过点击任务栏完成 webview 的插件操作,适合需要图形界面 & 频繁引流的插件场景。比如下图的 code-gpt 插件

因为篇幅限制,今天的这节分享里我不会过多介绍 vscode 插件的一些基础 API 和项目架构,在学习过程中有遇到不清楚的 API 或是目录结构大家可以到官方文档中查询或者直接评论交流都是可以的。本次分享的内容包含以下几个模块:

  • 如何注册一个可以绑定在任务栏的 webview?
  • 如何使用 react 开发 webview?
  • 如何完成 webview 和 extension 的相互通信?
  • 如何在三方逻辑中调起已注册 webview?

下面我们进入正题,开始这四个模块的学习

正文

如何注册一个可以绑定在任务栏的 webview?

Webview提供了标准的WebAPI和JavaScript接口,可以与VSCode API交互,并与VSCode本身的功能进行合作。例如,开发者可以使用Webview在VSCode中添加自定义的编辑器窗口,使用V Code API进行代码编辑和高亮。同时,Webview还提供了协议处理、界面元素和用户界面管理等丰富的功能,使得开发者可以打造出丰富的应用程序。

webview 简单来说就是一个内置的 web 浏览器,我们可以通过类开发 web 的方式完成 vscode 插件图形界面的开发。在 VSCode 官方文档中,webview 被区分为两种类型, 任务栏 Webview 称为 StatusBarItem,而通常的 Webview 称为 WebviewPanel,它们在 VS Code UI 中所处的区域也不同。

关于 WebviewPanel,在文档中是这样描述的:

WebviewPanel 是可以在编辑器区域打开的 Webview,它包含了一个 HTML 渲染引擎和一个 JavaScript 运行时,与 VS Code 上下文相隔离。

而针对 StatusBarItem,则在文档中被描述为:

StatusBarItem 表示现实在 VS Code 编辑器下方的状态栏中的一个项目,该项目可用于显示信息或进行交互。

这样区分的原因也是为了在交互设计上,让开发者将真正重要需要引流的 webview 放到 StatusBarItem,从而避免了各个插件 webview 位置百花齐放,造成的视觉负面冲击。

所以对于任务栏的 webview 我们使用 WebviewPanel 的常规注册方式,不论我们如何调整位置也是无法将它移动到 StatusBarItem 区域的,下面是一个 WebviewPanel 的注册方式。


import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  const panel = vscode.window.createWebviewPanel(
    'myWebview',
    'My Webview',
    vscode.ViewColumn.One,
    {}
  );
  panel.webview.html = `
    <html>
      <body>
        <h1>Hello, World!</h1>
      </body>
    </html>
  `;
  context.subscriptions.push(
    vscode.commands.registerCommand('myExtension.showWebview', () => {
      panel.reveal();
    })
  );
}

那么言归正传,我们应该如何创建一个任务栏的 webview 呢?StatusBarItem 的注册并不能直接通过某个 api 完成,我们需要继承一个 vscode.WebviewViewProvider 的基类,实现一个 provider 类后完成对应 webview 的注入,例如下面的 case

// mainWebviewProvider.ts
import * as path from 'path';
import * as vscode from 'vscode';

/**
 * 主任务 webview provider
 */
export class MainWebviewProvider implements vscode.WebviewViewProvider {
  private mainContext: vscode.ExtensionContext;
  private dirPath = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
  protected view: vscode.WebviewView | null = null;

  constructor(context: vscode.ExtensionContext, path?: string) {
    this.mainContext = context;
    if (path) {
      this.dirPath = path;
    }
  }

  public getInstance() {
    return this.view;
  }

  /**
   * webview provider 入口函数
   * @param webviewView
   */
  public resolveWebviewView(webviewView: vscode.WebviewView) {
    this.view = webviewView;
    this.view.webview.options = {
      enableScripts: true,
      localResourceRoots: [vscode.Uri.file(path.join(this.mainContext.extensionPath, 'dist'))] // 安全路径
    };
    this.view?.webview.onDidReceiveMessage((data) => {
      this.dirPath = data.path;
    });

    this.view.webview.html = this.getWebviewContent();

    // TODO demo, webview 调起 vscode 示例, 根据实际情况调整
    this.view.webview.onDidReceiveMessage(
      (message) => {
        if (message.method === 'showMessage') {
          vscode.window.showInformationMessage(message.params.text);
        }
      },
      undefined,
      this.mainContext.subscriptions
    );
  }

  public getWebviewContent() {
    return `<!DOCTYPE html>
        <html lang="en">
          <head>
            <meta charset="UTF-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <title>test</title>
            <script defer="defer" src="${this.view?.webview.asWebviewUri(
      vscode.Uri.file(path.join(this.mainContext.extensionPath, 'dist', 'bundle.js'))
    )}"></script>
          </head>
          <body>
            <div id="root"></div>
          </body>
        </html>`;
  }
}

在上面的 case 中,resolveWebviewView 作为整个 provider 类初始化的入口函数,其中 view 的 protected 变量则为 provider 类创建出来的实例,通过这个实例,我们可以远程控制 webview 的一些操作,也可以进行一些通信。完成这个 provider 类后,我们就可以在 extensions 入口文件完成注册。

// extension.ts
export function activate(context: vscode.ExtensionContext) {
  const sidebarPanel = new MainWebviewProvider(context);

  const mianWebview = vscode.window.registerWebviewViewProvider('test', sidebarPanel, {
    webviewOptions: { retainContextWhenHidden: true }
  });

  context.subscriptions.push(mianWebview, duplicateCode);
}

我们通过 vscode.window.registerWebviewViewProvider完成 webview 的注册,并注入给 vscode 插件实例集context.subscriptions。其中 test 为 webview 的 唯一 id,这部分需要在 package.json 中体现出来,因为 vscode 相关的配置都通过 package.json 读取。


// package.json
"views": {
  "test": [
    {
      "type": "webview",
      "id": "test",
      "name": ""
    }
  ]
},
"viewsContainers": {
  "activitybar": [
    {
      "id": "test",
      "title": "test",
      "icon": "images/code.svg"
    }
  ]
},

在上面的配置中,views 对应的注册的所有 webview,而 activitybar 则对应 vscode 插件的任务栏插件注册,id 的部分与 views 中注册的 webview 对应,title 的部分则是任务栏 hover 上去后展示的 tooltip, icon 则对应任务栏展示的 icon,在完成上面的部分以后我们启动插件就能在任务栏左侧看到对应的插件项了,例如下图

如何使用 react 开发 webview?

对于 webview 开发,官方提供的案例是使用原生的 html 和 js 完成开发,就比如上面提到的 case


import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  const panel = vscode.window.createWebviewPanel(
    'myWebview',
    'My Webview',
    vscode.ViewColumn.One,
    {}
  );
  panel.webview.html = `
    <html>
      <body>
        <h1>Hello, World!</h1>
      </body>
    </html>
  `;
  context.subscriptions.push(
    vscode.commands.registerCommand('myExtension.showWebview', () => {
      panel.reveal();
    })
  );
}

上面的 case 中webview.html注入的 html 字符串,所以我们只需要拿到目标页面的 html 即可,至于过程是用什么框架还是原生就并不重要了,

方案一:服务器端渲染

react 服务器渲染的 api 中有一个rendertoString方法,可以拿到渲染组件的 html 字符串,但这时候生成的 html 字符串是没有 css 和绑定事件的,我们可以按照服务器端渲染的方式实现一遍。


方案二:bundle 注入

react 转 html 大部分同学可能第一反应会想到使用 html-webpack-plugin 转换,但在 vscode 场景下是不能走通的,所有的 cdn 资源会走 vscode 的路径特殊处理,仅用相对路径生成的 html 是没办法软链到对应的 bundle 的。

既然如此,但我们可以手动完成 bundle 的注入,首先我们额外配置一个 webpack config 用于 webview 资源的打包,比如下面的 case。

'use strict';

const path = require('path');

const webviewConfig = {
  target: 'node',
  mode: 'production',
  entry: './src/webview/App.tsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  node: {
    __dirname: false,
  },
  externals: {
    vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed,  -> https://webpack.js.org/configuration/externals/
  },
  resolve: {
    extensions: ['.ts', '.js', '.tsx', '.jsx']
  },
  module: {
    rules: [
      {
        test: /.css$/i,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /.tsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react']
          }
        }
      }
    ]
  }
};

module.exports = webviewConfig;

也就是说,我们需要两套 webpack 配置,一套是脚手架默认生成的,用于打包 node 环境下的 extension 代码,另一个则是上面提到的,用来将 dom 打包成对应的 bundle,用于页面的注入。

然后我们正常配置 react 的相关项目配置,配置一个入口文件,将相关的内容注入到指定 dom 中。


import React from 'react';
import { render } from 'react-dom';
import { TestComponent } from './components/TestComponent';
import useParams from './hooks/useParams';

const App = () => {
  // ... your dom
};

render(<App />, document.getElementById('root'));

紧接着通过 webpack 打包我们不难拿到一个 bundle 文件,这时候我们手动将 bundle 注入到 webview 的 html 中

return `<!DOCTYPE html>
        <html lang="en">
          <head>
            <meta charset="UTF-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <title>AI Code Deduplication</title>
            <script defer="defer" src="${this.view?.webview.asWebviewUri(
      vscode.Uri.file(path.join(this.mainContext.extensionPath, 'dist', 'bundle.js'))
    )}"></script>
          </head>
          <body>
            <div id="root"></div>
          </body>
        </html>`;
  }

其中 asWebviewUri 可以将本地资源转化为 webview 中的 uri,而 vscode.Uri.file 则是帮我们拿到具体资源在 vscode 中的实际位置,通过这种方式我们就可以成功链接到我们的本地的 bundle 资源。

需要值得一提的是,在 webview 实体注册的时候,我们需要申请安全资源路径,来保证这次访问不会被 vscode 拦截,如下。

this.view.webview.options = {
  enableScripts: true,
  localResourceRoots: [vscode.Uri.file(path.join(this.mainContext.extensionPath, 'dist'))] // 安全路径
};

到这里我们 react 的配置就完成了,我们启动 extension 就可以看到对应的 dom 了

那么一些同学可能会有疑问,在 vscode webview 开发中,怎么看控制台呢?我们可以调起 vscode 提供的开发者工具,就可以和浏览器开发页面一样开发 webview 了。

如何完成 webview 和 extension 的相互通信?

第三个模块就是 webview 开发的重头戏了,我们知道 vscode extensions 是运行在 node 运行时下的,相比常规的浏览器环境,node 是 v8 引擎的一个修改版,提供了文件系统等交互能力,而 webview 运行在浏览器环境中。这也导致两种环境原则上是无法直接交互的(比如依赖调用等)。

但 webview 的本质作用就是希望在图形界面的基础上,调起 vscode 的插件能力,那么它们之间的相互通信就显得尤为重要了。

webview 向 extension 通信

vscode 的 webview 与常规浏览器有所不同,它在全局环境下注入了一个 acquireVsCodeApi,通过使用这个 api 完成实例的注册,我们就可以在 webview 沙盒中与 extension 产生通信,比如下面的例子

// 浏览器环境,webview
const vscode = acquireVsCodeApi();
vscode.postMessage({
  method: 'showMessage',
  params: {
    text: `click the dirPath`
  }
});
// node 环境,extension
this.view.webview.onDidReceiveMessage(
  (message) => {
    if (message.method === 'showMessage') {
      vscode.window.showInformationMessage(message.params.text);
    }
  },
  undefined,
  this.mainContext.subscriptions
);

不过需要注意的是,acquireVsCodeApi 只能被调用一次,当调用多次的时候会报错,所以我们这部分逻辑不要在 react function 中实现,避免重渲染机制导致的实例重建。

// app.tsx
import React from 'react';
import { render } from 'react-dom';
import { TestComponent } from './components/TestComponent';
import useParams from './hooks/useParams';

// @ts-ignore
const vscode = acquireVsCodeApi();

const App = () => {
  // 注入到组件中,避免重复注册
  return <TestComponent vscode={vscode} />;
};

render(<App />, document.getElementById('root'));

extension 向 webview 通信

extension 向 webview 的通信我们可以使用 postMessage 完成,因为 webview 本质是一个 vscode 环境下的一个沙盒浏览器,在 webview 中我们通过监听 message 事件来拿到指定方法

// node 环境,extension
this.view?.webview.postMessage({
    // ... 
});

webview 中的读取我们可以封装成一个 hooks

import { useState, useEffect } from 'react';

const useParams = () => {
  const [webviewParams, setWebviewParams] = useState<Record<string, any>>({});

  useEffect(() => {
    const messageHandler = (event) => {
      setWebviewParams(event.data);
    };

    window.addEventListener('message', messageHandler);
    return () => window.removeEventListener('message', messageHandler);
  }, []);

  return webviewParams;
};

export default useParams;

如何在三方逻辑中调起已注册 webview?

上面我们介绍了怎么开发一个 vscode 任务栏插件,那么如果我们除了希望能从左侧任务栏打开,还想能通过其他三方途径调起我们应该怎么做呢?我们来看下面的 case

import * as vscode from 'vscode';
import MainWebviewProvider from './cores/main-webview-provider/mainWebviewProvider';

export function activate(context: vscode.ExtensionContext) {
  const sidebarPanel = new MainWebviewProvider(context);

  const mainWebview = vscode.window.registerWebviewViewProvider('test', sidebarPanel, {
    webviewOptions: { retainContextWhenHidden: true }
  });

  const duplicateCode = vscode.commands.registerCommand('test.other-command', (uri) => {
    vscode.commands.executeCommand('workbench.view.extension.test');

    const dirPath = `/${uri.path.substring(1)}`;
    sidebarPanel.setDirPath(dirPath);
  });

  context.subscriptions.push(mainWebview, duplicateCode);
}

export function deactivate() { }

在上面的 case 中,我们使用了vscode.commands.executeCommand在test.other-command命令中手动调起了上面我们注册的 test webview,通过这种方式我们可以在三方调起插件工作空间内注册的 webview,这样我们的任务栏插件启动也就并非只能局限在任务栏了。


都快 2024 年了还不会 vscode 插件开发? ---- vscode 任务栏插件
原文链接:https://juejin.cn/post/7312724111399239743

相关推荐

最全的MySQL总结,助你向阿里“开炮”(面试题+笔记+思维图)

前言作为一名编程人员,对MySQL一定不会陌生,尤其是互联网行业,对MySQL的使用是比较多的。对于求职者来说,MySQL又是面试中一定会问到的重点,很多人拥有大厂梦,却因为MySQL败下阵来。实际上...

Redis数据库从入门到精通(redis数据库设计)

目录一、常见的非关系型数据库NOSQL分类二、了解Redis三、Redis的单节点安装教程四、Redis的常用命令1、Help帮助命令2、SET命令3、过期命令4、查找键命令5、操作键命令6、GET命...

netcore 急速接入第三方登录,不看后悔

新年新气象,趁着新年的喜庆,肝了十来天,终于发了第一版,希望大家喜欢。如果有不喜欢看文字的童鞋,可以直接看下面的地址体验一下:https://oauthlogin.net/前言此次带来得这个小项目是...

精选 30 个 C++ 面试题(含解析)(c++面试题和答案汇总)

大家好,我是柠檬哥,专注编程知识分享。欢迎关注@程序员柠檬橙,编程路上不迷路,私信发送以下关键字获取编程资源:发送1024打包下载10个G编程资源学习资料发送001获取阿里大神LeetCode...

Oracle 12c系列(一)|多租户容器数据库

作者杨禹航出品沃趣技术Oracle12.1发布至今已有多年,但国内Oracle12C的用户并不多,随着12.2在去年的发布,选择安装Oracle12c的客户量明显增加,在接下来的几年中,Or...

flutter系列之:UI layout简介(flutter-ui-nice)

简介对于一个前端框架来说,除了各个组件之外,最重要的就是将这些组件进行连接的布局了。布局的英文名叫做layout,就是用来描述如何将组件进行摆放的一个约束。在flutter中,基本上所有的对象都是wi...

Flutter 分页功能表格控件(flutter 列表)

老孟导读:前2天有读者问到是否有带分页功能的表格控件,今天分页功能的表格控件详细解析来来。PaginatedDataTablePaginatedDataTable是一个带分页功能的DataTable,...

Flutter | 使用BottomNavigationBar快速构建底部导航

平时我们在使用app时经常会看到底部导航栏,而在flutter中它的实现也较为简单.需要用到的组件:BottomNavigationBar导航栏的主体BottomNavigationBarI...

Android中的数据库和本地存储在Flutter中是怎样实现的

如何使用SharedPreferences?在Android中,你可以使用SharedPreferencesAPI来存储少量的键值对。在Flutter中,使用Shared_Pref...

Flet,一个Flutter应用的实用Python库!

▼Flet:用Python轻松构建跨平台应用!在纷繁复杂的Python框架中,Flet宛如一缕清风,为开发者带来极致的跨平台应用开发体验。它用最简单的Python代码,帮你实现移动端、桌面端...

flutter系列之:做一个图像滤镜(flutter photo)

简介很多时候,我们需要一些特效功能,比如给图片做个滤镜什么的,如果是h5页面,那么我们可以很容易的通过css滤镜来实现这个功能。那么如果在flutter中,如果要实现这样的滤镜功能应该怎么处理呢?一起...

flutter软件开发笔记20-flutter web开发

flutterweb开发优势比较多,采用统一的语言,就能开发不同类型的软件,在web开发中,特别是后台式软件中,相比传统的html5开发,更高效,有点像c++编程的方式,把web设计出来了。一...

Flutter实战-请求封装(五)之设置抓包Proxy

用了两年的flutter,有了一些心得,不虚头巴脑,只求实战有用,以供学习或使用flutter的小伙伴参考,学习尚浅,如有不正确的地方还望各路大神指正,以免误人子弟,在此拜谢~(原创不易,转发请标注来...

为什么不在 Flutter 中使用全局变量来管理状态

我相信没有人用全局变量来管理Flutter应用程序的状态。毫无疑问,我们的Flutter应用程序需要状态管理包或Flutter的基本小部件(例如InheritedWidget或St...

Flutter 攻略(Dart基本数据类型,变量 整理 2)

代码运行从main方法开始voidmain(){print("hellodart");}变量与常量var声明变量未初始化变量为nullvarc;//未初始化print(c)...