一、前言
在 上一篇文章 中,我介绍了服务于区块开发的命令行工具是如何实现的,本文将沿着区块开发这一主题继续讲解 VSCode
插件的实现方式。
本系列总共 3 篇文章,以下是传送门:
基于区块开发(一):概述
基于区块开发(二):命令行工具
基于区块开发(三):VSCode插件
二、区块列表展示
如果刚接触 VSCode
插件开发,可以先看看我之前写的 VS Code插件开发介绍。
为了方便用户使用,我希望有一个专门的 tab 页分类列出所有的区块,先看一下效果:
要在左侧工具栏添加 tab,需要先在 package.json
文件中配置一个 View Container
和一个 View
。
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "tce-block",
"title": "TCE Block",
"icon": "media/block.svg"
}
]
},
"views": {
"tce-block": [
{
"id": "tceBlock",
"name": "TCE Block",
"icon": "media/dep.svg",
"contextualTitle": "TCE Block"
}
]
}
}
这里指定了 tab 的位置放在左侧工具栏 activitybar
,另一个可选项是 panel
,在编辑器底部(终端)的位置。然后再给刚添加的这个 View Container
指定一个 View
,通过 tce-block
这个 ID 进行关联。
下一步就是定制 View
的显示内容了。由于显示的内容就是一棵目录树,所以用到了 VSCode
插件开发中内置的 Tree View API。下面我们来定义一棵树,关键是实现 vscode.TreeDataProvider
这一接口:
class BlockProvider implements vscode.TreeDataProvider<Block> {
constructor(private workspaceRoot: string) {}
getTreeItem(element: Block): vscode.TreeItem {
return element;
}
async getChildren(element?: Block) {
if (!this.workspaceRoot) {
return [];
}
// 不是根目录,返回元素的children
if (element) {
if (element.children.length > 0) {
return element.children;
}
return null;
}
// 根目录,构造树形数据结构
else {
const resp = await fetchData<BlockCategories>(
'http://xxx.com/block-categories.json'
);
if (!resp) {
vscode.window.showErrorMessage('获取区块列表失败');
return [];
}
const { blocks } = resp;
return toBlock(blocks);
}
}
}
这里的重点是实现 getChildren
方法,返回树的数据结构。这里有 2 种情况:
- 参数
element
为空时,说明是根目录,需要构造出树的第一层数据结构(数组)。 - 参数
element
非空时,返回子节点数组。
toBlock
函数的作用是构造出树的所有节点。我设计的树只有 2 层,第一层是区块分类,第二层是区块实例:
function toBlock(categories: BlockItem[]): Block[] {
// 区块分类
return categories.map((category) => {
const { id, label, children } = category;
const categoryItem = new Block(
id,
label,
'',
vscode.TreeItemCollapsibleState.Collapsed
);
// 区块实例
categoryItem.children = children!.map((blockItem) => {
const { id: blockId, label: blockLabel, url } = blockItem;
const block = new Block(blockId, blockLabel, url!);
block.type = blockItem.type;
return block;
});
return categoryItem;
});
}
下面再来看树节点的定义,继承自 vscode.TreeItem
:
export class Block extends vscode.TreeItem {
children: Block[] = [];
type: number = 2;
constructor(
public readonly id: string,
public readonly label: string,
public readonly url: string,
public readonly collapsibleState?: vscode.TreeItemCollapsibleState
) {
super(label, collapsibleState);
this.id = id;
this.tooltip = `${this.label}区块`;
// 区块实例
if (url) {
this.contextValue = 'block'; // 控制操作按钮的显示隐藏
this.command = {
title: this.label, // 标题
command: 'tceBlock.openWebview', // 命令 ID
tooltip: this.label, // 鼠标覆盖时的小小提示框
arguments: [this], // 向 registerCommand 传递的参数。
};
}
}
}
Block
的定义是包含所有类型的节点的(区块分类和区块实例),所以需要根据构造函数传入的值来定义不同的行为,比如这里对于区块实例,会有 url
属性,点击他会打开一个 webview
,这块在下一节详细讲解。
到此为止,树形结构已经能正常展示了。
三、预览区块
区块的预览本质上来说就是在 vscode
里面打开一个网页,这里就用到了 Webviews API。核心代码如下:
let currentPanel: vscode.WebviewPanel | undefined = undefined;
export function openWebView(url: string) {
if (currentPanel) {
currentPanel.dispose();
}
currentPanel = vscode.window.createWebviewPanel(
'tceBlock',
'TCE Block',
vscode.ViewColumn.One,
{
retainContextWhenHidden: true, // 控制是否保持webview面板的内容(iframe),即使面板不再可见。
enableScripts: true, // 下面的 html 页可以使用 Scripts
}
);
currentPanel.webview.html = getWebviewContent(url);
// Reset when the current panel is closed
currentPanel.onDidDispose(() => {
currentPanel = undefined;
}, null);
}
function getWebviewContent(url: string) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
<style>
html,
body {
margin: 0 !important;
padding: 0 !important;
width: 100%;
height: 100%;
}
#blockFrame {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<iframe id='blockFrame' src="${url}" scrolling="auto"></iframe>
</body>
</html>`;
}
代码很好理解,分为以下几步:
- 新建一个
webviewPanel
- 设置
webviewPanel
的html
- 在
html
中嵌入一个iframe
来动态加载网页
这个操作会注册成 vscode
的一个命令,然后在点击区块实例的时候被调用:
vscode.commands.registerCommand('tceBlock.openWebview', (node: Block) => {
openWebView(node.url);
});
export class Block extends vscode.TreeItem {
constructor(...) {
...
// 区块实例
if (url) {
...
this.command = {
command: 'tceBlock.openWebview', // 执行上面定义的命令
arguments: [this], // 向 registerCommand 传递的参数
...
};
}
}
}
四、安装区块
通过区块列表安装
我们希望插入区块这个操作显示在区块实例的边上,当鼠标移动到对应区块时被激活:
这就需要在 package.json
文件中定义这个操作:
"contributes": {
"menus": {
"view/item/context": [
{
"command": "tceBlock.addBlock",
"when": "view == tceBlock && viewItem == block",
"group": "inline"
}
]
}
}
具体的区块插入代码跟 上一篇文章 大同小异,在此就不重复了。这里会用到一些 VSCode
的 API,如通过对话框的方式获取用户希望区块插入的位置:
const options: vscode.OpenDialogOptions = {
title: '请选择区块插入位置',
openLabel: '插入区块',
canSelectMany: false,
canSelectFiles: false,
canSelectFolders: true,
};
const fileUri = await vscode.window.showOpenDialog(options);
insertPath = fileUri[0].fsPath;
通过上下文菜单安装
为了省却选择区块安装目录的麻烦,还能直接在项目中通过上下文菜单的方式安装区块:
这需要在 package.json
中配置上下文菜单:
"contributes": {
"menus": {
"explorer/context": [
{
"command": "tceBlock.generateBlock",
"group": "1_modification"
}
]
}
}
选择插入区块后会提升选择区块实例:
这里用到了 VSCode
的另一个 API:
const blockNames: any[] = []
...
const blockItem = await vscode.window.showQuickPick(blockNames, {
placeHolder: '请选择要插入的区块',
});
五、总结
本文讲解了基于区块开发的 VSCode
插件的实现细节,主要功能是以树的形式展示区块列表、预览区块和安装区块。当中用到的 VSCode
API 非常实用,可以用于开发读者自己设计的插件。
本文是本系列文章的终章,希望对你有所帮助,后会有期。