【业务分享】使用 vue-quill-editor 自定义图片上传 + Gin 上传文件至 aliyun OSS

1,567 阅读4分钟

2022 年 2 月 14 日更新

  • 掘金的 markdown 解析规则可能改了,两年前的文章样式全崩了,现已修复

前言

近期接到的众多需求里,有一个需求挺有意思,为了完成它,头发又掉了不少...

废话不多说,先来介绍一下需求吧:要求用户在管理后台能编写微信公众号格式的文章,编写过程中要求保持格式,插入的图片这种,在第二行插入的绝不能在第三行显示...

  • 图片来源于网络,如有侵权请私信删除

具体可以参考 135 微信文章编辑

分析


讲真,这种需求之前还真没搞过,查了很多富文本编辑器之后,发现都比较难搞,而且很多富文本编辑器,demo 都出问题...

后来发现了 vue-quill-editor 非常不错,功能多且强大,但是 vue-quill-editor 上传图片是将图片转为 base64 编码,所以就用到了他的 自定义上传 的功能,我们可以利用他的自定义上传功能调取 element 的上传组件,完成我们的需求

接下来就是图片存在哪了,直接扔到 OSS 上,然后 数据库URL,让前端加载去吧...

正常上传图片流程:

  1. 用户发送上传 Policy 请求到应用服务器
  2. 应用服务器返回上传 Policy 和签名给用户
  3. 用户直接上传数据到 OSS

But !!

由于公司没做内网穿透,设置上传回调就成了问题...

具体参考:help.aliyun.com/document_de…

其实倒也是有办法解决,但当时时间很紧迫,为了抓紧时间上线项目,只能变通一下咯,于是上面提到的流程变成了:

  1. 客户端上传图片至应用服务器
  2. 应用服务器直传 OSS
  3. 返回链接给客户端

没错,变成了服务端上传,这么搞其实有一些 风险 的,但考虑到时间紧,上线后后台又只有一个人管理,上传频率也不是很高,影响不是很大...

最终决定,使用 vue-quill-editor 的自定义上传调用 element uploadelement upload 上传图片至服务端,服务端使用 Goweb 框架 Gin 转存至 OSS 上,成功上传后将图片地址返回给 element uploadelement upload 再将地址回传给 vue-quill-editor

开发

确定好方案后,开始施工啦!

  • vue-quill-editor 安装与自定义上传

  • npm install vue-quill-editor --save

import VueQuillEditor from 'vue-quill-editor'
Vue.use(VueQuillEditor);

import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'

前端逻辑

  • html
 <quill-editor
    v-model="content"
    :options="editorOption"
    ref="QuillEditor">
</quill-editor>
<el-upload
      :data=""
      :multiple=""
      :show-file-list=""
      :on-success=""
      class=""
      drag
      :http-request="uploadImg"
    >
    </el-upload>
  • el-upload 参数可以根据场景自己填写,关键点在于 http-request, 选中图片后会直接调用此事件

  • vue-quill-editor 各种配置

   const toolbarOptions = [
     ['bold', 'italic', 'underline', 'strike'],        // toggled buttons
     ['blockquote', 'code-block'],
   
     [{'header': 1}, {'header': 2}],               // custom button values
     [{'list': 'ordered'}, {'list': 'bullet'}],
     [{'script': 'sub'}, {'script': 'super'}],      // superscript/subscript
     [{'indent': '-1'}, {'indent': '+1'}],          // outdent/indent
     [{'direction': 'rtl'}],                         // text direction
   
     [{'size': ['small', false, 'large', 'huge']}],  // custom dropdown
     [{'header': [1, 2, 3, 4, 5, 6, false]}],
   
     [{'color': []}, {'background': []}],          // dropdown with defaults from theme
     [{'font': []}],
     [{'align': []}],
     ['link', 'image', 'video'],
     ['clean']                                         // remove formatting button
   ]
   
   export default {
       data () {
           return {
               content: '',
               editorOption: {                
                   modules: {
                       toolbar: {
                           container: toolbarOptions,  // 工具栏
                           handlers: {
                               'image': function (value) {
                                   if (value) {
                                       // 这里最重要
                                       // 在编辑器中点击图片 icon 会触发此事件
                                       // 需自己写方法触发 <el-upload>.click()
                                       // 使用 <el-upload> 完成上传,在回调到
                                   } else {
                                       this.quill.format('image', false);
                                   }
                               }
                           }
                       }
                   }
               }
           }
       }
   } 

  • 由于我们使用 http-request 自定义了上传事件,上传完成后就可以将图片插入到 vue-quill-editor
uploadImg(res) {
   axios.post('服务端地址', res.file, {
       headers: {"content-type": "multipart/form-data"}
     }).then(res => {
     let quill = this.$refs.QuillEditor.quill
     // 如果上传成功, 获取光标所在位置, 插入图片,res 为服务器返回的图片链接地址
     if (res) {
        let length = quill.getSelection().index;
        quill.insertEmbed(length, 'image', res)
        // 调整光标到最后
        quill.setSelection(length + 1)
      } else {
	     // fail...
         }
  })
}

后端逻辑

我们使用 Gin 来获取前端上传的图片并转存至 OSS 上

func UploadToOss(c *gin.Context) (url string, err error)  {
	file, err := c.FormFile("file")
	if err != nil {
		return "", err
	}

	fileHandle, err := file.Open()   //打开上传文件
	if err != nil {
		return "", err
	}
	defer fileHandle.Close()
	fileByte, err := ioutil.ReadAll(fileHandle)  //获取上传文件字节流
	if err != nil {
		return "", err
	}

	url, err = uploadOss(file.Filename, fileByte)
	return url, err
 }

 func uploadOss(fileName string, fileByte []byte) (url string, err error)  {
    // oss 配置
	endpoint := ""
	accessKeyId := ""
	accessKeySecret := ""
	bucketName := ""
	domain := ""

	// 创建 OSS Client 实例
	client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
	if err != nil {
		return url,err
	}

	// 获取存储空间
	bucket, err := client.Bucket(bucketName)
	if err != nil {
		return url,err
	}

	// 随机数防止文件重复
	rand.Seed(time.Now().Unix())
	randNum := rand.Int()
	fileName = cast.ToString(randNum) + fileName

	//上传阿里云路径
	yunFileTmpPath := filepath.Join("OSS 存储路径", "可选参数")  + "/" + fileName 

	// 上传Byte数组
	err = bucket.PutObject(yunFileTmpPath, bytes.NewReader([]byte(fileByte)))
	if err != nil {
		return url,err
	}
    return domain + "/" + yunFileTmpPath ,nil
   }

总结

第一次写文章,上述如有不清晰,不正确等信息还请多指教。

代码只写了必要逻辑,大家可以结合自己的实际场景处理结果。