为ngx-markdown编辑器增加粘贴图片功能

180 阅读4分钟

前言

在开发自己的网站以前,我一直以为Typora或者掘金的bytemd编辑器中粘贴图片的功能,是真的将图片放进了编辑器里。

由于angular兼容性最好的markdown(以下均简称为md)编辑器我能找到的只有ngx-md,我在自己的网站后台实现类似于掘金编辑器的方法是左边一个textare,右边一个ngx-md渲染。

当我傻傻的对着一个textare粘贴图片却发现无事发生的时候,我才想明白md编辑器粘贴图片的原理。

写博客怎么能不上传图片呢(虽然理论上确实可行),我不能接受。相关的资料网上也基本查不到,所以我自己手搓了一套为md编辑器增加粘贴图片功能的方法。

分析实现步骤

通过了解md语法,我明白了图片其实只是通过导入图片的路径地址,在网页中渲染为<img src='图片地址'>的效果,具体语法如下:

![图片alt](图片链接 "图片title")

所以我们可以分析出,在编辑器中粘贴并在md中渲染出图片的步骤可以拆分为:

  1. 粘贴:在编辑器中进行Ctrl+V行为,传递剪切板中的图片
  2. 存放图片:将剪切板的图片存放在某个地方,并得知它的具体地址(网络地址或本地地址)
  3. 撰写md语法:获取图片的地址,转化为md语法并添加在编辑器中
  4. 成功渲染

明白了步骤,我们就一一进行实现即可。

注:本文章基于Angular+NestJS进行功能实现,若技术栈不同可参考思路。

md编辑器和ngx-md渲染的核心代码及字段参考:

1732244477579_d.png

具体实现

粘贴

这里很简单,使用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 = `![](${imageUrl})`;
    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';

成功渲染

来看看成功渲染的效果:

image.png

效果还是很不错的!