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

1,735 阅读11分钟

开发插件

插件开发知识

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 ,按提示输入项目信息

? 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

yo code创建插件项目

第一步选择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出来关键内容(只记录增加和修改的内容)。

{
  "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。

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

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

插件入口代码

// 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实现选择功能
// 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实现输入躺平时间
      与此同时还要记录当前选择的模式以及躺平的状态,还有计时器。计时器的清理很重要,需要在计时结束和取消躺平时终止并清理计时器,这一逻辑目前还存在一些问题,我后续会进行优化。

    输入躺平时间

    // 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实现躺平倒计时展示

    躺平倒计时

    // 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实现监听
    // src/extension.ts
    
    context.subscriptions.push(window.onDidChangeActiveTextEditor(event => {
        lyingAlert(event);
    }));
    
    context.subscriptions.push(workspace.onDidChangeTextDocument((event) => {
        lyingAlert(event);
    }));
    
    • 躺平模式下禁止Coding,弹窗提醒 >> 通过showErrorMessage弹出提醒

    弹窗提醒

    // src/command.ts
    export function lyingAlert(event: any) {
        lying && event && window.showErrorMessage('现在是躺平时间,去休息吧!', { modal: true });
    }
    
    • 如果有需要,可以取消当前躺平进度 >> 通过withProgress的token.onCancellationRequested实现

    取消躺平

    // 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实现选择工作时间

    选择工作时间

    // 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

    填写循环次数

    // 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
    // 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

创建Access Token

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

点我创建publisher

image.png

补充信息

一定要在package.json中配置

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

完成 README.mdCHANGELOG.md

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

README.md

CHANGELOG.md

发布插件

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

确认发布结果

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

发布完成

The end!

总结

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

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

转载请注明“来自掘金-EvenZhu”