开发一款超级实用的VS Code插件

5,397 阅读5分钟

uploadimage-to-qiniu

VS Code编辑器右键选择图片上传到七牛云,自动把链接粘贴到当前光标位置,鼠标悬浮在链接上面支持预览图片

啥也不说先看效果!!!

demo.gif

第一步:初始化项目

可以根据官网教程初始化项目,主要关注下图几个文件

目录结构.png

第二步:功能开发

  • 先看extension.ts 是插件的入口文件,util目录下面几个文件都是基于extension.ts具体功能的方法实现
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
import { upImageToQiniu } from './util/upload';
import { getHoverHttpLink, translateImageUrlToBase64 } from './util/handleHover';

// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
  // Use the console to output diagnostic information (console.log) and errors (console.error)
  // This line of code will only be executed once when your extension is activated
  // The command has been defined in the package.json file
  // Now provide the implementation of the command with registerCommand
  // The commandId parameter must match the command field in package.json
  let texteditor = vscode.commands.registerTextEditorCommand(
    'extension.choosedImage',
    async (textEditor, edit, args) => {
      console.log('选择图片');
      const qiniuConfig = vscode.workspace.getConfiguration('upload_qiniu_config');
      const uri = await vscode.window.showOpenDialog({
        canSelectFolders: false,
        canSelectMany: false,
        filters: {
          images: ['png', 'jpg', 'gif', 'jpeg', 'svg'],
        },
      });
      if (!uri) {
        return;
      }
      const upConfig = {
        accessKey: qiniuConfig.accessKey,
        secretKey: qiniuConfig.secretKey,
        domain: qiniuConfig.domain,
        gzip: qiniuConfig.gzip,
        scope: qiniuConfig.scope,
        directory: qiniuConfig.directory,
        imageWidth: qiniuConfig.imageWidth,
        formatWebp: qiniuConfig.formatWebp
      };
      const loaclFile = uri[0].fsPath;
      upImageToQiniu(
        loaclFile,
        (res: string) => {
          let url = res;
          // 将图片链接写入编辑器
          if(upConfig.imageWidth !== ''){
            url = `${url}?imageView2/2/w/${upConfig.imageWidth}`;
          }

          if(upConfig.formatWebp){
            url = `${url}/format/webp`;
          }

          console.log('图片上传成功', url);
          addImageUrlToEditor(url);
        },
        upConfig
      );
    }
  );

  // 鼠标悬浮预览图片
  vscode.languages.registerHoverProvider('*', {
    async provideHover(document, position) {
      try {
        const { character } = position;
        // 当前行的文本内容
        const currentLineText = document.lineAt(position).text;
        // 匹配当前行内
        const httpLink = getHoverHttpLink(currentLineText, character);
        var strToBase64 = await translateImageUrlToBase64(httpLink);
        const markString = strToBase64 ? new vscode.MarkdownString(`![](${strToBase64})`, true) : '';
        return {
          contents: [markString],
        };
      } catch (err) {
        console.log('error', err);
      }
    },
  });
  context.subscriptions.push(texteditor);
}

// 将图片链接写入编辑器
function addImageUrlToEditor(url: string) {
  let editor = vscode.window.activeTextEditor;
  if (!editor) {
    return;
  }
  // 替换内容
  const selection = editor.selection;
  editor.edit((editBuilder) => {
    editBuilder.replace(selection, url);
  });
}

// this method is called when your extension is deactivated
export function deactivate() {}
  • util/upload.ts 实现上传到七牛云的方法
const path = require('path');
const fs = require('fs');
const qiniu = require('qiniu');
const imagemin = require('imagemin');
const imageminPngquant = require('imagemin-pngquant');
const imageminJpegtran = require('imagemin-jpegtran');
import * as vscode from 'vscode';
import { getBufferFromFile, bufferToStream } from './base';

// 获取七牛token
const getToken = (accessKey: string, secretKey: string, scope: string) => {
  const options = {
    scope,
  };
  const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
  const putPolicy = new qiniu.rs.PutPolicy(options);
  const uploadToken = putPolicy.uploadToken(mac);
  return uploadToken;
};

// 图片压缩
const imageGzip = async (loaclFile: string): Promise<any> => {
  const bufferFile = await getBufferFromFile(loaclFile);
  let res;
  try {
    res = await imagemin.buffer(bufferFile, {
      plugins: [
        imageminJpegtran(),
        imageminPngquant({
          quality: [0.6, 0.8],
        }),
      ],
    });
    console.log('图片压缩成功', res);
  } catch (err) {
    vscode.window.showInformationMessage('图片压缩失败');
    res = null;
  }
  return res;
};

// 七牛上传配置
export interface QiNiuUpConfig {
  domain: string // 上传后域名
  accessKey: string // 七牛参数
  secretKey: string // 七牛参数
  scope: string // 七牛上传空间
  gzip: boolean // 是否需要压缩
  directory: string // 指定目录
  imageWidth: string // 图片展示宽度
  formatWebp: boolean // 是否自动转成webp格式
}

// 上传图片到七牛云
export const upImageToQiniu = async (
  loaclFile: string,
  cb: { (res: any): void; (arg0: any): void },
  upConfig: QiNiuUpConfig
) => {
  // 将图片路径统一为 xx/xxx
  const filePathArr = loaclFile.split(path.sep);
  loaclFile = path.posix.join(...filePathArr);

  const config = new qiniu.conf.Config();
  const formUploader = new qiniu.form_up.FormUploader(config);
  const putExtra = new qiniu.form_up.PutExtra();
  const token = getToken(upConfig.accessKey, upConfig.secretKey, upConfig.scope);
  let gzipImage;
  if (upConfig.gzip) {
    console.log('已经开启压缩');
    gzipImage = await imageGzip(loaclFile);
  }
  const file = filePathArr.pop();
  const fileName = file?.split('.')[0];
  const fileType = file?.split('.')[1];

  // 文件目录+文件名称
  const keyToOverwrite = `${upConfig.directory}/${fileName}-${new Date().getTime()}.${fileType}`;
  // 上传调用方法
  const uploadFnName = gzipImage ? 'putStream' : 'putFile';
  // 上传内容
  const uploadItem = gzipImage ? bufferToStream(gzipImage) : path.normalize(loaclFile);
  // 七牛上传
  formUploader[uploadFnName](
    token,
    keyToOverwrite,
    uploadItem,
    putExtra,
    function (respErr: any, respBody: any, respInfo: any) {
      if (respErr) {
        throw respErr;
      }

      if (respInfo.statusCode === 200) {
        const url = `${upConfig.domain}/${respBody.key}`;
        cb(url);
      } else {
        vscode.window.showInformationMessage(`上传失败: ${respInfo.statusCode}`);
      }
    }
  );
};
  • util/base.ts
const fs = require('fs');
const duplex = require('stream').Duplex;

// 获取buffer
export const getBufferFromFile = (filePath: string): Promise<Buffer> => {
  return new Promise((resolve, reject) => {
    fs.readFile(filePath, function (err: any, res: any) {
      if (!err) {
        resolve(res);
      }
    });
  });
};

// buffer 转 stream
export const bufferToStream = (buffer: Buffer) => {
  let stream = new duplex();
  stream.push(buffer);
  stream.push(null);
  return stream;
};
  • util/handleHover.ts 实现链接自动粘贴到光标位置和鼠标悬浮链接上面预览图片
import * as https from 'https';
import * as http from 'http';

// 将链接左右两边的引号删掉
const filterHttpLink = (link: string): string => {
  if (link) {
    link = link.substr(0, link.length - 1);
    link = link.substr(1);
  }
  return link;
};

// 获取http链接 在字符串中的位置
const getHttpLinkPosition = (content: string): Array<any> => {
  const regx = /["|'][http(s)://](.*?)["|']/g;
  // @ts-ignore
  const matchArr = [...content.matchAll(regx)];
  const arr: any[] = [];
  matchArr.forEach((item: any) => {
    const url = filterHttpLink(item[0]);
    arr.push({
      start: item.index - 1,
      end: item.index - 1 + url.length,
      value: url,
      length: url.length
    });
  });
  return arr;
};

// 获取hover的 http链接
export const getHoverHttpLink = (content: string, position: number): string => {
  let link = '';
  const httpPositions = getHttpLinkPosition(content);
  if (httpPositions.length) {
    httpPositions.forEach((item) => {
      if (item.start <= position && item.end >= position) {
        link = item.value;
      }
    });
  }
  return link;
};

// 图片添加裁剪参数
export const addImageCropParam = (
  url: string,
  width?: number,
  height?: number,
  type?: number
): string => {
  // 如果url中已经带有裁剪参数,先去掉之前的参数
  const [path] = url.split('?imageView2');
  url = path;

  let cropUrl = type ? `?imageView2/${type}` : '?imageView2/2';
  if (!!width) {
    cropUrl += `/w/${width}`;
  }
  if (!!height) {
    cropUrl += `/h/${height}`;
  }
  if (!!width || !!height) {
    url += cropUrl;
  }

  return url;
};

// 将图片链接转为base64
export const translateImageUrlToBase64 = (url: string): Promise<string> => {
  return new Promise((resolve, reject) => {
    let resUrl = '';
    // 链接是否为https
    const isHttps = url.includes('https');

    if (!url) {
      resolve(resUrl);
    } else {
      url = addImageCropParam(url, 100)
      ;(isHttps ? https : http).get(url, {}, function (res: http.IncomingMessage) {
        const contentType = res.headers['content-type'];
        // 请求为图片
        if (contentType && contentType.includes('image')) {
          var chunks: Array<any> = []; //用于保存网络请求不断加载传输的缓冲数据
          var size = 0; //保存缓冲数据的总长度
          res.on('data', function (chunk: any) {
            chunks.push(chunk);
            //累加缓冲数据的长度
            size += chunk.length;
          });
          res.on('end', function (err: any) {
            //Buffer.concat将chunks数组中的缓冲数据拼接起来,返回一个新的Buffer对象赋值给data
            var data = Buffer.concat(chunks, size);
            //将Buffer对象转换为字符串并以base64编码格式显示
            const base64Img = data.toString('base64');
            resolve(`data:image/png;base64,${base64Img}`);
          });
        } else {
          resolve(resUrl);
        }
      });
    }
  });
};

第三步:插件配置

package.json 包含插件的配置信息(插件命令、快捷键、菜单均在此配置)

  • menus
    • editor/context 编辑器上下文菜单
      • when 控制菜单何时出现
      • command 定义菜单被点击后要执行什么操作
      • group 定义菜单分组
  • command 命令配置
  • configuration 插件配置
"contributes": {
   "menus": {
    "editor/context": [
      {
        "when": "editorFocus",
        "command": "extension.choosedImage",
        "group": "navigation"
      }
    ]
  },
  "commands": [
    {
      "command": "extension.choosedImage",
      "title": "选择图片"
    }
  ],
  "configuration": [
    {
      "title": "上传七牛插件配置项",
      "properties": {
        "upload_qiniu_config.domain": {
          "type": "string",
          "default": "",
          "description": "设置上传域名"
        },
        "upload_qiniu_config.accessKey": {
          "type": "string",
          "default": "",
          "description": "设置七牛上传accessKey"
        },
        "upload_qiniu_config.secretKey": {
          "type": "string",
          "default": "",
          "description": "设置七牛上传secretKey"
        },
        "upload_qiniu_config.scope": {
          "type": "string",
          "default": "",
          "description": "设置七牛上传上传空间"
        },
        "upload_qiniu_config.gzip": {
          "type": "boolean",
          "default": "true",
          "description": "是否启用图片压缩"
        },
        "upload_qiniu_config.directory": {
          "type": "string",
          "default": "",
          "description": "设置七牛上传指定目录"
        },
        "upload_qiniu_config.imageWidth": {
          "type": "string",
          "default": "",
          "description": "设置图片展示宽度"
        },
        "upload_qiniu_config.formatWebp": {
          "type": "boolean",
          "default": "",
          "description": "是否自动转成webp格式"
        }
      }
    }
  ]
},

第四步:本地调试

  1. 安装vsce打包工具
npm install -g vsce
  1. 执行打包命令
vsce package

vsce.png

打包完成后会在根目录生产.vsix文件,点击右键选择“安装扩展VSIX”

  1. 运行调试

默认情况下,工程已经帮我们配置好了调试相关参数(有兴趣的可以查看.vscode/launch.json文件的写法),切换到vscode运行和调试面板,然后点击Run Extension,默认会打开一个新窗口,然后我们就可以在新窗口去测试和验证,调试控制台也可以看到console.log的输出结果。

console.png

本地验证没问题的话,那就可以准备发布了~~

第五步:发布

  1. 注册账号login.live.com/

  2. 访问aka.ms/SignupAzure…,如果你从来没有使用过Azure,那么会看到如下提示:

azure.png

点击继续,默认会创建一个以邮箱前缀为名的组织。

  1. 默认进入组织的主页后,点击右上角的Security

1.jpg

点击创建新的个人访问令牌,这里特别要注意Organization要选择all accessible organizationsScopes要选择Full access,否则后面发布会失败。

2.jpg

创建令牌成功后你需要本地记下来,因为网站是不会帮你保存的。

  1. 获得个人访问令牌后,使用vsce以下命令创建新的发布者:
vsce create-publisher your-publisher-name  

创建成功后会默认登录这个账号,接下来你可以直接发布了,当然,如果你是在其它地方创建的,可以试用vsce login your-publisher-name来登录。

  1. 执行发布命令
npm run pubilsh

发布成功后,可以访问marketplace.visualstudio.com... 查看

在VSCode中也可以搜索进行安装

微信截图_20220725123519.png

  1. 增量发布

版本号:major.minor.patch

如果想让发布之后版本号的patch自增,例如:1.0.2 -> 1.0.3,可以执行命令

vsce publish patch

以此类推...

郑重申明: 文章有参考自 从零开发Vscode上传图片插件,如有侵权,请联系删除!