前言
在开发自己的网站以前,我一直以为Typora或者掘金的bytemd编辑器中粘贴图片的功能,是真的将图片放进了编辑器里。
由于angular兼容性最好的markdown(以下均简称为md)编辑器我能找到的只有ngx-md,我在自己的网站后台实现类似于掘金编辑器的方法是左边一个textare,右边一个ngx-md渲染。
当我傻傻的对着一个textare粘贴图片却发现无事发生的时候,我才想明白md编辑器粘贴图片的原理。
写博客怎么能不上传图片呢(虽然理论上确实可行),我不能接受。相关的资料网上也基本查不到,所以我自己手搓了一套为md编辑器增加粘贴图片功能的方法。
分析实现步骤
通过了解md语法,我明白了图片其实只是通过导入图片的路径地址,在网页中渲染为<img src='图片地址'>的效果,具体语法如下:

所以我们可以分析出,在编辑器中粘贴并在md中渲染出图片的步骤可以拆分为:
- 粘贴:在编辑器中进行Ctrl+V行为,传递剪切板中的图片
- 存放图片:将剪切板的图片存放在某个地方,并得知它的具体地址(网络地址或本地地址)
- 撰写md语法:获取图片的地址,转化为md语法并添加在编辑器中
- 成功渲染
明白了步骤,我们就一一进行实现即可。
注:本文章基于Angular+NestJS进行功能实现,若技术栈不同可参考思路。
md编辑器和ngx-md渲染的核心代码及字段参考:
具体实现
粘贴
这里很简单,使用textare的paste事件即可获取剪切板中的内容,只需要注意判断是否是图片、以及剪切板是否有多张图片即可即可。
粘贴相关的具体代码为:
onPaste(event: ClipboardEvent) {
const items = event.clipboardData?.items;
if (items) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
// 检查是否为图片类型
if (item.type.startsWith('image/')) {
const file = item.getAsFile();
if (file) {
// 确保传入的是正确的文件,调用图片上传接口
this.uploadImage(file);
}
}
}
}
}
存放图片
存放图片的方法有两种,一种是存在本地(例如项目的assets文件夹),另一种是将图片通过后端接口公开,获取网络地址。因为我已经有一个在服务器部署的后端接口了,所以我使选择了后者。。
那么我现在需要做的就是:将图片上传、获取图片的地址。
在这里我踩了一个坑,因为我不太明白QQ截图的原理,所以如果使用常规的“本地文件上传”方式去上传,就会报错,而后我采用了最靠谱的base64上传方式,对于所有的图片都需要进行base64转码
在前端中撰写base64转码并调用图片上传接口:
uploadImage(file: File) {
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result as string;
// 构造 Base64 上传数据
const payload = { file: base64 };
// 调用上传服务
this.BlogService.uploadImg2(payload).subscribe({
next: (response: any) => {
if (response.code === 200 && response.data?.watermarkedUrl) {
const imageUrl = `${API.BASE_URL}${response.data.watermarkedUrl}`;
this.insertMarkdownImage(imageUrl);
} else {
console.error('Image upload failed:', response.msg);
}
},
error: (err) => {
console.error('Upload error:', err);
},
});
};
reader.onerror = (error) => {
console.error('Base64 conversion error:', error);
};
reader.readAsDataURL(file);
}
接下来需要在后端编写一个图片上传接口,并且支持base64:
@Post('blog')
async uploadImageBlog(@Body('file') base64Image: string) {
if (!base64Image || !base64Image.startsWith('data:image')) {
throw new Error('Invalid image format');
}
// 提取 Base64 数据部分
const matches = base64Image.match(/^data:(image\/\w+);base64,(.+)$/);
if (!matches) {
throw new Error('Invalid base64 data');
}
const base64Data = matches[2]; // Base64 数据
const fileBuffer = Buffer.from(base64Data, 'base64'); // 转换为 Buffer
// 获取文件扩展名和文件名
const mimeType = matches[1]; // 例如 "image/png"
const extension = mimeType.split('/')[1]; // 提取文件扩展名 (png/jpg)
const filename = `${Date.now()}.${extension}`;
const originalFilePath = path.join(this.uploadDir, filename);
// 返回图片访问路径
return {
message: 'Image uploaded successfully',
originalUrl: `/uploads/blog/${filename}`,
};
}
撰写md语法
在图片上传成功后,获取到图片的完整地址,需要转化md语法并添加在编辑器中:
insertMarkdownImage(imageUrl: string) {
// 将图片地址插入为 Markdown 格式
const markdownImageSyntax = ``;
this.data.content += `\n${markdownImageSyntax}`;
}
因为我不需要图片描述和图片标题,所以这里只存放了地址。
水印功能
在我的网站,我的博客享有版权,那肯定图片也得有模有样的搞点儿水印。
并且水印功能因为需要对图片进行处理,如果全都交给前端会增加客户端压力,所以尽可能的在后端进行。
先在nestjs中安装sharp插件
npm install sharp
然后在上传接口中新增水印功能
// 保存原图
await fs.writeFile(originalFilePath, fileBuffer);
// 生成带水印的文件路径
const watermarkedFilePath = path.join(
this.uploadDir,
`watermarked-${filename}`,
);
const svgWatermark = `
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="100">
<text x="290" y="90" font-size="24" font-family="Noto Sans SC" fill="white"
stroke="black" stroke-width="0.5" text-anchor="end">@FlowersInk</text>
</svg>`
// 添加水印
await sharp(fileBuffer)
.composite([
{
input: Buffer.from(svgWatermark),
gravity: 'southeast', // 确保水印位于右下角
},
])
.toFile(watermarkedFilePath);
`;
返回图片路径则返回原图和水印图,保证原图和水印图都可以进行使用
// 返回图片访问路径
return {
message: 'Image uploaded successfully',
originalUrl: `/uploads/blog/${filename}`,
watermarkedUrl: `/uploads/blog/watermarked-${filename}`,
};
注意sharp插件在导入的时候需要这样导入,否则无法使用:
import * as sharp from 'sharp';
成功渲染
来看看成功渲染的效果:
效果还是很不错的!