记录一个 VSCode markdown 图片上传拓展的开发

329 阅读5分钟

image.png转存失败,建议直接上传图片文件

1 需求

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

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

2 开发

VSCode extension 开发基础

开发文档链接

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

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

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

初始化项目

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

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。