1

image.png

1 需求

一个简单的需求:用markdown 写的文章,在发布到一些平台上的时候,要反复的上传图片,如果在写博文的时候,可以把markdown 中的图片直接上传到图床服务,markdown 中的图片都是通用的URL ,就不用每个平台都上传一次图片了。

编辑markdown 最多的是 VSCode,VSCode 的扩展体系也比较成熟,那就写一个 VSCode 插件,在编辑 markdown文件的时候,可以右键选中图片本地链接,上传到服务上,然后用服务上的链接替换掉本地的链接。

2 开发

VSCode extension 开发基础

开发文档链接

VSCode extension 不是什么都能做,总共分四大类:

  • 通用能力

    • 注册命令、配置、快捷键绑定和 右键菜单
    • 存储工作共建或全局数据
    • 展示通知消息
    • 利用 Quick Pick 收集用户输入
    • 打开文件系统让用户选中文件或文件夹
  • 主题
  • 声明语言和拓展语言特性
  • 工作台扩展

我们只用到了通用能力中命令注册和右键菜单,其他类型也可以细分,这里不展开将,可以自己看文档中拓展能力的介绍:https://code.visualstudio.com/api/extension-capabilities/overview

初始化项目

VSCode extension 开发,初始化项目用的是 项目模板共建yo,所有要先安装yo,文档:https://code.visualstudio.com/api/get-started/your-first-extension

npx --package yo --package generator-code -- yo code

之后我们创建了一个 VSCode extension 的 helloworld 项目,在初始化项目中有三个点关注,1 我们在那里写代码,2是我们怎么调试写的代码,3在那里更改初始化的参数配置。

插件的入口是 extension.ts 文件,在这里写自己的逻辑

image.png

因为模板已经为我们声明了调试的配置(launch.json),我们在导航栏debugger图标,就可以开始调试,启动调试后会出现一个新的 VSCode 窗口。
image.png

在新的窗口 Command + shift + P ,输入 Image Upload,消息提示出现。

image.png

我们的 image upload命令 在package.json 中,配置如下

// package.json
 "contributes": {
    "commands": [
      {
        "command": "image-linker.updloadImage",
        "title": "Upload Image"
      }
    ],
  }

开始实现功能

第一步:给编辑markdown 文件添加右键选中菜单,这里只需要在package.json中 添加一个配置,并且配置限制了编辑文件的类型。

 // package.json
 "contributes": {
 
   "menus": {
      "editor/context": [
        {
          "command": "image-linker.updloadImage",
          "when": "resourceExtname == .md",
          "group": "navigation"
        }
      ]
    }
 }

第二步:实现触发右键菜单执行命令后的逻辑。 获取选中的文本,在文本中提取 图片的本地文件路径,把路径传给 图片上传函数,返回图片的 图床链接URL,然后替换 选中的图片本地文件路径。


// extension.ts

export function activate(context: vscode.ExtensionContext) {

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

    const disposable = vscode.commands.registerCommand('image-linker.updloadImage', async (...args: any[]) => {
    
        vscode.window.showInformationMessage('Hello World from image-linker!');
        const editor = vscode.window.activeTextEditor;
        if (editor) {
            const selection = editor.selection;
            const selectedText = editor.document.getText(selection);
            // 正则表达式匹配 Markdown 图片链接
            const markdownImageRegex = /!\[(.*?)\]\((.*?)\)/;
            const match = markdownImageRegex.exec(selectedText);

            if (match && match[2]) {
                const imagePath = decodeURIComponent(match[2]);
                const currentFilePath = editor.document.uri.fsPath;
                const currentDir = path.dirname(currentFilePath);

                // 组合得到绝对路径
                const absoluteImagePath = path.isAbsolute(imagePath) ? imagePath : path.join(currentDir, imagePath);
                // 检查文件路径是否为本地文件
                if (fs.existsSync(absoluteImagePath)) {
                    try {
                        const imageUrl = await uploadImageToHostingService(absoluteImagePath);
                        const replacementText = `![${match[1]}](${imageUrl})`;

                        editor.edit(editBuilder => {
                            editBuilder.replace(selection, replacementText);
                        });
                    } catch (error: any) {
                        vscode.window.showErrorMessage('图片上传失败: ' + error.message);
                    }
                } else {
                    vscode.window.showErrorMessage('选中的不是有效的本地图片路径:' + absoluteImagePath);
                }
            } else {
                vscode.window.showErrorMessage('选中的内容不是有效的 Markdown 图片链接');
            }
        }
    
    });

    context.subscriptions.push(disposable);
}

第三步:实现文件上传。不同的图床服务有不同的接口和图片上传方法,这个是自己搭建的简单的图床服务,需要三个参数,这三个参数目前先固定。主要逻辑上是读取图片的本地文件,上传成功后返回可以显示图片的路径。


async function uploadImageToHostingService(imagePath: string): Promise<string> {
    
    // TODO 从配置冲获取参数
    const uploadUrl = “”;
    const apiToken = “”;
    const imageShowBaseURL = “”;
    if (!uploadUrl || !apiToken || !imageShowBaseURL) {
        throw new Error('请在插件设置中配置图床服务的相关信息。');
    }
    const formData = new FormData();
    const fileBuffer = fs.readFileSync(imagePath);
    const fileName = path.basename(imagePath);
    const file = new File([fileBuffer], fileName); // 创建一个 File 对象
    formData.append('file', file);
    // return ImageShowBaseURL;
    // 在这里替换为你使用的图床服务的上传逻辑
    const response = await fetch(uploadUrl, {
        method: 'POST',
        body: formData,
        headers: {
            // 添加必要的请求头,例如 API 密钥等
            'Authorization': `Bearer ${apiToken}`, // 示例:图床服务可能需要授权
        }
    })

    if (!response.ok) {
        throw new Error(`上传失败,服务器返回状态码: ${response.status}`);
    }

    const result = await response.json() as { code: number, data: { id: number } };

    if (result && result.data) {
        return imageShowBaseURL + result.data.id;
    } else {
        throw new Error('上传失败,未返回有效的图片链接');
    }
}

第四步:是把图片图床服务的配置从VSCode 配置中获取,主要有2个地方需要修改,参数声明和参数获取。 参数声明在 package.json中

// package.json

"contributes": {
    "commands": [
      {
        "command": "image-linker.updloadImage",
        "title": "Upload Image"
      }
    ],
    "configuration": {
      "type": "object",
      "title": "Image Linker",
      "properties": {
        "imageUploader.uploadUrl": {
          "type": "string",
          "default": "https://api.someseivec.cn/chunk/upload",
          "description": "The URL of the image hosting service's upload endpoint.",
          "scope": "application"
        },
        "imageUploader.imageShowBaseUrl": {
          "type": "string",
          "default": "https://api.some.cn/w?id=",
          "description": "The URL of the image show url base",
          "scope": "application"
        },
        "imageUploader.apiToken": {
          "type": "string",
          "default": "",
          "description": "The API token used for authenticating with the image hosting service.",
          "scope": "application"
        }
      }
    },}

参数获取

 async function uploadImageToHostingService(imagePath: string): Promise<string> {
  // 读取插件的配置项
    const configuration = vscode.workspace.getConfiguration('imageUploader');
    const uploadUrl = configuration.get<string>('uploadUrl');
    const apiToken = configuration.get<string>('apiToken');
    const imageShowBaseURL = configuration.get<string>('imageShowBaseUrl');
    if (!uploadUrl || !apiToken || !imageShowBaseURL) {
        throw new Error('请在插件设置中配置图床服务的相关信息。');
    }
    
    // ......
    
}

3 构建 vsix 离线安装包

功能开发完成,运行 npm run build , 构建出生成的可运行插件,但是要在VSCode 用,还要打包成vsix离线包。这里要额外的安装一个 官方的工具包("@vscode/vsce": "^3.0.0",)在package.json中添加 packer 执行命令

  "scripts": {
    "compile": "tsc -p ./",
    "packer": "vsce package -o  out/image-linker-$npm_package_version.vsix",
    "build": "npm run compile && npm run packer",
    "watch": "tsc -watch -p ./",
    "lint": "eslint src --ext ts",
    "test": "vscode-test"
  },
  "vsce": {
    "baseImagesUrl": "https://my.custom/base/images/url",
    "dependencies": true,
    "yarn": false
  },

再运行 build 命令,在out文件夹下会得到一个 vsix包,如下图:

image.png

在 VSCode 扩展 栏下安装离线包,成功后就可以使用了,但正常工作需要配置 图传的参数.

安装离线包

安装离线包

插件配置项

插件配置项

右键菜单效果

右键菜单效果

总结

VSCode 扩展 从想法 到实现用了一个上午,代码并不多,开发中遇到的主要问题是配置项不好找,不知道能实现那些功能,在那里配置,代码写在哪里。我在开发时候一直在用 GPT 代替自己找文档,GPT 给的配置不行再仔细看相关的文档。其中遇到的一个坑是 使用pnpm 运行 vsce 一直报错,最后只能改用npm。


today
906 声望41 粉丝