GO+NextJS项目:Quill编辑器富文本与图片存储至PostgreSQL的困惑
处理Quill富文本图片的完整方案(NextJS + Go + PostgreSQL)
核心流程实现
后端Go处理逻辑
Quill提交的富文本里的base64图片不能直接存数据库,得先提取、上传到存储服务,再替换成URL存入。核心分为三步:
- 提取base64图片:用正则匹配富文本中的
<img>标签里的base64数据 - 上传到存储服务:解码base64为二进制数据,上传到对象存储(比如S3、OSS),拿到可访问的URL
- 替换并存储:把原富文本中的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> ); }
行业标准做法
- 禁止存储base64到数据库:base64会让数据体积膨胀30%以上,严重影响数据库查询性能和备份效率,必须将图片分离到对象存储。
- 抽象存储层:业务代码不要直接依赖具体存储服务(比如S3),封装统一的存储接口,方便后续切换存储服务商。
- 图片优化:上传时自动压缩(Go可使用
imaging库)、生成多尺寸缩略图,前端根据设备加载对应尺寸,提升页面加载速度。 - CDN加速:所有图片URL通过CDN访问,配置合理的缓存策略(比如缓存1年),减少源站压力和用户加载等待时间。
- 异步清理资源:删除文章时,不要同步删除图片(避免请求阻塞),用消息队列触发异步删除任务,保证数据一致性。
适配存储服务变更的方案
- 定义统一存储接口:
type ImageStorage interface { Upload(data []byte, ext string) (string, error) Delete(url string) error }
然后分别实现S3Storage、OSSStorage、LocalStorage等结构体,满足该接口。业务代码仅依赖ImageStorage接口,不关心具体实现细节。
- 配置驱动实例化:通过环境变量指定存储类型,启动时自动初始化对应的存储实例:
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") } }
数据库存相对路径:不要在数据库里存完整的URL前缀,只存相对路径(比如
posts/123.jpg),前端访问时通过统一前缀拼接(比如https://cdn.example.com/+相对路径)。这样变更存储服务时,仅需修改前缀配置,无需修改数据库数据。批量迁移脚本:如果数据库中已有旧URL,可编写Go脚本批量替换,或通过后端中间层(比如Nginx反向代理)处理旧URL到新存储的跳转。
内容的提问来源于stack exchange,提问作者Dolarious
相关产品推荐
相关产品推荐

