You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

GO+NextJS项目:Quill编辑器富文本与图片存储至PostgreSQL的困惑

处理Quill富文本图片的完整方案(NextJS + Go + PostgreSQL)

核心流程实现

后端Go处理逻辑

Quill提交的富文本里的base64图片不能直接存数据库,得先提取、上传到存储服务,再替换成URL存入。核心分为三步:

  1. 提取base64图片:用正则匹配富文本中的<img>标签里的base64数据
  2. 上传到存储服务:解码base64为二进制数据,上传到对象存储(比如S3、OSS),拿到可访问的URL
  3. 替换并存储:把原富文本中的base64地址替换成新URL,同时把图片URL存入post_imageurl字段(多图场景建议用PostgreSQL的text[]数组类型)

Go代码示例:

import (
    "regexp"
    "encoding/base64"
    "strings"
    "time"
    "fmt"
    "os"
)

// 提取富文本中的base64图片,返回匹配结果(格式:[图片格式, base64内容, 完整img标签, 额外属性])
func extractBase64Images(content string) ([][]string, error) {
    imgRegex := regexp.MustCompile(`<img src="data:image/([a-zA-Z0-9]+);base64,([^"]+)"([^>]*)>`)
    return imgRegex.FindAllStringSubmatch(content, -1), nil
}

// 上传图片到存储服务(这里以本地存储为例,换成S3/OSS只需要修改此函数)
func uploadImage(data []byte, ext string) (string, error) {
    // 生成唯一文件名
    fileName := fmt.Sprintf("posts/%d.%s", time.Now().UnixNano(), ext)
    // 保存到本地静态目录(实际项目建议用对象存储)
    savePath := "./public/" + fileName
    err := os.WriteFile(savePath, data, 0644)
    if err != nil {
        return "", err
    }
    // 返回可访问的URL(生产环境用CDN或存储服务的公网地址)
    return fmt.Sprintf("http://your-domain/%s", fileName), nil
}

// 处理提交的富文本内容
func processRichText(content string) (string, []string, error) {
    matches, err := extractBase64Images(content)
    if err != nil {
        return "", nil, err
    }

    var imageURLs []string
    for _, match := range matches {
        imgExt := match[1]
        base64Data := match[2]
        imgTag := match[0]
        extraAttrs := match[3]

        // 解码base64
        decodedData, err := base64.StdEncoding.DecodeString(base64Data)
        if err != nil {
            return "", nil, err
        }

        // 上传图片
        imgURL, err := uploadImage(decodedData, imgExt)
        if err != nil {
            return "", nil, err
        }
        imageURLs = append(imageURLs, imgURL)

        // 替换原img标签的src为新URL
        newImgTag := fmt.Sprintf(`<img src="%s"%s>`, imgURL, extraAttrs)
        content = strings.ReplaceAll(content, imgTag, newImgTag)
    }

    return content, imageURLs, nil
}

在处理POST请求时,调用processRichText函数,把处理后的content和imageURLs(单图场景取第一个元素)存入PostgreSQL即可。

前端NextJS处理

  • 提交阶段:直接获取Quill编辑器的HTML内容(editor.root.innerHTML或通过Quill API转HTML),作为请求的content字段发给后端,无需前端处理base64。
  • 渲染阶段:从数据库拿到处理后的content,用dangerouslySetInnerHTML渲染:
export default function Post({ post }) {
  return (
    <div className="post-content">
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </div>
  );
}

行业标准做法

  1. 禁止存储base64到数据库:base64会让数据体积膨胀30%以上,严重影响数据库查询性能和备份效率,必须将图片分离到对象存储。
  2. 抽象存储层:业务代码不要直接依赖具体存储服务(比如S3),封装统一的存储接口,方便后续切换存储服务商。
  3. 图片优化:上传时自动压缩(Go可使用imaging库)、生成多尺寸缩略图,前端根据设备加载对应尺寸,提升页面加载速度。
  4. CDN加速:所有图片URL通过CDN访问,配置合理的缓存策略(比如缓存1年),减少源站压力和用户加载等待时间。
  5. 异步清理资源:删除文章时,不要同步删除图片(避免请求阻塞),用消息队列触发异步删除任务,保证数据一致性。

适配存储服务变更的方案

  1. 定义统一存储接口:
type ImageStorage interface {
  Upload(data []byte, ext string) (string, error)
  Delete(url string) error
}

然后分别实现S3Storage、OSSStorage、LocalStorage等结构体,满足该接口。业务代码仅依赖ImageStorage接口,不关心具体实现细节。

  1. 配置驱动实例化:通过环境变量指定存储类型,启动时自动初始化对应的存储实例:
func NewImageStorage() ImageStorage {
  storageType := os.Getenv("IMAGE_STORAGE")
  switch storageType {
  case "s3":
    return NewS3Storage(os.Getenv("S3_BUCKET"), os.Getenv("S3_REGION"))
  case "oss":
    return NewOSSStorage(os.Getenv("OSS_BUCKET"), os.Getenv("OSS_ENDPOINT"))
  case "local":
    return NewLocalStorage("./public/posts")
  default:
    panic("unsupported image storage type")
  }
}
  1. 数据库存相对路径:不要在数据库里存完整的URL前缀,只存相对路径(比如posts/123.jpg),前端访问时通过统一前缀拼接(比如https://cdn.example.com/+相对路径)。这样变更存储服务时,仅需修改前缀配置,无需修改数据库数据。

  2. 批量迁移脚本:如果数据库中已有旧URL,可编写Go脚本批量替换,或通过后端中间层(比如Nginx反向代理)处理旧URL到新存储的跳转。

内容的提问来源于stack exchange,提问作者Dolarious

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 09:14:51