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 文件,在这里写自己的逻辑
因为模板已经为我们声明了调试的配置(launch.json),我们在导航栏debugger图标,就可以开始调试,启动调试后会出现一个新的 VSCode 窗口。
在新的窗口 Command + shift + P ,输入 Image Upload,消息提示出现。
我们的 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包,如下图:
在 VSCode 扩展 栏下安装离线包,成功后就可以使用了,但正常工作需要配置 图传的参数.
安装离线包
插件配置项
右键菜单效果
总结
VSCode 扩展 从想法 到实现用了一个上午,代码并不多,开发中遇到的主要问题是配置项不好找,不知道能实现那些功能,在那里配置,代码写在哪里。我在开发时候一直在用 GPT 代替自己找文档,GPT 给的配置不行再仔细看相关的文档。其中遇到的一个坑是 使用pnpm 运行 vsce 一直报错,最后只能改用npm。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。