vscode扩展插件开发实战记录

最近在做一个前端动态主题替换的项目,我们架构侧定义了一批颜色变量(基于Css Variables),要求所有项目中,不得写死色值,所有颜色设置都要使用css变量来定义。
此要求一出,相关项目负责人立刻反馈了开发成本提升的问题,希望架构侧能提供一些工具上的支持。因此就有了此插件的需求。

初始化项目

环境准备

vscode官方推荐使用Yeoman脚手架系统来初始化插件项目,因此,首先第一步要安装yo,我们这里连同VSCode代码生成器一起安装了:

1
npm install -g yo generator-code

初始化项目

1
yo code

根据提示,自行按步骤选择或输入相关信息,系统会自动生成项目模板。

info

初始化成功后,vscode会提示是否用vscode单独打开项目独立窗口,我们按照默认打开项目。

运行项目

初始化成功的项目,已经自动生成launch.json文件,我们只需要打开vscode运行和调试面板,点击Run Extension即可启动项目。

launch
run

项目运行成功后,会自动打开一个vscode测试窗口。

在新窗口中,打开命令面板(ctrl+shift+p),输入hello world,选择执行hello world命令:

run-hello-world

在窗口右下角弹出Hello world from vscode-css-var-replace的提示框,就表示我们的项目已经运行成功并且生效。

hello-world

调试

vscode插件的调试方式一般有两种:

  1. console:vscode中集成了标准的console系统,输出窗口位于编辑器底部调试控制台;

console

  1. 直接在编辑器中打断点,然后运行过程中会在指定位置中断程序,把鼠标悬停在相应代码,即可弹出变量的相关信息。

debug

以上,我们的demo已经成功跑起来了。

环境介绍

接下来,我们来了解一下开发和配置环境。

项目目录结构

1
2
3
4
5
6
7
8
9
10
11
12
├── CHANGELOG.md ## 插件更新日志说明文档,用来后期版本迭代的说明
├── README.md ## 插件介绍文档
├── dist ## 打包目录
├── out ## 输出目录
├── package-lock.json ## npm版本锁定文件
├── package.json ## 项目配置文件
├── src ## 项目源码
│ ├── extension.ts ## extension.js是插件工程的入口文件,当插件被激活,即触发package.json中的activationEvents配置项时,extension.js文件开始执行。
│ └── process.ts ## 方法
├── tsconfig.json ## ts配置文件
├── vsc-extension-quickstart.md ## 初始化时自带的开发说明文档
└── webpack.config.js ## webpack配置文件

package.json

package.json是开发vscode插件的核心配置文件,插件的名称、描述、搜索关键词、激活时机和激活命令等都是在此配置

activationEvents

1
2
3
"activationEvents": [
"onCommand:vscode-css-var-replace.replace",
]

激活时机,类似于VUE的声明周期的概念。在VS Code中,插件都是懒加载的,所以你得为VS Code提供插件激活的时机。

vscode中默认提供了以下激活时机:

  • onLanguage:${language}: 特定语言文件打开时激活。
  • onCommand:${command}: 当调用命令时激活。
  • onDebug: 调试会话(debug session)启动前激活。
  • workspaceContains:${toplevelfilename}: 文件夹打开后,且文件夹中至少包含一个符合glob模式的文件时触发。
  • onFileSystem:${scheme}: 从协议(scheme)打开的文件或文件夹打开时触发。通常是file-协议,也可以用自定义的文件供应器函数替换掉,比如ftpssh
  • onView:${viewId}: 指定的视图id展开时触发。
  • onUri: 插件的系统级URI打开时触发。这个URI协议需要带上vscode或者vscode-insiders协议。URI授权必须是插件的唯一标识,剩余的URI是可选的。
  • *: 当VS Code启动时触发。为了保证良好的用户体验,只在你的插件没有其他任何激活事件的前提下,添加这个激活事件。

contributes

我们可以在此注册插件的激活方式,插件的激活方式主要有以下三种:

首先,参考我们当前项目的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"contributes": {
"commands": [{
"command": "vscode-css-var-replace.replace",
"title": "replace selected code"
}],
"menus": {
"editor/context": [{
"when": "editorFocus",
"command": "vscode-css-var-replace.replace",
"group": "navigation"
}]
},
"keybindings": [{
"command": "vscode-css-var-replace.replace",
"key": "ctrl+f10",
"mac": "cmd+f10",
"when": "editorTextFocus"
}]
},

以上配置,分别注册了三种激活方式:

  • commands: 注册命令,在vscode使用快捷键(ctrl+shift+p)打开命令面板,输入replace selected code来激活插件
  • menus: 注册右键菜单,在vscode点击右键打开右键菜单,选择replace selected code来激活插件
  • keybindings: 注册快捷键,在vscode中,使用快捷键(ctrl+f10)来激活插件

相关关键词

  • menus 下的 editor/context 是配置场景自定义菜单,定义这个菜单出现在哪里;
  • when 控制菜单合适出现;
  • command 定义菜单被点击后要执行什么操作;
  • alt 定义备用命令,按住alt键打开菜单时将执行对应命令;
  • group 定义菜单分组;

目前插件可以给以下场景配置自定义菜单:

  • 资源管理器上下文菜单 - explorer/context
  • 编辑器上下文菜单 - editor/context
  • 编辑标题菜单栏 - editor/title
  • 编辑器标题上下文菜单 - editor/title/context
  • 调试callstack视图上下文菜单 - debug/callstack/context
  • SCM标题菜单 - scm/title
  • SCM资源组菜单 - scm/resourceGroup/context
  • SCM资源菜单 - scm/resource/context
  • SCM更改标题菜单 - scm/change/title
  • 左侧视图标题菜单 - view/title
  • 视图项菜单 - view/item/context
  • 控制命令是否显示在命令选项板中 - commandPalette

when语句语法有很多,这里列举几个常用的:

  • resourceLangId == javascript:当编辑的文件是js文件时;
  • resourceFilename == test.js:当当前打开文件名是test.js时;
  • isLinuxisMacisWindows:判断当前操作系统;
  • editorFocus:编辑器具有焦点时;
  • editorHasSelection:编辑器中有文本被选中时;
  • view == someViewId:当当前视图ID等于someViewId时;

configuration

通过configuration我们可以设置一个属性,这个属性可以在vscode的settings.json中设置,然后在插件工程中可以读取用户设置的这个值,进行相应的逻辑。

参考我们当前项目的配置

1
2
3
4
5
6
7
8
9
10
11
"configuration": [{
"title": "css变量替换工具配置",
"type": "Object",
"properties": {
"vscode-css-var-replace.files": {
"type": "array",
"default": [],
"description": "css变量文件"
}
}
}],

此配置声明了vscode-css-var-replace.files属性,我们可以在vscode设置文件settings.json中配置此CSS变量替换插件的CSS变量声明文件。

1
2
3
"vscode-css-var-replace.files": [
"D://work//work//git//fx//src//assets//newstyle//vars.css" // css变量文件的绝对路径
]

功能实现

实现替换的核心类

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
// process.ts
import * as tinycolor from 'tinycolor2';

export default class CssVarProcess {
constructor(config: { [key: string]: any } = {}) {
const { cssVarMap } = config;
if (cssVarMap) {
this.colorMap = cssVarMap;
}
}
regs: { [key: string]: string } = {
hex: '#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})',
rgb: '[rR][gG][Bb][\(]([\\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?)[\\s]*,){2}[\\s]*(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)[\\s]*[\)]{1}',
rgba: '[rR][gG][Bb][Aa][\(]([\\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?)[\\s]*,){3}[\\s]*(1|1.0|0|0.[0-9])[\\s]*[\)]{1}',
hsl: '[hH][Ss][Ll][\(]([\\s]*(2[0-9][0-9]|360|3[0-5][0-9]|[01]?[0-9][0-9]?)[\\s]*,)([\\s]*((100|[0-9][0-9]?)%|0)[\\s]*,)([\\s]*((100|[0-9][0-9]?)%|0)[\\s]*)[\)]',
hsla: '[hH][Ss][Ll][Aa][\(]([\\s]*(2[0-9][0-9]|360|3[0-5][0-9]|[01]?[0-9][0-9]?)[\\s]*,)([\\s]*((100|[0-9][0-9]?)%|0)[\\s]*,){2}([\\s]*(1|1.0|0|0.[0-9])[\\s]*)[\)]'
};
colorMap: { [key: string]: string } = {};
replaceColor(str: string) {
return Object.keys(this.regs).reduce((pre, cur) => pre.replace(new RegExp(this.regs[cur], 'ig'), (match) => `${this.getColorMapItem(match) || match}`), str);
}
// 基于tinycolor,来实现不同颜色空间的相同颜色的变量替换
getColorMapItem(key: string) {
const targetKey = Object.keys(this.colorMap).find(colorKey => tinycolor.equals(colorKey, key));
return targetKey ? `var(${this.colorMap[targetKey]})` : '';
}
}

extension.ts中对需要的功能进行注册,主要使用vscode.commands.registerTextEditorCommand相关的api,来为package.json中的contributes配置项中的事件绑定方法或者监听器。

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
35
36
37
38
39
40
41
// extension.ts
import * as vscode from 'vscode';
import CssVarProcess from './process';
import * as fs from 'fs';

export function activate(context: vscode.ExtensionContext) {

const config = vscode.workspace.getConfiguration().get('vscode-css-var-replace');
const cssVarMap: { [key: string]: string } = {};
fs.readFile(config.files[0], (err, buffer) => {
if (!err) {
const cssText = buffer.toString();
cssText.match(/(--[a-zA-Z0-9-]+):[\s]*([#\(\)0-9a-zA-Z\s]+)[;\n]/ig)?.forEach(item => {
const [cssvar, cssvalue] = item.split(':');
cssVarMap[cssvalue.trim().replace(';', '')] = cssvar;
});
}
});
const cssVarProcess = new CssVarProcess({ cssVarMap });

let disposable = vscode.commands.registerTextEditorCommand('vscode-css-var-replace.replace', (textEditor) => {
const doc = textEditor.document;
const selections: vscode.Range[] = Array.from(textEditor.selections);
if (textEditor.selection.isEmpty) {
const start = new vscode.Position(0, 0);
const end = new vscode.Position(doc.lineCount - 1, doc.lineAt(doc.lineCount - 1).text.length);
selections.push(new vscode.Range(start, end));
}
// builder.replace为异步函数,此处用Array.reduce做了一个串联
selections.reduce((promise, selection) => promise.then(() => {
let text = doc.getText(selection);
return textEditor.edit(builder => {
builder.replace(selection, cssVarProcess.replaceColor(text));
});
}), new Promise(resolve => resolve({})));
});

context.subscriptions.push(disposable);
}

export function deactivate() { }

核心功能相对较简单,只针对当前的需求,没考虑过多的优化。如果有啥意见或建议,欢迎指出。

插件打包

vscode插件支持打包成单独的vsix文件,并通过手动安装到vscode中使用。插件打包功能依赖vsce(Visual Studio Code Extensions)。

安装

1
npm install -g vsce

用法

1
vsce package

我在开发这个插件时,打包过程出了点问题,插件默认配置有一些干扰,需要手动配置package.jsonscripts,添加package: vsce package,并且清理掉README.md内容,才能正常打包,这些都是多次尝试得出的结果,并没有查到相关资料。

插件发布

插件开发完了,我们分享给他人使用呢?通常有三种方式

  • 直接把源码发给别人,用调试的方式使用,一般不推荐;
  • 把我们打包好的vsix文件发给别人,然后手动安装,如果你的插件没什么通用性,或者涉及机密不方便发布到应用市场,可以尝试采用这种方式;
  • 注册开发者账号,发布到官网应用市场,这个发布和npm一样是不需要审核的。

发布到应用市场的流程

  1. 在网站https://dev.azure.com/vscode获取一个access token,这个token用来创建一个publisher

  2. 创建publisher

1
vscr create-publisher (publisher name)
  1. 登入一个publisher
1
vscde login (publisher name)
  1. 打包
1
vsce package
  1. 发布
1
vsce publish

参考

  1. Vscode Extension Guides
  2. vscode 中文文档

本文永久链接: https://www.mulianju.com/develop-vscode-plugins/