likes
comments
collection
share

还不会开发 VSCode 插件?看这一篇就够了

作者站长头像
站长
· 阅读数 16

前言

VSCode 插件区域如图所示,下图所示的区域都可以进行插件扩展。

本文不会深入讲解全部 API,主要讲解如何在 VSCode Activity Bar 点位,使用 React 语法,开发一个 Webview 界面插件。

还不会开发 VSCode 插件?看这一篇就够了

Demo 项目

仓库链接

1. 搭建插件骨架,搭建 Webview 视图

1.1 使用 VSCode 官方脚手架创建插件项目

npm install -g yo generator-code
yo code

还不会开发 VSCode 插件?看这一篇就够了

按照脚手架提示,我们选择 New Extension (TypeScript),并且选择使用 Webpack 打包,便可以创建出一个最简的插件项目。

1.2 注册自定义侧边栏

首先我们按照 VSCode 官方文档,在插件项目 package.json 中注册自定义侧边栏,配置插件所需配置如下:

{
  // 插件激活时机
  "activationEvents": [
    "onView:vs-sidebar-view"
  ],
  "main": "./dist/extension.js",
  "contributes": {
    // 往 activity bar 添加一个容器
    "viewsContainers": {
      "activitybar": [
        {
          "id": "vs-sidebar-view",
          "title": "VSCode extension demo",
          "icon": "icon.svg"
        }
      ]
    },
    // 往容器中添加一个 webview 视图
    "views": {
      "vs-sidebar-view": [
        {
          "type": "webview",
          "id": "vs-sidebar-view",
          "name": "demo page"
        }
      ]
    }
  },
}

这一步是为了增加一个自定义的 VSCode 侧边栏,如下图所示。

还不会开发 VSCode 插件?看这一篇就够了

1.3 创建一个 webview 视图容器

vscode 包提供 webview 容器的 TS 实现类型,我们根据 vscode.WebviewViewProvider 类型,初始化 webview 容器构造函数。

一个容器必须要实现 resolveWebviewView 方法。在下方代码中,我们配置了 webview 允许加载 js script 文件,并且配置了静态资源的 root 路径。

最后为 webviewView.webview.html 属性挂载 html 模板字符串。如果 webview 能成功渲染,预期在侧边栏界面中看到 123。

export class SidebarProvider implements vscode.WebviewViewProvider {
  constructor(protected context: vscode.ExtensionContext) {}

  public resolveWebviewView(webviewView: vscode.WebviewView) {
    webviewView.webview.options = {
      enableScripts: true,
      localResourceRoots: [this.context.extensionUri],
    };

    webviewView.webview.html = `<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>test</title>
  </head>
  <body>
    <div id="root">123</div>
  </body>
</html>`
  }
}

1.4 在插件入口文件挂载视图容器

每个 VSCode 插件,入口文件 extension.ts 都必须声明一个 activate 方法,该方法会在 VSCode 插件被激活后作为回调函数被触发。

我们在插件入口文件 activate 方法中,使用 registerWebviewViewProvider API 渲染 Webview 视图容器。

// ./src/extension.ts
// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
  const sidebarPanel = new SidebarProvider(context);
  context.subscriptions.push(
    vscode.window.registerWebviewViewProvider('vs-sidebar-view', sidebarPanel)
  );
}

1.5 打包插件,本地调试查看效果

VSCode 脚手架直接生成了插件的 Webpack 打包配置,并且提供了本地调试的方式。

我们直接在插件项目中按 F5,即可开启 webpack --watch 模式,若插件项目打包成功,会弹出一个 Extension Development Host 的 VSCode 界面调试窗口。

我们查看效果,可以看到 123 已经被渲染到页面上,至此我们已经成功将 webview 视图渲染。

还不会开发 VSCode 插件?看这一篇就够了

2. 使用 React 渲染 Webview

我们在 Webview 中,也想像开发平台页面一样开发 React 组件,并且引入 arco-design 等 UI 组件,如下图所示。

还不会开发 VSCode 插件?看这一篇就够了

我们知道 Webpack 的 HtmlWebpackPlugin 插件提供了注入 bundle script 的能力。

但该插件并不满足我们的使用场景。原因是 VSCode 要求 webview 挂载的 html 文件中 script 的 src 路径必须为 webviewUri 的绝对路径,而该插件无法转换 srcwebview 路径。

2.1 定义 html 模板文件

为了能在 html 文件中,加载 React 项目打包后的 bundle,我们手写一个 html 模板文件,该文件用来渲染 webview 页面,在模板中我们预留出 <script> 插槽和 id="root" 的 dom 节点。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>test</title>
  </head>
  <body>
    <div id="root"></div>
    {{#each scriptUris}}
    <script src="{{this}}"></script>
    {{/each}}
  </body>
</html>

2.2 提供 React 的入口文件

像开发普通页面一样,需要提供一个 React 的入口文件。

这里我们只渲染一个 <Button>demo button</Button>的按钮

import React from 'react';
import ReactDom from 'react-dom';

import { createGlobalStyle } from 'styled-components';
import { Button } from '@arco-design/web-react';
import '@arco-design/web-react/dist/css/arco.css';

const GlobalStyle = createGlobalStyle`
  html, body {
    height: 100vh;
    padding: 20px;
    background: yellow;
  }
`;

interface ISidebarProps {}

const Sidebar: React.FC<ISidebarProps> = () => {
  return (
    <>
      <GlobalStyle />
      <Button>demo button</Button>
    </>
  );
};

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

2.3 配置 React 打包配置

下方省略了一些配置,我们要做的是,支持将 entry 对应的入口文件编译为对应的 [name].bundle.js 文件。

由于 webpack 支持多配置,我们直接将 React 打包配置放入同一个 webpack.config.js 中,这样可以在插件打包的同时,一块打包 webview 组件。

/** @type WebpackConfig */
const configForWebview = merge(commonConfig, {
  mode: 'development',
  entry: {
    sidebar: path.resolve(__dirname, './src/Webview/index.tsx'),
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js',
  },
  // ignore some logics...
});

module.exports = [extensionConfig, configForWebview];

在该 case 中,打包完成之后会输出 ./dist/sidebar.bundle.js 文件

2.4 提供渲染模板方法

下方逻辑为将 script 注入到 html 模板之中。

import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import Handlebars from 'handlebars';

const makeUriAsWebviewUri = (
  context: vscode.ExtensionContext,
  webviewView: vscode.WebviewView,
  uri: string
) => {
  return webviewView!.webview
    .asWebviewUri(vscode.Uri.file(path.resolve(context.extensionPath, uri)))
    .toString();
};

/**
 * inject params to template
 */
export const getHtmlForWebview = (
  context: vscode.ExtensionContext,
  webviewView: vscode.WebviewView,
  bundleName: string
) => {
  const htmlTemplateUri = path.resolve(
    context.extensionPath,
    './dist/index.html'
  );
  const content = fs.readFileSync(htmlTemplateUri, 'utf-8');

  const template = Handlebars.compile(content);

  // inject params to template
  const sidebarBundleWebViewUri = makeUriAsWebviewUri(
    context,
    webviewView,
    `./dist/${bundleName}.bundle.js`
  );

  const html = template({
    scriptUris: [sidebarBundleWebViewUri],
  });

  return html;
};

2.5 改造 webview 视图容器

上节我们已经实现了渲染模板方法,我们改造 1.3 节中的 webview 容器,使用 getHtmlForWebview 方法获取 html。完成 React 渲染的 webview 挂载。

import * as vscode from 'vscode';
import { getHtmlForWebview } from './utils';

export class SidebarProvider implements vscode.WebviewViewProvider {
  constructor(protected context: vscode.ExtensionContext) {}

  public resolveWebviewView(webviewView: vscode.WebviewView) {
    // ignore some logics ...
    webviewView.webview.html = getHtmlForWebview(
      this.context,
      webviewView,
      'sidebar'
    );
  }
}

至此完成了 webview 视图的挂载,但是我们只是光有一个 Webview 视图,没有任何交互行为,接下来讲一下 webview 如何与 VSCode 通信。

3. Webview 与 VSCode 如何通信

3.1 VSCode 插件侧

Webview 视图容器属性 webviewView.webview.onDidReceiveMessage 提供了监听 webview 层发送到 VSCode 侧事件的能力,这种通信方式类似于 postMessage

import * as vscode from 'vscode';
import { getHtmlForWebview } from './utils';
import { init, test } from './handler';

export interface Message {
  type: string;
  payload?: Record<string, any>;
}

export class SidebarProvider implements vscode.WebviewViewProvider {
  constructor(protected context: vscode.ExtensionContext) {}

  public resolveWebviewView(webviewView: vscode.WebviewView) {
    // ignore some logics...
    webviewView.webview.onDidReceiveMessage(async (message: Message) => {
      webviewView.webview.postMessage({
        type: message.type,
        payload: {
          message: msg,
        },
      });
    });
  }
}

3.2 Webview 侧

3.2.1 监听 VSCode 插件侧发送到 Webview 中的事件

window.addEventListener('message', handler);

3.2.2 Webview 向 VSCode 插件侧发送事件

vscode.postMessage(message);

3.2.3 具体使用方式如下

const vscode = (window as any).acquireVsCodeApi();

const Sidebar: React.FC<ISidebarProps> = () => {
  useEffect(() => {
    window.addEventListener('message', providerMessageHandler);
    vscode.postMessage({ type: 'init' });
    return () => {
      window.removeEventListener('message', providerMessageHandler);
    };
  }, []);
  // ignore some logics...

4. 如何在 Webview 中调用项目 npm scripts 命令

4.1 实际场景

现在我们有一个项目,下方是项目的 npm scripts。其中 test 命令的功能是,当执行 test 命令之后,1s 后输出 Success 日志。

// index.js
setTimeout(() => {
  console.log('Success');
}, 1000);
// package.json
{
    "scripts": {
        "test": "node index.js"
    }
}

现在我们想要实现一个功能,我们不想让用户手动执行 npm run test 命令,想让用户当点击 webview 中 invoke test script 按钮后,可以直接执行脚本命令。

并且当右侧脚本 log 执行完毕之后,改变 webview 中视图文案为 Success

还不会开发 VSCode 插件?看这一篇就够了 还不会开发 VSCode 插件?看这一篇就够了

4.2 webview 侧操作

在 Webview 中,我们放置一个按钮组件。

点击之后,向 VSCode 插件侧发送一个 type: 'test' 的事件,并且在 useEffect 中监听 message 事件,如果脚本执行完毕,就改变 UI 视图。

import React, { useEffect, useState } from 'react';
import ReactDom from 'react-dom';
import { Spin, Button } from '@arco-design/web-react';
import '@arco-design/web-react/dist/css/arco.css';

const vscode = (window as any).acquireVsCodeApi();

const Sidebar: React.FC<ISidebarProps> = () => {
  const [testText, setTestText] = useState('');
  const [loading, setLoading] = useState(false);

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

  const providerMessageHandler = function (event: any) {
    const data = event.data;
    const { type, payload } = data;
    if (type === 'test') {
      setTestText(payload.message);
      setLoading(false);
    }
  };

  return (
    <>
      <GlobalStyle />

      {!testText ? (
        <Button
          onClick={() => {
            vscode.postMessage({ type: 'test' });
            setLoading(true);
          }}
          loading={loading}
        >
          invoke test script
        </Button>
      ) : (
        testText
      )}
    </>
  );
};

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

4.3 VSCode 侧操作

4.3.1 监听 webview 发送的事件

我们使用 webviewView.webview.onDidReceiveMessage 监听 Webview 发送过来的事件,当收到 type: 'test' 事件后,执行 test() 函数,执行完毕后,向 webview 发送事件告知执行结果,下方我们讲解 test 函数。

export class SidebarProvider implements vscode.WebviewViewProvider {
  constructor(protected context: vscode.ExtensionContext) {}

  public resolveWebviewView(webviewView: vscode.WebviewView) {
    // ignore some logics...
    webviewView.webview.onDidReceiveMessage(async (message: Message) => {
      let msg = '';

      switch (message.type) {
        case 'test': {
          msg = await test();
          break;
        }
      }

      webviewView.webview.postMessage({
        type: message.type,
        payload: {
          message: msg,
        },
      });
    });
  }
}

4.3.2 在 VSCode 插件侧执行事件对应项目脚本

为了执行项目脚本命令,我们需要用到 child_processspawn 命令。

执行命令之后,我们需要监听脚本的日志输出,来判断脚本是否运行成功,这里使用 child.stdout.on 方法,监听脚本输出的每一行日志。

在这个 case 中 index.js 会在 1s 之后输出 Success。所以如果日志输出 Success,代表脚本已执行完毕,


import { spawn } from 'child_process';
import * as vscode from 'vscode';

export const test = () => {
  return new Promise<string>((resolve, reject) => {
    const child = spawn('npm', ['run', 'test'], {
      cwd: getWorkspaceRootPath(),
    });
    child.stdout.on('data', (data: any) => {
      const dataString = data?.toString() || '';
      if (dataString.includes('Success')) {
        resolve(dataString);
      }
    });
    child.stderr.on('data', (data: any) => {
      reject(Status.Fail);
    });
    child.on('close', (code: any) => {
      if (code !== 0) {
        reject(Status.Fail);
      }

      resolve(Status.Success);
    });
  });
};

结语

VSCode 插件可以提供 Webview 界面供用户直接操作,用户无需了解 CLI 工具底层实现,作为 GUI 工具,是一种更好的消费形式。