从零开发一个vscode插件

lxf2023-05-05 01:01:15

背景

鉴于当前前端技术人员比较紧张,避免前端开发人员浪费在毫无意义的增删改查上面,思考着能否基于vscode指令生成一套增删改查模版,后端人员可以通过简单指令就可以生成代码模版。首先想到的是vscode代码片段功能,vscode代码片段可以根据指令生成代码片段,但有一个缺点是不能根据代码片段中的引入文件去创建文件,这对于有些后端人员来说是灾难性的伤害。因此想到可以借助vscode插件来生成代码片段并创建代码片段中引入的文件和方法,大大减少后端人员上手开发的难度,通过修改对应的api就可以轻松完成前端增删改查页面的开发。
tips:项目后台模版是基于umi脚手架生成的后台管理项目,生成的代码片段是基于antd-pro,所以项目中必须引入antd-pro依赖。

实现如下功能

  1. 根据指令生成代码片段;
  2. 根据代码片段中引入的文件去创建目标文件并给文件中写入相应方法;

前期准备

  1. 先安装vscode官方提供的插件脚手架
    npm install -g yo generator-code
  1. 运行 yo code来生成项目结构,选择生成类型;
    yo code

    _-----_ ╭──────────────────────────╮

    | | │ Welcome to the Visual │

    |--(o)--| │ Studio Code Extension │

    `---------´ │ generator! │

    ( _´U`_ ) ╰──────────────────────────╯

    /___A___\ /

    | ~ |

    __'.___.'__

    ´ ` |° ´ Y `

    ? What type of extension do you want to create? (Use arrow keys)

    ❯ New Extension (TypeScript)

    New Extension (JavaScript)

    New Color Theme

    New Language Support

    New Code Snippets

    New Keymap

    New Extension Pack

    New Language Pack (Localization)

    New Web Extension (TypeScript)

    New Notebook Renderer (TypeScript)

最终生成的项目结构如下

    ├── .vscode

    ├── out

    ├── src

    ├── test

    ├── extension.ts

    ├── .eslintrc.json

    ├── .gitignore

    ├── .vscodeignore

    ├── CHANGELOG.md

    ├── package-lock.json

    ├── package.json

    ├── README.md

    ├── tsconfig.json

    ├── vsc-extension-quickstart.md
  1. 运行vscode插件:按下F5即可进入调试阶段,会打开一个新的vscode窗口,Ctrl+Shift+P 打开命令行,输入Hello World ,右下角弹出提示框 Hello World from jikaibo !

代码分析

通过阅读官方vscode开发文档,了解到extension.ts是脚手架的入口文件。

    // extension.ts
    import * as vscode from 'vscode';
    export function activate(context: vscode.ExtensionContext) {
        console.log('Congratulations, your extension "react-antd" is now active!');
        // registerCommand 注册命令api
        let disposable = vscode.commands.registerCommand('react-antd.helloWorld', () => {
            vscode.window.showInformationMessage('Hello World from jikaibo!');=
        });
        // 所有注册类的 API 执行后都需要将返回结果放到 `context.subscriptions`中。
        context.subscriptions.push(disposable);
    }
    export function deactivate() {}

其中 vscode.commands.registerCommand 用于注册命令,react-antd.helloWorld 为命令id,在package.json文件中已经配置了该命令如下:

    // package.json

    {

        "activationEvents": [ // 激活事件

            "onCommand:react-antd.helloWorld"

        ],

        "main": "./dist/extension.js",

        "contributes": {

            "commands": [

                {

                "command": "react-antd.helloWorld",

                "title": "Hello World"

                }

            ]

        },

        "keybindings": [ // 按键

            {

            "command": "react-antd.helloWorld", // 指定快捷键执行的操作;

            "key": "ctrl+f10", // windows下快捷键

            "mac": "cmd+f10", // mac下快捷键

            "when": "editorTextFocus" // 快捷键何时生效

            }

        ],

        "menus": { // 菜单

            "editor/context": [

                {
                    "when": "editorFocus", // 菜单出现时机

                    "command": "react-antd.helloWorld", // 定义菜单被点击后要执行什么操作;

                    "group": "navigation" // 定义菜单分组
                }
            ]
        }
    }

注意:activationEvents中的 "onCommand:react-antd.helloWorld" 中的 react-antd为插件id应该与extensions.js中的注册命令匹配,onCommand为监听类型,值可以为onUri、onLanguage 等。contributes 属性用于配置显示命令方式,还可以是按键或者菜单形式。 第二个参数为回调函数,当命令触发时,弹出提示框。
以上就是脚手架生成的项目提供的功能,顺便也是熟悉下基本的api,接下来根据要求实现我们自己的功能。

功能开发

  1. 根据代码提示功能注入代码文本,具体核心代码如下:
    // extension.ts
    import * as vscode from 'vscode';
    import * as fs from 'fs';
    import autoCompletionItemProvider from './autoCompletionItemProvider';
    import createWriteFile from './createWriteFile';
    export function activate(context: vscode.ExtensionContext) {
        console.log('恭喜,您的扩展“vscode-plugin-antd-pro”已被激活!');
        const commandId ="vscode-extension.quick-antd-pro"; // 定义命令
        const commandHandler = (
            editor: vscode.TextEditor,
            edit: vscode.TextEditorEdit,
            position: vscode.Position,
            str: string,
        ) => {
            // 获取文件路径
            // const filePath = path.normalize(editor.document.uri.path)
            // const code = path.basename(path.dirname(filePath))
            const lineText = editor.document.lineAt(position.line).text;
            const startSpaces = lineText.length - lineText.trimStart().length;
            // 删除当前行内容
            edit.delete(
                new vscode.Range(
                position.with(undefined, startSpaces),
                position.with(undefined, lineText.length)
            );
            // 插入代码提示片段
            edit.insert(position.with(undefined, startSpaces), str);
            // 创建写入文件内容
            createWriteFile();
            return Promise.resolve([]);
        };
        context.subscriptions.push(
            // 注册编辑器命令,仅在编辑器被激活时调用才生效,访问到当前活动编辑器`textEditor`
            vscode.commands.registerTextEditorCommand(commandId, commandHandler)
        );
        // 获取当前工作区文件信息
        let {textDocuments,name} = vscode.workspace;
        if(textDocuments.length === 0) {
            return;
        }
        const {fileName} = textDocuments[textDocuments.length -1];
        const fileNameArr = fileName.split('/');
        // 当前文件目录
        const currnetFolder = fileNameArr[fileNameArr.length - 2];
        const disposable = vscode.languages.registerCompletionItemProvider(
            ["javascript", "javascriptreact", "typescript", "typescriptreact", "html"],
            new autoCompletionItemProvider(currnetFolder), // 当前的文件目录传入构造函数中
            '.' // 触发字符
        );
        // context.subscriptions.push(disposable);
    }
    export function deactivate() {}

registerCompletionItemProvider 接受三个参数:

  • 第一个参数为在何种文件格式下触发此函数 类型为string 或者 array

  • 第二个参数为对象合集,包括两个函数

    • provideCompletionItems 键入一个字符时触发,在此阶段提供代码文本。接受vscode传入的当前文档(document)和当前光标位置(position),通过这两个参数获取当前行输入的文本。
    • resolveCompletionItem 选中此代码时的触发动作;
    • 第三个参数为剩余参数,为该函数触发的字符,英文默认触发,数字 . / 等字符需要手动触发。

registerTextEditorCommand 为注册文本编辑器命令,仅在编辑器被激活时调用才生效,此外,这个命令可以访问到当前活动编辑器 textEditor,其接受两个参数

  • 第一个参数为当前命令ID,类型为string;
  • 第二个参数为回调函数
  1. autoCompletionItemProvider 代码如下:
    import * as vscode from 'vscode';
    import createIndexFragment from './fragments/indexFragment';
    class AutoCompletionItemProvider implements vscode.CompletionItemProvider {
        private position?: vscode.Position;
        private str = '';
        constructor(props:any) {
            // 赋值代码片段
            this.str = createIndexFragment(props);
        }
        // 提供代码提示的候选项
        public provideCompletionItems(
            document: vscode.TextDocument,
            position: vscode.Position
        ) {
            this.position = position;
            // 用于提示正在键入的文件内容
            const snippetCompletion = new vscode.CompletionItem(
                'react-antd-pro',
                 vscode.CompletionItemKind.Operator
            );
            // 文档注释
            snippetCompletion.documentation = new vscode.MarkdownString('quick antd-pro');
            snippetCompletion.detail = 'selected it'; // 附加信息
            snippetCompletion.filterText = ''; // 刷选项过滤字符串
            return [snippetCompletion];
        }
        // 光标选中当前自动补全item时触发动作
        public resolveCompletionItem(item: vscode.CompletionItem) {
            const label = item.label;
            if (this.position && typeof label === "string") {
                // 选中代码,发起vscode-extension.quick-antd-pro指令
                item.command = {
                    command: "vscode-extension.quick-antd-pro",
                    title: "refactor",
                    // 通过arguments传递参数
                    arguments: [this.position.translate(0, label.length + 1), this.str], // 这里可以传递参数给该命令
            };
        }
            return item;
        }
   }

大致流程为用户键入字符时,触发 provideCompletionItems 函数,代码提示悬浮框插入代码提示,用户选择此提示,再触发 resolveCompletionItem 函数,命令操作删除文本,插入自定义文本内容。

  1. createWriteFile 代码如下:
    import * as vscode from 'vscode';
    import * as fs from 'fs';
    import modalFragment from './fragments/componentFragmentModal';
    import apiFragment from './fragments/apiFragment';
    import createServicesFragment from './fragments/servicesFragment';
    const createWriteFile = () => {
        let {textDocuments,name} = vscode.workspace;
        if(textDocuments.length === 0) {
            return;
        }
        const {fileName} = textDocuments[textDocuments.length -1];
        const fileNameArr = fileName.split('/');
        // 创建文件index.less文件
        const createFileName = `${fileNameArr.filter(item => item !== 'index.tsx').join('/')}/index.less`;
        const currnetFolder = fileNameArr[fileNameArr.length - 2];
        if (fs.existsSync(createFileName)) {
            vscode.window.showErrorMessage(`文件${createFileName}已存在`);
        }
        fs.writeFile(createFileName, '', () => {
            vscode.window.showTextDocument(vscode.Uri.file(createFileName), {
                viewColumn: vscode.ViewColumn.Two, // 显示在第二个编辑器窗口
         });
    });
    // 异步创建文件夹;同步用 mkdirSync api
    const currentDirName = `${fileNameArr.filter(item => item !== 'index.tsx').join('/')}/components`;
    fs.mkdir(currentDirName,() => {
        fs.writeFile(`${currentDirName}/RuleModal.tsx`, modalFragment, (err) => {
            if(err) {
                vscode.window.showErrorMessage('文件写入失败');
                return;
            }
             vscode.window.showTextDocument(vscode.Uri.file(`${currentDirName}/RuleModal.tsx`), {
                viewColumn: vscode.ViewColumn.Two, // 显示在三个编辑器窗口
            });
        });
    });
    const basePath = fileName.slice(0, fileName.indexOf('/src'));
    // 判断文件路径是否存在 src下是否存在api、serveces
    if (fs.existsSync(`${basePath}/src/api`) && fs.existsSync(`${basePath}/src/services`) ) {
            // vscode.window.showErrorMessage(`文件已存在`);
            // return;
            // 写入api下面的接口文件
            fs.writeFile(`${basePath}/src/api/${currnetFolder}.ts`, apiFragment, (err) => {
                if(err) {
                    vscode.window.showErrorMessage('文件写入失败');
                    return;
                }
                     vscode.window.showTextDocument(vscode.Uri.file(`${basePath}/src/api/${currnetFolder}.ts`), {
                     viewColumn: vscode.ViewColumn.Three, // 显示在四个编辑器窗口
                });
            });
            // 写入serveces下的服务文件
            fs.writeFile(`${basePath}/src/services/${currnetFolder}.ts`, createServicesFragment(currnetFolder), (err) => {
                if(err) {
                    vscode.window.showErrorMessage('文件写入失败');
                    return;
                }
                vscode.window.showTextDocument(vscode.Uri.file(`${basePath}/src/api/${currnetFolder}.ts`), {
                    viewColumn: vscode.ViewColumn.Four, // 显示在第五个编辑器窗口
                });
            });
        }
    };
    export default createWriteFile;

该方法主要使用的fs来创建文件并写入文件内容,上文代码片段创建完成,会在该模块下同步创建相应的index.less文件。并在src/api文件下创建与模块同名的api文件,此时后端开发人员只需修改里面对应的api即可。在src/services下创建与模块同名的services服务文件,其中

  • fs.existsSync 判断文件是否存在。接受一个参数,类型为string,表示文件路径。返回为true 或者false
  • fs.writeFile 创建一个文件并写入内容。其接受三个参数。第一个参数为文件路径,第二个参数为文件内容,类型为string。第三个参数为回调函数,在里面可以做异常判断。
  • fs.mkdir 为异步创建文件夹与之对应的是同步创建文件夹 fs.mkdirSync ,其接受两个参数。第一个参数为文件夹名称,第二个参数为创建之后的回调函数。 至此该插件的核心逻辑已经完成了。整体的大致流程为用户键入字符时,触发 provideCompletionItems 函数,代码提示悬浮框插入代码提示,用户选择此提示,再触发 resolveCompletionItem 函数,该函数发起一个命令,命令操作删除文本,插入自定义文本内容,创建首页需要引入的文件及写入文件内容。

发布

接下来就是发布了,发布之前需要注意下:package.json中的name取名必须在应用市场里面唯一,不能有重名,否则会发布失败,生成的包名即为name名称。

  • 先安装本地打包工具vsce

        // 全局安装vsce工具
        npm i vsce -g
    

    然后在项目根文件下执行

        vsce package
    

    在打包时会出现提示信息,例如修改README.md, 添加LICENSE。根据提示操作即可。之后会生成vsix文件,之后就可以通过vscode的扩展右上角选择 install from vsix 来安装,一些涉及到公司的机密插件可以通过这种方式来安装,而不需要发不到应用市场。

    从零开发一个vscode插件

  • 发布到应用市场

    • 注册账号获取Token
      Visual Studio Code 使用 Azure DevOps 作为其 Marketplace 服务,所以需要登录 Azure,如果没有组织的话会创建提示创建组织。点击下图按钮创建一个新Token

      从零开发一个vscode插件

      从零开发一个vscode插件 根据提示填写相关信息,点击create就可完成token的创建。

    • 创建发布者:通过网页版端 marketplace.visualstudio.com/manage

      从零开发一个vscode插件 填写一些基本信息,这里需要注意的是 name 的名称必须与package.json中的 publisher 保持一致,其中verified domain的时候必须在本地dns服务器配置中添加其提供的相关信息,点击保存即可成功创建发布者。之后执行命令:

          vsce login <publisher name>
      

      publisher name 为package.json中的name,然后根据要求输入Personal Access Token,即为刚创建的token,复制过来即可。成功之后会提示 The Personal Access Token verification succeeded for the publisher 'xxxx'

    • 发布应用:

      • 方式一:执行命令

            vsce publish
        

        运行成功之后提示 Published jikaibo.antd-pro-pluginv1.0.0. ,说明发布完毕。此时可以在应用市场里面查看已经发布的插件Extensions for Visual Studio Code.

      • 方式二:通过 Extensions for Visual Studio Code. 从零开发一个vscode插件

        点击New extension 选择从Visual Studio Code 上传本地打包的vsix文件来进行发布。

使用方法

  1. 在pages下面新建一个业务文件夹,名称如demo,然后在此文件下新建一个index.tsx文件;
  2. 在index.tsx文件里面输入 react-antd-pro.指令,根据弹出的执行提示选择生成代码模版;
  3. 成功之后会在index.tsx文件中创建相关业务代码,并在src/api下创建同名(demo)api文件,在src/services下面创建同名(demo)request服务文件

总结

到此一个vscode插件从开发到发布上线已经完成了,整体过程的体验还算ok。遇到的问题还是注册Azure用户账号的时候,重复验证不是机器人,比较emo,还有创建发布者的时候需要验证域名信息,需要根据生成的地址配置本地dns服务,算是比较坑的存在吧。本插件主要实现的功能根据代码提示生成代码片段,并创建相关文件、写入文件内容。其实用的vscode api只是冰山一角,有机会可以深入到 vscode 插件开发当中去,Enenen, 但是不建议造轮子(毕竟有很多大牛已经帮我们实现了功能完善的插件了)。

从零开发一个vscode插件

相关链接

  • Vscode 插件官方文档
  • Vscode 应用商店
  • Vscode 官方插件

参考文章

  • /post/711909…
  • Vscode 插件开发攻略
本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!