VSCode插件开发笔记

# VSCode插件开发笔记

VSCode (opens new window)应该算是目前使用率最高的代码编辑器了吧,它给我们开放了一些API方便进行插件开发,方便我们解决开发中的一些问题,提高生产效率。

# 代码自动补全插件

项目开发中,经常会遇到代码补全的功能~

VsCode本身就有很多内置的代码片段,例如:JavaScript、TypeScript、Markdown 和 PHP;代码片段也可以帮助我们进行快速的输入,一般的代码片段都不止一行代码,可以帮我们省略很多输入。除了内置的代码片段,我们也可以配置自己的代码片段

Snippets in Visual Studio Code (opens new window)

前端常用的代码补全插件有:antd-snippets (opens new window), vetur (opens new window), Vue 3 Snippets (opens new window)等,这里从0到1实现一个我自己的代码补全插件~

# 创建项目

  • 首先全局安装开发插件的脚手架:npm install -g yo generator-code

    1. yo模块全局安装后就安装了YeomanYeoman是通用型项目脚手架工具,可以根据一套模板,生成一个对应的项目结构;
    2. generator-code模块是VS Code扩展生成器,与yo配合使用才能构建项目。
  • 安装完成后执行:yo code, 初始化项目:

? What type of extension do you want to create? New Code Snippets # 选择代码片段
Folder location that contains Text Mate (.tmSnippet) and Sublime snippets (.sublime-snippet) or press ENTER to start with a new snippet file.
? Folder name for import or none for new: 
? What's the name of your extension? code-snippets 
? What's the identifier of your extension? code-snippets
? What's the description of your extension? ui code snippets
Enter the language for which the snippets should appear. The id is an identifier and is single, lower-case name such as 'php', 'javascript'
? Language id: javascript
? Initialize a git repository? Yes

输入一些基础信息后项目就创建成功了~

项目目录结构:

├── .vscode/
├── snippets/
    ├── snippets.json // 配置代码片段
└── package.json

package.json:

"engines": {
    "vscode": "^1.85.0" // 最低支持的vscode版本,使用的vscode需在此版本之上
  },
  "categories": [ // 分类
    "Snippets"
  ],
  "contributes": {
    "snippets": [ // 代码片段
      {
        "language": "javascript", // 支持js
        "path": "./snippets/snippets.json"
      },
      // ...
    ]
  }

具体参考extension-manifest (opens new window)

  • 添加代码片段

有一个网站可以帮助我们快速的创建code snippet: snippet-generator (opens new window)

在这个网站里,左边输入代码,右侧就会生成 snippet 模板,拷贝到项目中的 snippets.json 文件下的 JSON 对象中即可~

// snippets/snippets.json

{
  "fn snippets": { // 名称
    "prefix": ["fn"], //  触发字符,可模糊匹配
    "body": [ // 实际插入的代码片段
            "function fn(${1:val}: number, ${2:val2}: string) {", // 每一项表示换行
            "    console.log ($CURRENT_YEAR: val, val2);",
            "}"
    ],
    "description":[ // 描述说明
      "快速写一个fn函数",
      "",
      "调用fn方法",
      "@param val — 参数1",
      "@param val2 — 参数2."
    ]
  }
}
  • 使用$1, $2标识tab定位,使用tab可快速切换
  • 使用${1:val}标识tab定位,且有默认值
  • 一些内置变量,比如CURRENT_YEAR,可用于自定义注释头

snippet语法 (opens new window)

# 本地调试

  • 点击VSCode左侧菜单Run and Debug => 顶部Extension,运行,会新开一个vscode窗口;

顶部的调试信息可以在.vscode/launch.json中进行配置~

  • 新建一个 js 文件夹,输入fn:

如有上图提示,即表示新添加代码片段生效,点击即会添加配置的代码片段~

这里只是添加一个fn作为示例,其他的代码片段跟这个一样,添加到 json 中即可, 这里不再赘述~

# 添加到本地

如果只是自己使用,可直接在VSCode本地中新建代码片段~

  • 文件-->首选项-->用户代码片段-->点击新建代码片段, 如:取名vue.json, 确定~

这是会在~/Library/Application Support/Code/User/snippets/目录下生成一个vue.json.code-snippets文件~

  • 之后直接在里面添加想要的代码片段即可,保存后,重新打开项目即可使用~

参考:VSCode 初次写vue项目并一键生成.vue模版 (opens new window)

这里如果想把上面的代码片段添加到本地,也是添加到这里即可~

  • 也可以通过cmd + shift + P => 输入user snippets => 新加代码片段 进行添加,将 json 拷贝进去就可以在 vscode 中使用了, 效果是一样的~

# 发布

如果是想给团队使用,就可以将该代码片段封装成插件发布,这样别人直接下载对应的插件就可以多个编辑器通用,比存在本地好很多~

第一次访问需要先创建一个Azure DevOps组织,默认会创建一个以邮箱前缀为名的组织~

  • 组织创建成功后,点击右上角的个人头像 => Personal access tokens,创建个人访问令牌;

注意: 这里的 organizations 必须要选择 all accessible organizations; Scopes 要选择 full access,否则后面发布会失败。

创建 token 成功后将token复制,先本地进行保存,之后会用到~!!!

  • 之后需要创建一个发布者:发布者是 visualstudio (opens new window) 代码市场的扩展的唯一身份标识。每个插件都需要在 package.json 文件中指定一个 publisher 字段。

具体创建点击发布者管理 (opens new window),按提示创建即可~

其中NameID是唯一且必填的,可配置Logo和其他自定义信息~

发布者创建成功后,接下来就可以开始正式发布插件了~!

  • 首先全局安装vsce: npm install vsce -g

  • 输入刚才创建的 publisher 进行登录:vsce login <publisher name>; 再输入刚复制的个人令牌token,即可登录成功;

  • 登录成功之后,就可以发布了;在发布前检查下packge.json信息:

{
  "name": "vn-code-snippets", // name
  "displayName": "vn-code-snippets", // 显示名称
  "description": "ui code snippets", // 描述
  "version": "0.0.1", // 版本号
  "publisher": "verneyzhou", // 发布者id
  "engines": {
    "vscode": "^1.85.0" // 兼容的vscode版本号
  },
  "categories": [ // 分类
    "Snippets"
  ],
  "contributes": {
    "snippets": [ // 代码片段
      {
        "language": "javascript", // 支持的语言
        "path": "./snippets/snippets.json" // json路径
      },
      {
        "language": "javascriptreact",
        "path": "./snippets/snippets.json"
      },
      {
        "language": "typescript",
        "path": "./snippets/snippets.json"
      },
      {
        "language": "typescriptreact",
        "path": "./snippets/snippets.json"
      }
    ]
  },
  "author": "zhou",
  "license": "MIT", // 开源协议
  "repository": { // 代码仓库地址
    "type": "git",
    "url": "*****"
  }
}
  • 之后直接执行vsce publish,即可进行发布~

如果不出意外,应该就会发布成功;如果报错可能是pkg脚本配置不全,按报错提示检查下吧~

➜  code-snippets git:(master) ✗ vsce publish
 WARNING  LICENSE.md, LICENSE.txt or LICENSE not found
Do you want to continue? [y/N] y
 INFO  Publishing 'verneyzhou.vn-code-snippets v0.0.1'...
 INFO  Extension URL (might take a few minutes): https://marketplace.visualstudio.com/items?itemName=verneyzhou.vn-code-snippets
 INFO  Hub URL: https://marketplace.visualstudio.com/manage/publishers/verneyzhou/extensions/vn-code-snippets/hub
 DONE  Published verneyzhou.vn-code-snippets v0.0.1.
  • 发布成功之后,在vscode扩展市场应该就可以搜索到该插件了:

https://marketplace.visualstudio.com/items?itemName=verneyzhou.vn-code-snippets (opens new window)

注意,itemName应该为${publisher}.${name}~

# 使用

之后就跟一般的插件一样,install安装,即可使用了:

发布之后也可以取消发布,或删除扩展,更多关于发布的操作可以参考VSCode发布插件官方文档:publishing-extension (opens new window)

# 打包扩展

对于某些打算给团队使用,但又不想发布到线上的情况,可以将插件打包,私下分享给别人~

包装扩展 (opens new window)

  • 首先执行打包命令:vsce package

此命令将在扩展的根文件夹下生成一个.vsix文件,如:vn-code-snippets-0.0.1.vsix

  • 安装:
    • 方法1:点击vscode左侧扩展菜单 => 右上角... => 从 VSIX 安装,选择刚打包生成的文件即可~
    • 方法2:打包完成后,终端执行:code --install-extension vn-code-snippets-0.0.1.vsix
➜  code-snippets git:(master) ✗ code --install-extension vn-code-snippets-0.0.1.vsix
Installing extensions...
(node:85301) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.
(Use `Electron --trace-deprecation ...` to show where the warning was created)
Extension 'vn-code-snippets-0.0.1.vsix' was successfully installed.
➜  code-snippets git:(master)
  • 安装完成后即可在扩展中搜到了~,然后直接在代码中使用即可~~~!!!

如果不生效,重启 vscode 试试~~

# VSCode翻译插件

在平时项目开发中经常会遇到不认识的英文单词,这里开发一个翻译插件,方便在vscode中直接进行翻译~

# 初始化项目

  • 由于上面已经全局安装了yo,这里直接执行:yo code:
? What type of extension do you want to create? New Extension (TypeScript) # 选择ts
? What's the name of your extension? translate
? What's the identifier of your extension? translate # 项目名称
? What's the description of your extension? translate extension
? Initialize a git repository? Yes
? Bundle the source code with webpack? No
? Which package manager to use? npm

初始化完成后会生成一个项目,接下来看看这个项目默认生成哪些内容~

项目目录结构:

├── .vscode/
├── src/
    ├── extension.ts // 初始项目时的入口文件
└── package.json
└── tsconfig.json // ts配置

package.json:

{
    "engines": {
        "vscode": "^1.85.0"
    },
    "contributes": {
        "commands": [ // 命令列表
        {
            "command": "translate.helloWorld", // 命令的id
            "title": "Hello World" // 命名语句
        }
        ]
    },
}

初始项目时的入口文件:src/extension.ts:

import * as vscode from 'vscode';

// 这里执行插件被激活时的操作
export function activate(context: vscode.ExtensionContext) {

	console.log('Congratulations, your extension "translate" is now active!');

	// 注册命令:translate.helloWorld,该命令需在 pkg 已经被定义
	// 当执行 translate.helloWorld 命令时,会触发后面的回调函数
	let disposable = vscode.commands.registerCommand('translate.helloWorld', () => {
		// 触发了一个弹出框
		vscode.window.showInformationMessage('Hello World from translate!');
	});
	// 把这个对象放入上下文中, 使其生效
	context.subscriptions.push(disposable);
}

// 插件被销毁时调用的方法, 比如可以清除一些缓存, 释放一些内存
export function deactivate() {}

接下来我们试着调试下这个插件,看看是否能调试成功~

  • 打开translate项目,点击VSCode左侧菜单Run and Debug => 顶部Extension,运行,会新开一个vscode调试窗口,这个窗口默认集成了我们当前开发的这个插件工程;

或者fn + F5也可以新开调试窗口~

  • 然后cmd + shift + P,输入:Hello World,不出意外的话,项目调试控制台会打印:Congratulations, your extension "translate" is now active!,调试窗口右下角会弹窗展示:Hello World from translate!

这样就表示我们的插件初始化成功~~🎉🎉

# 接入翻译API

  • 翻译API这里选择有道云:首先进入有道智能云服务平台 (opens new window),注册,添加微信客服,填写问卷会送50体验金,个人开发够用了;

  • 注册成功后进入到有道智云控制台 (opens new window),首先创建一个应用,选择服务为自然语言翻译服务,接入方式为API;应用创建成功后会获得应用ID应用秘钥;同时也可以在应用控制台看到js接入实例;

有道云文本翻译 API (opens new window)

# 翻译API秘钥本地配置

之后在我们的插件中会用到生成的应用ID和应用秘钥;但如果直接在代码中暴露appSecret,有被盗用造成损失的风险,所以这里需要先将appSecret配置在VSCode本地~

  • package.json中添加配置信息:
"contributes": {
    "configuration": { // 添加配置
      "title": "vscodeVnTranslate", // 自定义字段名称
      "type": "object", // 类型
      "properties": { // 属性
        "vscodeVnTranslate.youdaoAppkey": { // 子属性 应用ID
          "type": "string", // 类型
          "description": "youdao appKey" // 描述
        },
        "vscodeVnTranslate.youdaoAppSecret": { // 应用秘钥
          "type": "string",
          "description": "youdao appSecret"
        }
      }
    }  
  },
  • 然后打开调试窗口,右上角 Code => 首选项 => 设置,展开左侧Extensions菜单,滑到最底部就会看到刚刚新增的vscodeVnTranslate字段;之后将刚创建的应用ID和应用秘钥保存在这里就可以了~

# 插件开发

src目录下新建index.ts,添加插件核心逻辑,这里直接看代码吧~

import * as vscode from "vscode";
import CryptoJS from "crypto-js"; // crypto加密
import axios from "axios"; // 接口请求
import querystring from "querystring";

export interface Word {
  key: string;
  value: string[];
}

// 字符串截取前10位和后10位
function truncate(q: string): string {
  var len = q.length;
  if (len <= 20) {
    return q;
  }
  return q.substring(0, 10) + len + q.substring(len - 10, len);
}

// 驼峰文本格式化: 例如:将helloWorld转换为hello World
function changeWord(text: string): string {
  if (!text.includes(" ") && text.match(/[A-Z]/)) {
    const str = text.replace(/([A-Z])/g, " $1");
    let value = str.substr(0, 1).toUpperCase() + str.substr(1);
    return value;
  }
  return text;
}


// 封装有道翻译接口,具体参数参考下方文档
// https://ai.youdao.com/DOCSIRMA/html/trans/api/wbfy/index.html
async function youdao(query: string, appKey: string, appSecret: string) {
  var appKey = appKey;
  var key = appSecret; //注意:暴露appSecret,有被盗用造成损失的风险
  var salt = new Date().getTime();
  var curtime = Math.round(new Date().getTime() / 1000);
  // 多个query可以用\n连接  如 query='apple\norange\nbanana\npear'
  var from = "auto";
  var to = "auto";
  var str1 = appKey + truncate(query) + salt + curtime + key;
  //  生成加密签名
  var sign = CryptoJS.SHA256(str1).toString(CryptoJS.enc.Hex);

  const res = await axios.post(
    "http://openapi.youdao.com/api",
    querystring.stringify({
      q: changeWord(query), // 待翻译文本
      appKey, // 应用id
      salt, // 盐,随机字符串
      from, // 语言
      to, // 目标语言
      sign, // 签名
      signType: "v3", // 签名类型
      curtime,
    })
  );

  return res.data;
}

// 这里执行插件被激活时的操作
export function activate(context: vscode.ExtensionContext) {

  vscode.window.showInformationMessage('翻译插件成功激活!!!🎉🎉🎉');

  // 拿到配置文件中的有道翻译的appkey和appSecret
  const config = vscode.workspace.getConfiguration("vscodeVnTranslate");
  const appKey = config.get("youdaoAppkey") as string;
  const appSecret = config.get("youdaoAppSecret") as string;
  // 是否开启自动翻译
  const autoTranslate = config.get("openAutoTranslate") as boolean;

  console.log('====appSecret', appSecret, appKey, autoTranslate);

  // 划词hover翻译
  // registerHoverProvider:注册hover事件 
  autoTranslate === true && vscode.languages.registerHoverProvider("*", {
    // VS code 提供一个 provideHover 当鼠标移动在上面的时候就可以根据当前的单词做一些具体操作
    async provideHover(document, position, token) {
     // 获取当前选中的单词  
      const editor = vscode.window.activeTextEditor;
      if (!editor) {
        return; // No open text editor
      }

      if (!appKey || !appSecret) {
        vscode.window.showWarningMessage('请配置有道翻译的appkey和appSecret');
        return;
      }

      // 获取选取文本   
      const selection = editor.selection;
      const text = document.getText(selection);
      console.log('text', text);
      if (!text || !text.length) {
        // vscode.window.showWarningMessage('请选中需要翻译的单词');
        return;
      }

      const res = await youdao(text, appKey, appSecret);
      console.log('res', res);
      if (res.errorCode !== "0") {
        vscode.window.showErrorMessage('翻译失败:', res.errorCode, res.msg);
        return;
      }

      // md格式
      const markdownString = new vscode.MarkdownString();

      markdownString.appendMarkdown(
        `#### 翻译: \n\n ${res.translation[0]} \n\n`
      );
      if (res.basic) {
        // 添加音标展示
        if (res.basic["us-phonetic"]) {
          markdownString.appendMarkdown(
            `**美** ${res.basic["us-phonetic"]}    **英** ${res.basic["uk-phonetic"]} \n\n`
          );
        }

        // 添加解释
        if (res.basic.explains) {
          res.basic.explains.forEach((w: string) => {
            markdownString.appendMarkdown(`${w} \n\n`);
          });
        }
      }
      // 添加网络释义
      if (res.web) {
        markdownString.appendMarkdown(`#### 网络释义 \n\n`);
        res.web.forEach((w: Word) => {
          markdownString.appendMarkdown(
            `**${w.key}:** ${String(w.value).toString()} \n\n`
          );
        });
      }
      markdownString.supportHtml = true; // 支持html标签 
      markdownString.isTrusted = true;

      return new vscode.Hover(markdownString); // hover展示
    },
  });

  // 划词翻译替换
  context.subscriptions.push(
    // 注册命令: vscode.translate.replace
    vscode.commands.registerCommand("vscode.translate.replace", async () => {
      // 获取当前选中的单词
      let editor = vscode.window.activeTextEditor;
      if (!editor) {
        return; // No open text editor
      }

      if (!appKey || !appSecret) {
        vscode.window.showWarningMessage('请配置有道翻译的appkey和appSecret');
        return;
      }

      let selection = editor.selection;
      let text = editor.document.getText(selection);
      if (!text || !text.length) {
        vscode.window.showWarningMessage('请选中需要翻译的单词');
        return;
      }

      //有选中翻译选中的词
      if (text.length) {
        const res = await youdao(text, appKey, appSecret);
        console.log(res);

        //vscode.window.showInformationMessage(res.translation[0]);
        // 替换选中的文本
        editor.edit((builder) => {
          builder.replace(selection, res.translation[0]);
        });
      }
    })
  );
}

// 插件被销毁时调用的方法, 比如可以清除一些缓存, 释放一些内存
export function deactivate() {}

这里需要用到三个库,直接npm install crypto-js axios querystring即可,如果是ts可能还需要npm install -D @types/crypto-js~

  1. 上方代码实现了一个翻译替换命令vscode.translate.replace,选中需要替换的文本,cmd + shift + p,输入`翻译替换即可;

下方也配置了右侧菜单和快捷键可执行该命令~

  1. 同时也实现了鼠标选中文本后,自动翻译文本的功能,该功能可通过配置openAutoTranslate=true来开启;

# 快捷键和菜单配置

可以在package.json中配置快捷键和菜单命令~

{
  "name": "vn-translate-extension", // 插件名称
  "displayName": "vn-translate-extension", // 插件
  "description": "一个简单的翻译插件~", // 描述
  "publisher": "verneyzhou", // 发布者id
  "version": "0.0.1", // 版本
  "engines": {
    "vscode": "^1.85.0" // vscode版本
  },
  "categories": [ // 分类
    "Other"
  ],
  "activationEvents": [ // 指明该插件在何种情况下才会被激活,因为只有激活后插件才能被正常使用
    "onStartupFinished", // 插件启动完成后就会被激活
    // "*", // 只要一启动vscode,插件就会被激活
    // "onCommand:extension.sayHello", // 每当调用命令时,都会激活
  ],
  "main": "src/index.js", // 入口
  "contributes": { // 贡献点,通过扩展注册contributes用来扩展Visual Studio Code中的各项技能,其有多个配置
    "commands": [ // 命令列表
      {
        "command": "vscode.translate.replace", // 命令id
        "title": "翻译替换" // 命令名称
      },
      {
        "command": "translate.helloWorld",
        "title": "hello translate"
      }
    ],
    "keybindings": [ // 绑定快捷键
      {
        "command": "vscode.translate.replace", // 执行命令id
        "key": "ctrl+t", // window快捷键
        "mac": "cmd+t", // mac快键键
        "when": "editorTextFocus" // 触发时机,当文本聚焦的时候
      }
    ],
    "menus": { // 菜单配置
      "editor/context": [ // 编辑器上下文菜单
        {
          "when": "editorTextFocus", // 触发时机,当文本聚焦的时候
          "command": "vscode.translate.replace", // 执行命令id
          "group": "navigation" // 菜单分组,navigation 会排序到菜单顶部
        }
      ]
    },
    "configuration": { // 字段配置信息
      "title": "vscodeVnTranslate",
      "type": "object",
      "properties": {
        "vscodeVnTranslate.youdaoAppkey": {
          "type": "string",
          "description": "有道 appKey"
        },
        "vscodeVnTranslate.youdaoAppSecret": {
          "type": "string",
          "description": "有道 appSecret"
        },
        "vscodeVnTranslate.openAutoTranslate": {
          "type": "boolean",
          "description": "是否开启自动翻译"
        }
      }
    }  
  },
}

上方具体配置信息参考下方官方文档:

激活事件 activationEvents (opens new window)

贡献点 contributes (opens new window)

menus group 菜单排序参考 (opens new window)

# 使用

  • 本地打开调试窗口时,如果openAutoTranslate配置为true,即可使用文本聚焦自动翻译功能:
  • 选中文本,双击呼起右侧菜单时,可看到翻译替换按钮,点击即可替换;也可通过cmd + T快捷键替换:

# 上线

上线流程跟vn-code-snippets插件差不多~

  • 不想上线的话,直接vsce package打包扩展生成.vsix文件,然后安装到本地即可使用~

  • 要发布到线上的话,先vsce login登录,再vsce publish发布,参考上方vn-code-snippets的发布即可~

发布成功后等个一两分钟就可以在官方市场 (opens new window)搜到了~

  • 发布成功后,扩展市场直接搜索即可使用了~~~!!!🎉🎉🎉

# 查看文件信息插件

接下来趁热打铁,再封装一个查看文件信息的插件,大致效果如下:

# 开发

  • 开发流程跟上面的翻译插件类似,这里不再赘述了,直接yo code 初始化插件项目;

  • 项目创建完成后,先看下package.json中的配置:

{
  "activationEvents": [ // 在执行 getFileState 指令时激活
    "onCommand:getFileState"
  ],
  "main": "./out/extension.js",
  "contributes": {
    "commands": [
        {
            "command": "getFileState", // 定义命令id
            "title": "查看文件信息"
        }
    ],
    "menus": { // 菜单项
        "editor/context": [ // 右侧编辑上下文菜单
            {
                "when": "editorFocus",
                "command": "getFileState",
                "group": "navigation" // 菜单顶部展示
            }
        ],
        "explorer/context": [  // 左侧资源管理器上下文菜单
            {
                "command": "getFileState",
                "group": "navigation"
            }
        ]
    }
  },
}
  • 再看下核心js逻辑,比翻译插件简单多了:

import * as vscode from 'vscode';
import fs from 'fs';

// 插件激活时触发,所有代码总入口
export function activate(context: vscode.ExtensionContext) {

	console.log('file state 插件已经被激活');

    // 注册命令
    let commandOfGetFileState = vscode.commands.registerCommand('getFileState', uri => {

        console.log('uri', uri);
        // 文件路径
        const filePath = uri.path.substring(1);
        fs.stat(filePath, (err, stats) => {
            console.log('====fs.stat', err, stats.isFile(), stats.isDirectory());
            if (err) {
                vscode.window.showErrorMessage(`获取文件时遇到错误了${err}!!!`);
            }

            if (stats.isDirectory()) {
                vscode.window.showWarningMessage(`检测的是文件夹,不是文件,请重新选择!!!`);
            }

            if (stats.isFile()) {
                const size = stats.size;
                const createTime = stats.birthtime.toLocaleString();
                const modifyTime = stats.mtime.toLocaleString();

                vscode.window.showInformationMessage(`
                    Hi, 上午好!
                    今天是:${getDate()}
                    又是元气满满的一天呢~~!!!💪🏻💪🏻😄😄🎉🎉

                    您选择的文件路径为:
                    ${filePath}
                    文件大小为: ${size}字节
                    文件创建时间为: ${createTime}
                    文件修改时间为: ${modifyTime}
                `, { modal: true });
            }
        });
    });

    // 将命令放入其上下文对象中,使其生效
    context.subscriptions.push(commandOfGetFileState);
}

// 获取时间
function getDate() {
    let day = new Date();
    // day.setTime(day.getTime() + 24 * 60 * 60 * 1000);
    const weekMap: any = {1: '一', 2: '二', 3: '三', 4: '四', 5: '五', 6:'六', 0: '日', 7: '日'};
    const week = weekMap[day.getDay()];
    let date = `${day.getFullYear()}-${day.getMonth() + 1}-${day.getDate()} ${day.getHours()}:${day.getMinutes()}:${day.getSeconds()}${week}`;
    return date;
}

注意: 上面的showInformationMessage方法添加的{modal: true}参数,需要在首选项 => Settings 中添加:

"window.dialogStyle": "custom"

弹窗样式才能生效哦~

# 发布

本地调试完成后就可以进行发布了~

可以选择打包扩展,也可以直接发布线上;具体流程参考上面翻译插件,不细讲了~

# Chat问答插件

这里开发一个Chat插件,主要会用到 VSCode 的 Webview 功能~

VSCode-WebviewAPI (opens new window), Webview API 允许扩展在 visualstudio 代码中创建完全可定制的视图,可以将 webview 看作是 VS Code 中的 iframe。

# 初始化项目

  • 这里直接yo code创建项目,跟上面的翻译插件一样的,这里就不展示了~

初始化项目完成后,可以先本地调试下,试下是否调通~

# 前端项目初始化

因为VSCode可以用iframe展示线上web网页, 所以这里新起一个前端项目,用于展示视图~

vite (opens new window)

  • 这里是用 vite 快速启了一个vue3项目,npm run dev后本地服务地址为http://localhost:5173/,之后会用到~

# 嵌入前端页面

接下来将web页面展示在vscode侧边栏~

vscode 提供了两种创建iframe的方法,WebviewViewProvidercreateWebviewPanel,选其一即可,这里我们介绍一下WebviewViewProvider如何使用

  • 首先需要在 pkg 中配置视图信息:
"contributes": {
    "commands": [],
    "viewsContainers": { // 自定义视图的视图容器, 必须指定视图容器的标识符、标题和图标
      "activitybar": [ // 活动栏
        {
          "id": "chat-sidebar-view", // 容器id, 确保在下方views中有对应的视图
          "title": "聊一下", // 标题
          "icon": "images/chat-icon.png" // 图标
        }
      ]
    },
    "views": {// 定义视图
      "chat-sidebar-view": [
        {
          "type": "webview", // 类型为webview
          "id": "chat-sidebar-view", // 容器id
          "name": " 聊一下",
          "icon": "images/chat-icon.png",
          "contextualTitle": "聊一下"
        }
      ]
    }
  },

views 是配置视图列表,activitybar 是定义下显示在侧边导航上的视图。

  • 然后在extention.ts注册视图模块,同时引入侧边栏视图ChatWebview
import * as vscode from 'vscode';
import { ChatWebview } from "./chatWebview";

// 插件的入口函数, 当插件第一次加载时会执行activate
export function activate(context: vscode.ExtensionContext) {
	console.log('Congratulations, your extension "chat" is now active!');
	// 实现侧边栏的初始化
	// 实例化一个chatWebview
	const chatWebview = new ChatWebview();
	// 注册webview 到id为 chat-sidebar-view 的views中,这个id为 chat-sidebar-view 的视图我们稍后会在
	// package.json 中声明,先理解为我们要把iframe渲染在那个地方(侧边栏还是标签页)需要在 package.json 中控制
	context.subscriptions.push(
		vscode.window.registerWebviewViewProvider("chat-sidebar-view", chatWebview)
	);

  // 这里实现了一个简单的功能,在vscode打开的文件中,选中代码时会实时展示在web页面上
	// 监听用户选中文本事件
	vscode.window.onDidChangeTextEditorSelection((event) => {
		const editor = event.textEditor;
		let document = editor.document;
		let selection = editor.selection;
	// 获取当前窗口的文本
		let text = document.getText(selection);
		console.log('===onDidChangeTextEditorSelection', text);
		// 上文提到chatWebview可能为null 因此需要可选链写法,所以这里存在不稳定性,不过测试没问题~
		chatWebview?.webview?.webview.postMessage({
		// 第一次postMessage,下一次在chatWebview文件的iframe中
		command: "vscodeSendMesToWeb",
		data: text,
		});
	});
}
  • 接着就是最重要的,新建chatWebView.ts文件,实现页面的展示:
// src/chatWebView.ts

import { window, Position, WebviewView, WebviewViewProvider } from "vscode";
export class ChatWebview implements WebviewViewProvider {
  // 写一个public变量,方便对象引用创建后的webview实例,但是可能存在还未完全解析完成时,访问值为null
  // 看了vscode api发现,resolveWebView 返回一个 Thenable,可以在解析完成后拿到webview实例
  // 但是这个函数是在webview容器第一次显示时自动执行,不需要手动调用,不知道怎么拿到Thenable
  public webview: WebviewView | null = null;
  resolveWebviewView(webviewView: WebviewView): void | Thenable<void> {
    this.webview = webviewView;
    webviewView.webview.options = {
      enableScripts: true,
    };
    // 监听web端传来的消息
    webviewView.webview.onDidReceiveMessage((message) => {
      switch (message.command) {
        case "WebSendMesToVscode":
          // 实现一个简单的功能,将web端传递过来的消息插入到当前活动编辑器中
          let editor = window.activeTextEditor;
          editor?.edit((edit) => {
            let position = editor?.selection
              ? editor?.selection.start
              : new Position(0, 0);
            edit.insert(position, message.data);
          });
          return;
      }
    }, undefined);
    // webview 展示的内容本身就是嵌套在一个iframe中,因此在此html中再嵌套一个iframe时,需要传递两次postMessage
    webviewView.webview.html = `
    <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <style>
        html,
        body {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            background-color:#000000;
            overflow:hidden;
        }
        .webView_iframe {
            width: 100%;
            height: 100%;
            border: none;
        }
        .outer{
          width: 100%;
          height: 100%;
          overflow: hidden;
        }
      </style>
    </head>
    <body>
      <script>
      
      console.log('Hello from the webview!');
      // 向vscode 传递消息的固定写法, vscode 为我们封装好了postMessage
      const vscode = acquireVsCodeApi();
      // 接收来自web页面的消息
      window.addEventListener('message', event => {
          const message = event.data;
          switch (message.command) {
               // 插件传递消息给web端
              case 'vscodeSendMesToWeb':
                  let iframe = document.getElementById('WebviewIframe')
                  WebviewIframe.contentWindow.postMessage(message, "*")
                  console.log("fromWebViewIframe: "+message.data)
                  break;
              // web端发送消息给插件
              case 'WebSendMesToVscode':
                    vscode.postMessage(message);
                    break;
          }
      });

     </script>
        <div class="outer">
           <iframe id='WebviewIframe' class="webView_iframe" sandbox="allow-scripts allow-same-origin allow-forms allow-pointer-lock allow-downloads" allow="cross-origin-isolated; clipboard-read; clipboard-write;" src="http://localhost:5173/"></iframe>
        </div>
    </body>
    </html>
    `;
  }
}

我们刚启动的前端服务http://localhost:5173/会被嵌套在iframe中~

webviewView.webview.onDidReceiveMessage就是监听 web 端向 vscode 发的消息;

webviewView.webview.html里面的内容会被 webview 嵌套在一个父iframe中,而我们的前端页面http://localhost:5173/会嵌套到父iframe子iframe中,所以需要在父iframe中通过window.addEventListener('message', ...)监听 vscode 和 web 之间的通信,并作为通道将双方的通信信息传递过去;

  • 这时打开左上角的Run Extension按钮,会新开一个调试窗口;这时会看到左侧多了一个图标按钮,点击就可以看到我们启动的页面了~~~!!!

如果想像Chrome浏览器中打开调试控制台,点击顶部Help => Toggle Developer Tools即可开始调试了~

# 前端项目与插件通信

上面是实现前端页面嵌入的核心js代码了,接下来解析 web 跟 vscode 是如何通信的:

如上图所示,首先是 web => vscode 通信:

  1. 首先在页面中的click事件中,点击会通过window.parent.postMessage向 vscode 发送信息;
  2. 之后在 chatWebview.tswebviewView.webview.onDidReceiveMessage中就会监听到 web 传过来的信息;

vscode => web 通信:

  1. 代码中通过vscode.window.onDidChangeTextEditorSelection事件监听用户选中文本事件,同时通过chatWebview?.webview?.webview.postMessage向 web 传递信息;
  2. 在前端代码中,通过 window.addEventListener("message", ...)监听 vscode 传过来的信息,从而实现通信。

# 接入文心一言API

这里接入的大语言模型看你自己选择,国外的OpenAI (opens new window)Gemini (opens new window),国内的文心一言等等,都可以;为了方便,我这边选择的是百度的文心一言~

百度智能云千帆大模型平台 (opens new window)

ERNIE-Bot是百度自行研发的大语言模型,覆盖海量中文数据,具有更强的对话问答、内容创作生成等能力。

  • 接下来创建应用:进入应用接入 (opens new window), 点击【创建应用】,输入应用名称,描述即可;创建成功后即会生成API KeySecret Key

  • 然后进入调试API (opens new window)页面,如图选择自己创建的应用,输入body信息,开始调试:

  • 接下来就可以在项目中新建一个文件夹,用Node.js接入文心一言的API了,具体接入传参什么的参考官方文档就行;

这里node后端项目的搭建这里不再赘述了,我用的是express搭建的,接下来主要展示核心代码~

  • chat.js:
// server/chat.js

const ERNIEB ="https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions";

// 获取token
async getAccessToken() {
        return new Promise(async (resolve, reject) => {
            const res = await axios.post('https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' + AK + '&client_secret=' + SK);
            const { data } = res;
            resolve(data.access_token);

        });

    }

// 封装聊天接口
async ask(prompt) {
  // 问句push进去
  this.messages.push({ role: "user", content: prompt });
  console.log("message" + this.messages[0]);
  try {
    const res = await axios.post(
      ERNIEB,
      { messages: this.messages },
      { params: { access_token: await this.getAccessToken() } }
    );
    const { data } = res;
    console.log(data);
    // 答案也放进去
    this.messages.push({ role: "assistant", content: data.result });
    return data.result;
  } catch (error) {
    console.log("调用模型失败" + error);
  }
}

大致逻辑就是按API文档直接调用接口,然后用node封装一层,之后前端直接调用即可~ 具体实现逻辑见源码~

  • 本地启动前端服务,node服务,再Run Extension, 就可以开始本地调试了:

其实这个插件的client目录就是一个完全的前端web项目了,只是通过 vscode 的 webview 将前端的url通过iframe的形式嵌入进来;client就算单独作为一个引入大模型的项目也是成立的,这个插件只是加了跟web页面的通信~

# 本地调试

  • 进入client项目,npm run vercel启动项目;

  • 然后回到chat目录,将chatWebview.ts中的webUrl设置为 client 的本地启动地址,Run Extension打开调试窗口,点击左侧【聊】图标即可开始调试;

  • client中有修改,点击vscode的刷新按钮即可刷新;

# 发布

  • 首先将client前端项目部署vercel: 进入到 vercel 目录,vercel --prod

  • chatWebview.ts下的webUrl改为vercel线上地址,回到chat目录:

    1. vsce package 打包扩展;
    2. vsce publish 发布到线上;

关于插件的发布配置上面已经讲过了,这里不再赘述,参考上面就可以了~

  • 发布成功后直接搜索vn-chat-extension即可使用了~

插件地址 (opens new window)

# 其他

  • 聊天界面UI优化

  • 聊天接口token获取优化,appKey在vscode本地存储

这里appKey的存储可以参考上方翻译插件,存储在vscode本地,然后通过postMessage传给前端web项目~

  • client和server部署

这里用 vercel 的 serverless functions 来部署后端接口,具体参考之前的Vercel部署笔记

  • 增加连续对话能力

  • 引入其他API,比如openAI, Gemini等等

# 源码

上面的实战项目的完整代码都在vscode-plugins-project (opens new window)~

# 报错记录

  • 翻译插件本地调试时报错:Activating extension 'verneyzhou.vn-translate-extension' failed: Cannot use import statement outside a module.

调试时,pkg中的main指向打包后的js文件就暂时好了~

  • chat插件,client项目 vercel dev 本地调试的时候,api/下的接口没生效,但部署到vercel上生效的?

我这边重新新创建一个vite项目,重新配置vercel,重新添加接口后就可以了?!...暂时无解,,,可能是之前的client项目配置vercel哪里有问题吧...

# 收藏

# 参考

上次更新: 1/15/2024, 1:36:41 AM
最近更新
01
taro开发实操笔记
09-29
02
前端跨端技术调研报告
07-28
03
Flutter学习笔记
07-15
更多文章>