VSCode插件 - ”躺平“ 开发和发布

开发插件

插件开发知识

VSCode插件开发需要前端开发的基础知识,包括但不限于Javascript(必须)、HTML和CSS(非必须)。

随着插件开发工具的版本提升,提供了对TS语法的支持,这个"躺平"插件就是采用TS开发的。

另外还需要一些Webpack方面的知识,不过不需要自己调整和优化配置,只需要根据官方教程设置即可。当然,如果后续涉及到复杂的开发工作,具备相关知识的jym也可以对工程进行优化。

VS Code 插件开发文档 - 中文
VS Code 插件开发文档 - 英文

由于我的"躺平"插件是一个已经具备完备功能的插件了,所以我就不把官方教程复制一遍了,而是跟着开发思路一步一步走下来。这样既可以贴近官方教程,又有一些脱离官方文档的内容和思考。不过由于时间有限,我只能提供思路,具体的代码请查看我的开源代码库。

"躺平"插件Github代码仓库

创建插件工程

首先需要安装好Node和Git,不写具体步骤了,直接三、二、一,上链接:

安装Node
Mac系统安装Git
Windows系统安装Git

还有,需要安装VSCode。倒不是非得让你使用VSCode进行开发工作,但是你开发VSCode插件,不安装VSCode,就很......你懂得。

OK,万事俱备,正式开始。

首先打开终端,执行以下命令:

npm install -g yo generator-code

上面的命令作用是安装YeomanVS Code Extension Generator,这是VSCode官方提供的脚手架开发项目工具。

然后执行 yo code ,按提示输入项目信息

zsh 复制代码
? What type of extension do you want to create? (Use arrow keys)
New Extension (TypeScript) 

? What's the name of your extension? 
lying

? What's the identifier of your extension? 
lying

? What's the description of your extension? 
遇事不决,躺平一下

?Initialize a git repository?
Yes

? Bundle the source code with webpack? 
Yes

? Which package manager to use? 
yarn

第一步选择Typescript是因为我平时使用的是TS,jym也可以选择New Extension (JavaScript),最后一步包管理工具选择npm、yarn、pnpm都可以。如果你选择yarn或pnpm,请确保你本地已经安装了对应的包管理工具。

填写完上面的配置信息后,lying 项目会被创建并开始构建过程。

构建完成后,执行 code ./lying

code命令会打开VSCode,并加载命令后面传入的项目目录。

因为创建的项目根目录是当前目录下的lying目录,所以需要使用 ./lying ,这一点需要注意。

VSCode打开项目后,按下F5,你会立即看到一个 插件发开主机 窗口,其中就运行着插件。

在命令面板 Ctrl+Shift+P 中输入 lying 命令即可看到 Hello World 提示弹窗。

上面这部分如果出现问题或者有疑问,请移步 VS Code 插件开发中文文档 仔细查阅教程,再次尝试。

配置插件信息

我们已经把"躺平"插件的项目创建并运行成功,现在开始配置插件信息。

插件项目的配置信息在package.json中,我直接po出来关键内容(只记录增加和修改的内容)。

json 复制代码
{
  "name": "lying", // 插件名称,英文
  "displayName": "躺平", // 插件商店展示的插件名称
  "description": "遇事不决,躺平一下 ~ ", // 项目描述,会展示在VSCode插件商店的插件描述位置
  "version": "0.1.4", // 没错,已经是0.1.4版本了
  "icon": "lying.png", // 插件图标,不配置的话在插件商店展示灰色占位图标
  "LICENSE": "MIT", // 开源协议
  "repository": {
    "type": "git",
    "url": "https://github.com/EvenZhu/lying.git"
  },
  "publisher": "EvenZhu", // 必须和创建的publisher保持一致
  "categories": [
    "Other"
  ],
  "main": "./dist/extension.js", // 这里指定的路径是使用webpack打包编译后的路径
  "contributes": {
    "commands": [ // 配置命令列表,配置此项,用户才可以在命令面板查询并执行该命令
      {
        "command": "evenzhu.lying", // 命令包含两部分,第一部分是标识符,第二部分是命令名称
        "title": "躺平"
      }
    ],
    "keybindings": [ // 为命令绑定快捷键
      {
        "command": "evenzhu.lying",
        "title": "躺平",
        "key": "shift+alt+ctrl+l",
        "mac": "shift+alt+cmd+l",
        "when": "true" // 代表在VSCode中任何情况下都可以通过快捷键唤起evenzhu.lying命令
      }
    ]
  },
  "scripts": {
    // 增加了下面这条指令,为了方便发布插件,后面的Access token是发布前在Azure DevOps创建的
    "vp": "vsce publish -p Access token"
  },
  "devDependencies": {
      ...官方配置,不需要任何调整
  },
  "dependencies": {
    "dayjs": "^1.11.10" // 为了计算和格式化时间
  }
}

命令开发

根据package.json中的配置 "main": "./dist/extension.js" 可以看出,入口文件应该是src下的。extension.ts。

插件入口文件会导出两个函数,activatedeactivate,你注册的 激活事件 被触发之时执行 activatedeactivate 则提供了插件关闭前执行清理工作的机会。

接下来,在代码中出现的ExtensionContext、window、commands、workspace等,都是从'vscode'库中导入的。

插件入口代码

ts 复制代码
// src/extension.ts

import {
    ExtensionContext,
    window,
    commands,
    workspace
} from 'vscode';
import command, { finished, lyingAlert } from './command';

export function activate(context: ExtensionContext) {
    // 注册evenzhu.lying命令
    context.subscriptions.push(commands.registerCommand('evenzhu.lying', command));

    /**
     * 监听编辑器焦点变化和输入事件
     * 为了让你实现真正意义上的躺平,真是操碎了心
     */
    context.subscriptions.push(window.onDidChangeActiveTextEditor(event => {
        lyingAlert(event);
    }));

    context.subscriptions.push(workspace.onDidChangeTextDocument((event) => {
        lyingAlert(event);
    }));
}

export function deactivate() {
    finished(); // 停止"躺平"插件插件的执行逻辑
}

命令执行逻辑

快捷键或者命令激活并触发"躺平"插件后,执行逻辑如下:

弹出输入框,选择立即躺平 or 番茄钟

  • 使用QuickPick实现选择功能
ts 复制代码
// src/command.ts

const options: { [key: string]: (context: ExtensionContext) => Promise<string | void> } = {
  立即躺平: showInputBox,
  番茄钟: (context: ExtensionContext) => multiStepInput(context, (workTime: number, lyingTime: number, repeatCount: number) => {
    mode = 1;
    repeat = --repeatCount;
    doProgress(false, workHandler, workTime, lyingTime);
  })
};
const quickPick = window.createQuickPick();
quickPick.items = Object.keys(options).map(label => ({ label }));
quickPick.onDidChangeSelection(selection => {
  if (selection[0]) {
    options[selection[0].label](context)
      .then((text) => {
        if (selection[0].label === '立即躺平') {
          mode = 0;
          doProgress(true, lyingHandler, 0, Number.parseInt(text ?? '1'));
        }
      })
      .catch(console.error);
  }
});
quickPick.onDidHide(() => quickPick.dispose());
quickPick.show();
  • 选择立即躺平

    • 弹出输入框,填入躺平时间 >> 通过showInputBox实现输入躺平时间
      与此同时还要记录当前选择的模式以及躺平的状态,还有计时器。计时器的清理很重要,需要在计时结束和取消躺平时终止并清理计时器,这一逻辑目前还存在一些问题,我后续会进行优化。
    ts 复制代码
    // src/basicInput.ts
    
    window.showInputBox({
        valueSelection: [1, 120],
        placeHolder: '请输入躺平时间(分钟数:1~120)',
        validateInput: text => {
            if (text === '') {
                return '躺平时间不得为空';
            }
    
            const minute = Number.parseInt(text);
            if (Number.isNaN(minute)) {
                return '请输入数字';
            }
    
            if (minute > 120 || minute < 1) {
                return '请输入 1~120 之间的数字';
            }
            return null;
        },
    });
    • 展示躺平阶段倒计时 >> 通过dayjs实现时间转换,通过Progress实现躺平倒计时展示
    ts 复制代码
    // src/command.ts
    
    function doProgress(lyingState: boolean, handler: ProgressHandler, workTime?: number, lyingTime?: number) {
        lying = lyingState;
        window.withProgress({
            location: ProgressLocation.Notification,
            title: lyingState ? '躺平阶段' : '工作阶段',
            cancellable: lyingState
        }, (progress, token) => handler(progress, token, workTime, lyingTime));
    }
    
    const lyingHandler = (progress: any, token: any, workTime?: number, lyingTime?: number) => {
        const totalSecond = lyingTime! * 60;
        let second = totalSecond;
        token.onCancellationRequested(() => {
        finished('躺平大业,中道崩阻,你是真卷啊!', 'warn');
        });
        progress.report({ message: '开始躺平' });
    
        interval = setInterval(() => {
        const time = formatSeconds(second - 1);
        const percent = (totalSecond - second - 1) / totalSecond * 10;
        progress.report({ message: `还有 ${time}` });
        second--;
        }, 1000);
    
        const p = new Promise<void>(resolve => {
            setTimeout(() => {
                if (interval) {
                    if (mode === 0) {
                      finished('躺平结束,继续愉快地Coding~');
                      return;
                    }
    
                    if (repeat > 0) {
                      repeat--; // 番茄钟循环次数减1
                      doProgress(false, workHandler, workTime, lyingTime);
                    } else {
                      finished('番茄钟结束了,继续愉快地Coding吧!', 'warn');
                    }
                }
                resolve();
            }, second * 1000);
        });
    
        return p;
    };
    
    // 调用方法doProgress方法,传入lyingHandler即可展示躺平倒计时
    • 监听Tab切换和编辑区输入 >> 通过onDidChangeActiveTextEditor和onDidChangeTextDocument实现监听
    ts 复制代码
    // src/extension.ts
    
    context.subscriptions.push(window.onDidChangeActiveTextEditor(event => {
        lyingAlert(event);
    }));
    
    context.subscriptions.push(workspace.onDidChangeTextDocument((event) => {
        lyingAlert(event);
    }));
    • 躺平模式下禁止Coding,弹窗提醒 >> 通过showErrorMessage弹出提醒
    ts 复制代码
    // src/command.ts
    export function lyingAlert(event: any) {
        lying && event && window.showErrorMessage('现在是躺平时间,去休息吧!', { modal: true });
    }
    • 如果有需要,可以取消当前躺平进度 >> 通过withProgress的token.onCancellationRequested实现
    ts 复制代码
    // src/command.ts
    
    token.onCancellationRequested(() => {
        finished('躺平大业,中道崩阻,你是真卷啊!', 'warn');
    });
    
    export function finished(message: string | undefined = undefined, type: string = 'info') {
        interval && clearInterval(interval), interval = undefined;
        lying = false;
        if (message) {
            switch (type) {
                case 'error':
                    window.showWarningMessage(message, { modal: true });
                    break;
                case 'warn':
                    window.showWarningMessage(message, { modal: true });
                    break;
                default:
                    window.showInformationMessage(message, { modal: true });
                    break;
            }
        }
    }
    • 同时防止通过快捷键和命令多次触发躺平操作;
      • 记录当前执行状态和计时器
      • 如果当前处于执行状态且计时器正在执行,则不可再次触发
      • jym反馈的多次开启和关闭"躺平"插件存在的问题就是这个逻辑存在漏洞造成的
  • 选择番茄钟

    • 选择工作阶段时间 >> 通过showQuickPick实现选择工作时间
    ts 复制代码
    // src/multiStepInput.ts
    
    // 输入工作时间的方法与输入躺平时间的方法共用,根据当前步骤进行判断
    // 第1步是选择工作时间,第2步是选择躺平时间
    async function pickWorkTime(input: MultiStepInput, state: Partial<State>) {
        const first = !state.step;
        const step = first ? 1 : 2;
        const pick: PickItemWithMinute = await input.showQuickPick({
            title,
            step: step,
            totalSteps: 3,
            placeholder: first ? '请选择工作阶段时长' : '请选择躺平时长',
            items: first ? workGroups : lyingGroups,
            shouldResume: shouldResume
        });
        if (first) {
            state.workTime = pick;
        } else {
            state.lyingTime = pick;
        }
        state.step = step;
        if (state.step === 2) {
            // 如果当前步骤是第2步,下一步则输入循环次数
            return (input: MultiStepInput) => inputRepeat(input, state);
        }
        return pickWorkTime(input, state);
    }
    • 选择躺平阶段时间 >> 继续执行pickWorkTime
    • 填写番茄钟循环次数 >> 通过showInputBox实现填写循环次数
      • 番茄钟循环指定次数后自动停止
      • 留空代表无限循环,直到用户取消躺平或关闭VSCode
    ts 复制代码
    // src/multiStepInput.ts
    
    async function inputRepeat(input: MultiStepInput, state: Partial<State>) {
        const repeatCount = await input.showInputBox({
            title,
            step: 3,
            totalSteps: 3,
            value: '',
            prompt: '请输入循环次数,留空代表不限次数',
            validate: validateNameIsUnique,
            shouldResume: shouldResume
        });
        state.repeatCount = repeatCount ? parseInt(repeatCount) : -1;
    }
    • 展示工作阶段倒计时 >> 通过dayjs实现时间转换,通过Progress实现躺平倒计时展示
      • 工作倒计时和躺平时间倒计时的逻辑可以复用
      • 注意不同状态的切换和记录更新
    • 工作阶段结束后展示躺平阶段倒计时
      • 切换状态
      • 展示躺平倒计时
    • 躺平模式下禁止Coding,弹窗提醒
    • 躺平模式结束后判断是否继续工作模式
      • 开启模式为番茄钟,且循环次数
      • 开启模式为番茄钟,且循环次数未到0
    ts 复制代码
    // src/command.ts
    
    // mode - 0:躺平 1:番茄钟
    if (mode === 0) {
        finished('躺平结束,继续愉快地Coding~');
        return;
    }
    
    if (repeat > 0) {
        repeat--; // 番茄钟循环次数减1
        doProgress(false, workHandler, workTime, lyingTime);
    } else {
        finished('番茄钟结束了,继续愉快地Coding吧!', 'warn');
    }
    • 如果有需要,可以取消当前躺平(或番茄钟)进度
    • 同时防止通过快捷键和命令多次触发躺平操作

OK,逻辑梳理完毕,相关的API和使用到插件功能也已经列出来了,具体逻辑请结合源码理解,如有问题可以评论区留言。
"躺平"插件Github代码仓库

发布插件

发布插件的教程官方已经整理出来了 发布插件教程

我今天只把关键的步骤写出来,如有疑问欢迎留言或者去官网查看详细资料。

创建组织

Azure DevOps

安装vsce

终端执行命令 npm install -g vsce

vsce是一个用于将插件发布到市场上的命令行工具。

创建Access token

打开 dev.azure.com/vscode 进入安全(Security)页面。

举例:我创建的组织(evenzhu)安全页面地址 dev.azure.com/evenzhu/_us...

选择Personal Access Token,点击New Token创建一个新的 Personal Access Token

创建发行方(publisher) - 在插件商店的发布人名称

点我创建publisher

补充信息

一定要在package.json中配置

  • icon - 插件图标
  • publisher - 创建的publisher名称

完成 README.mdCHANGELOG.md

  • README.md - 插件介绍、使用方法,宣传图文等
  • CHANGELOG.md - 会展示在插件商店的更改日志中

发布插件

在终端的项目根目录下执行命令 vsce publish -p <token> 进行发布

确认发布结果

  • 查看VSCode插件商店上插件是否已经发布成功
  • 插件的名称、版本、描述和其他信息是否显示正确

The end!

总结

  • 累,真累
  • 累,是因为
    • 两天的时间完成了VSCode插件开知识学习和插件开发
    • 以及发布、迭代和BUG修复
    • 又在这个过程中发布了插件创意思路和开发、发布过程
  • 值,真值
  • 值,是因为这个过程中学会了很多
    • VSCode插件开发技术
    • 插件运行的基本原理
    • 对产品设计的思考
    • 尝试理解用户思维
    • 推广营销的重要性
  • 也发现了自身的不足和增长点
    • 写作过程中发现很多知识想要说清楚太难了,必须有取舍
    • 很多知识在应用时还需要查阅文档和资料,说明并未烂熟于心
    • 时间安排过紧,文章的结构和内容无法进行有效的沉淀和优化
    • 希望这样热血的阶段,不是昙花一现,要持之以恒

PS:如果有需要补充的内容,请在评论区留言

转载请注明"来自掘金-EvenZhu"
相关推荐
Hamm4 小时前
用装饰器和ElementPlus,我们在NPM发布了这个好用的表格组件包
前端·vue.js·typescript
tianchang11 小时前
TS入门教程
前端·typescript
demo007x11 小时前
四、从 0 开始构建一个代码库-向量数据库的选择与集成
visual studio code·cursor·trae
胖方Hale18 小时前
04. Typescript 数组类型
前端·typescript
胖方Hale18 小时前
01. Typescript 基础数据类型
前端·typescript
Kjjia18 小时前
考试过程中校园网突然发力,答案没能保存...我炸了
前端·typescript
隐形喷火龙21 小时前
搭建TypeScript单元测试环境
javascript·typescript·单元测试
khalil1 天前
浅析TS枚举与位运算的结合
前端·typescript
YuShiYue2 天前
pnpm monoreop 打包时 node_modules 内部包 typescript 不能推导出类型报错
javascript·vue.js·typescript·pnpm
hepherd2 天前
Flutter 环境搭建 (Android)
android·flutter·visual studio code