VSCode插件开发(二)-React实现一个复杂的webview

在上一篇文章 - 最简单的一个VSCode插件中,我们学习了如何创建一个简单的VSCode插件,实现了一个基本的图片调整大小功能。然而,那个插件的用户界面比较简单,只能通过输入框来设置图片宽度。在这篇文章中,我们将学习如何使用React、Tailwind CSS、Shadcn和Vite来创建一个更复杂、更美观的Webview界面,提供更丰富的图片编辑功能。

1. 环境准备

1.1 回顾上一篇项目

我们将在上一篇创建的letsdoit-image插件基础上进行扩展。如果你还没有完成上一篇的内容,请先按照那篇文章创建一个基本的VSCode插件项目。

1.2 首先创建一个最简单的webview

package.json中声明一个新的命令:

1
2
3
4
5
6
7
8
"commands": [
//... other commands
{
"command": "letsdoit-image.editImage",
"title": "图片快修"
}
//... other commands
]

然后在src/extension.ts中注册并添加命令的实现代码,现在我们只是简单的显示文本Edit Image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const editImageDisposable = vscode.commands.registerCommand('letsdoit-image.editImage', async () => {
// create webview panel
const panel = vscode.window.createWebviewPanel(
'letsdoit-image.editImage',
'Edit Image',
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
panel.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Image</title>
</head>
<body>
<div>Edit Image</div>
</body>
</html>`;
});
// 注册命令
context.subscriptions.push(editImageDisposable);

注意:在上面的代码中,我们使用了enableScripts: true, 这里必须设为true,否则后面我们的React应用将无法运行。另外,我们还设置了retainContextWhenHidden: true,这意味着当Webview面板被隐藏时,它的上下文将被保留,而不是被销毁。这对于我们的插件来说是非常重要的,因为我们希望用户能够在编辑图片后,能够继续在VSCode中进行其他操作,比如编辑其他的文件。

我们按F5运行插件,在新打开的VSCode窗口中按Ctrl+Shift+P,输入图片快修,即可打开我们的插件。顺利的话,我们应该看到下面的界面:

最简单的webview

1.3 然后创建一个React项目来实现webview的内容

首先,我们需要在插件项目中创建一个用于Webview的React应用。我们将使用Vite来创建一个React项目,因为它提供了快速的开发体验和优秀的构建性能。

在插件项目根目录下执行以下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建一个React + TypeScript项目
npm create vite@latest webview -- --template react-ts

# 进入webview目录
cd webview

# 安装依赖
npm install

# 安装Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# 安装Shadcn UI
npx shadcn-ui@latest init
npx shadcn-ui@latest add button card input label slider tabs

# 安装其他需要的依赖
npm install lucide-react

1.4 配置Tailwind CSS

  • 修改webview/tsconfig.json中配置compilerOptions如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
  • 同样的,还要修改webview/tsconfig.app.json中配置compilerOptions如下:
1
2
3
4
5
6
7
8
9
10
{
"compilerOptions": {
//... other configurations
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
//... other configurations
}
}
  • webview/src/index.css中引入Tailwindcss, 删除原来内容。
1
@import 'tailwindcss';
  • 最后,我们修改下vite的配置文件vite.config.ts,使其支持tailwindcss:
1
2
3
4
5
6
7
8
9
10
11
// other code
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
}
})

2. 开发React Webview界面

2.1 实现图片编辑功能

webview/src/App.tsx中实现图片编辑功能,由于内容比较多,这里就不贴代码了,如果有兴趣可以查看完整代码

2.2 配置Vite构建

我们需要修改vite.config.ts文件,将构建输出目录改为../out/,并将入口js文件名改为assets/index.js,因为默认生成的js文件名会带上hash值,而我们需要在src/extension.ts中引用这个文件,所以简单起见这里需要指定为固定文件名。

1
2
3
4
5
6
7
8
9
10
11
12
export default defineConfig({
// other code
build: {
outDir: '../out/',
rollupOptions: {
output: {
entryFileNames: 'assets/index.js',
assetFileNames: 'assets/[name].[ext]',
},
}
},
})

3. 将React应用集成到webview中

3.1 调整webview显示内容

修改src/extension.ts文件,添加Webview相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const jsUri = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'out', 'assets', 'index.js'));
const cssUri = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'out', 'assets', 'index.css'));

panel.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="${cssUri}">
<title>Edit Image</title>
</head>
<body>
<div id="root"></div>
<script src="${jsUri}"></script>
</body>
</html>`;

注意:上面代码中,我们将React应用的入口js文件和css文件都加载到了webview中,这是因为我们需要在webview中运行React应用。这是将React应用的内容嵌入到webview的关键步骤。另外,我们注意到panel.webview.asWebviewUri方法用于将插件项目中的文件路径转换为webview可以访问的URI,这是因为webview访问插件本地资源有特殊的安全控制,必须转换成WebviewUri才能访问。

3.2 处理下载文件的问题

由于webview运行在一个独立的环境中,直接使用浏览器的下载功能可能会受到限制。为了解决这个问题,我们可以通过VSCode的消息传递机制,将下载请求发送回扩展主进程,由主进程来处理文件保存。

  • 首先在下载图片的逻辑中,我们发送一个消息给扩展主进程,请求保存图片:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 在VSCode插件环境中,使用postMessage与插件通信
    if (vscode) {

    // 从data URL中提取base64数据和图片格式
    const mimeType = imageToDownload.match(/^data:(image\/[a-z]+);base64,/)?.[1] || 'image/png';
    const base64Data = imageToDownload.replace(/^data:image\/[a-z]+;base64,/, '');

    // 根据MIME类型确定文件扩展名
    let extension = 'png';
    if (mimeType.includes('jpeg') || mimeType.includes('jpg')) {
    extension = 'jpg';
    } else if (mimeType.includes('png')) {
    extension = 'png';
    } else if (mimeType.includes('gif')) {
    extension = 'gif';
    } else if (mimeType.includes('webp')) {
    extension = 'webp';
    }

    // 发送消息给VSCode插件,请求保存文件
    vscode.postMessage({
    command: 'saveImage',
    data: base64Data,
    fileName: `edited-image.${extension}`
    });
    } else {
    // 在普通浏览器环境中使用原有下载方式
    }
  • 然后在extension.ts中添加消息监听,处理保存图片请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 处理来自webview的消息
panel.webview.onDidReceiveMessage(
async (message) => {
console.log('message received from webview', message);
switch (message.command) {
case 'saveImage':
try {
// 获取用户选择的保存位置
const saveUri = await vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file(message.fileName),
filters: {
'Images': ['png', 'jpg', 'jpeg', 'gif', 'webp']
}
});

if (saveUri) {
// 将base64数据转换为Buffer
const imageBuffer = Buffer.from(message.data, 'base64');

// 写入文件
await vscode.workspace.fs.writeFile(saveUri, imageBuffer);

// 显示成功消息
vscode.window.showInformationMessage(`图片已保存到: ${saveUri.fsPath}`);
}
} catch (error) {
vscode.window.showErrorMessage(`保存图片失败: ${error}`);
}
return;
}
},
undefined,
context.subscriptions
);

4. 测试插件

至此,我们已经完成了代码的编写,可以开始测试了。

我们先在webview目录下运行npm run build,确保React应用的代码已经构建完成。

  1. 在VSCode中打开插件项目
  2. F5 或点击”运行 > 启动调试”
  3. 在新打开的VSCode窗口中,Ctrl+Shift+P打开命令面板,输入”图片快修”
  4. 在打开的Webview中编辑图片
  5. 点击”下载”按钮保存编辑后的图片

5. 打包和发布

一切顺利,我们该打包了。

5.1 添加构建脚本

在插件项目的package.json中添加构建脚本:

1
2
3
4
5
6
7
8
9
10
11
{
...
"scripts": {
"vscode:prepublish": "npm run compile && npm run build-webview",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"build-webview": "cd webview && npm run build",
"package": "vsce package"
},
...
}

如果我们还没有安装vsce,可以使用以下命令安装:

1
npm install -g vsce

5.2 打包插件

运行以下命令打包插件:

1
npm run package

成功后,会在项目根目录下生成一个.vsix文件,这就是我们的插件安装包。

5.3 发布插件

按照上一篇文章中的步骤,将插件发布到VSCode Marketplace。

6. 总结

在这篇文章中,我们学习了如何使用React、Tailwind CSS、Shadcn和Vite来创建一个复杂的Webview界面,并将其集成到VSCode插件中。这个插件我们已经发布到VSCode Marketplace,你可以在插件市场搜索”图片快修”来安装它。

但是现在还有个问题,就是我们打的包有10多MB,这对于一个简单的插件来说是比较大的。后面一篇文章我们会介绍如何优化插件的大小,正常情况下,我们这个插件应该在100K以内。
希望这篇文章能帮助你进一步了解VSCode插件开发!如果你有任何问题或建议,欢迎在评论区留言讨论。