Vue中添加Quill富文本自定义图片上传方法

一. 为什么要做自定义上传,搞这么麻烦干什么?

我司要求所有的资源上传都不通过服务器处理,直接上传到阿里云对象存储。

二. 先说组件自带的方法

1. quill本身支持

2. 拦截自定义处理

  1. 源码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    editorOption: {
    modules: {
    ImageExtend: { // 如果不作设置,即{} 则依然开启复制粘贴功能且以base64插入
    name: 'fileFile', // 图片参数名
    action:'www.img:8080', // 服务器地址, 如果action为空,则采用base64插入图片
    size: 5, // 可选参数 图片大小,单位为M,1M = 1024kb
    // response 为一个函数用来获取服务器返回的具体图片地址
    // 例如服务器返回{code: 200; data:{ url: 'baidu.com'}} 则 return res.data.url
    response: (res) => {// 图片上传成功或错误 回调方法 成功后将图片地址return出去
    // 这里将后台返回的地址return出去
    return res.filePath;
    },
    // 可选参数 设置请求头部xhr.setRequestHeader('Content-Type','multipart/form-data')
    headers: (xhr) => { },
    sizeError: () => {}, // 图片超过大小的回调
    start: () => {}, // 可选参数 自定义开始上传触发事件
    end: () => {}, // 可选参数 自定义上传结束触发的事件,无论成功或者失败
    error: () => {}, // 可选参数 上传失败触发的事件
    success: () => {}, // 可选参数 上传成功触发的事件
    change: (xhr, formData) => {
    // xhr.setRequestHeader('myHeader','myValue')
    // formData.append('token', 'myToken')
    } // 可选参数 每次选择图片触发,也可用来设置头部,但比headers多了一个参数,可设置formData
    },
    toolbar: {
    container: container,
    handlers: {
    'image': function (value) {
    QuillWatch.emit(this.quill.id)
    }
    }
    },
    }
    }

你会发现整个ImageExtend 属性都是图片上传相关了以及各种事件的回调方法 ,大家根据注释配置相关的信息就好了
toolbar 这个属性此处不必配置
到这里我们的使用后台的接口上传就完事了。

五. 使用Element-UI + Ali-oss直接上传到阿里云的对象存储

1. 首先安装Ali-oss

1
npm install ali-oss --save

2. 创建oss上传的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
'use strict'
/**
* 时间日期格式化
* @param format
* @returns {*}
*/
const dateFormat = function(dateObj, format) {
const date = {
'M+': dateObj.getMonth() + 1,
'd+': dateObj.getDate(),
'h+': dateObj.getHours(),
'm+': dateObj.getMinutes(),
's+': dateObj.getSeconds(),
'q+': Math.floor((dateObj.getMonth() + 3) / 3),
'S+': dateObj.getMilliseconds()
}
if (/(y+)/i.test(format)) {
format = format.replace(RegExp.$1, (dateObj.getFullYear() + '').substr(4 - RegExp.$1.length))
}
for (const k in date) {
if (new RegExp('(' + k + ')').test(format)) {
format = format.replace(RegExp.$1, RegExp.$1.length === 1
? date[k] : ('00' + date[k]).substr(('' + date[k]).length))
}
}
return format
}


var OSS = require('ali-oss')
function getUUID() {
return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}

export default {

/**
* 创建随机字符串
* @param num
* @returns {string}
*/
randomString(num) {
const chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
let res = ''
for (let i = 0; i < num; i++) {
var id = Math.ceil(Math.random() * 35)
res += chars[id]
}
return res
},

/**
* 创建oss客户端对象
* @returns {*}
*/
createOssClient() {
return new Promise((resolve, reject) => {
const client = new OSS({
region : '你自己的',
accessKeyId : '你自己的',
accessKeySecret : '你自己的',
bucket : '你自己的'
})
resolve(client)
})
},
/**
* 文件上传
* @param option 参考csdn: https://blog.csdn.net/qq_27626333/article/details/81463139
*/
// 这里的我司的默认路径是test/ 有需要自行修改
ossUploadFile(option,folder='test/') {
const file = option.file
const self = this
return new Promise((resolve, reject) => {
const date = dateFormat(new Date(), 'yyyyMMdd') // 当前时间
const dateTime = dateFormat(new Date(), 'yyyyMMddhhmmss') // 当前时间
const randomStr = self.randomString(4)// 4位随机字符串
const extensionName = file.name.substr(file.name.indexOf('.')) // 文件扩展名
const fileName = folder + date + '/' + getUUID() + extensionName // 文件名字(相对于根目录的路径 + 文件名)
// 执行上传
self.createOssClient().then(client => {
// 异步上传,返回数据
resolve({
fileName: file.name,
fileUrl: fileName
})
// 上传处理
// 分片上传文件
client.multipartUpload(fileName, file, {
progress: function(p) {
const e = {}
e.percent = Math.floor(p * 100)
// console.log('Progress: ' + p)
option.onProgress(e)
}
}).then((val) => {
console.info(val)
if (val.res.statusCode === 200) {
option.onSuccess(val)
return val
} else {
option.onError('上传失败')
}
}, err => {
option.onError('上传失败')
reject(err)
})
})
})
}
}

3.注意文件中的这里需要你自己oss对象存储配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 创建oss客户端对象
* @returns {*}
*/
createOssClient() {
return new Promise((resolve, reject) => {
const client = new OSS({
region : '你自己的',
accessKeyId : '你自己的',
accessKeySecret : '你自己的',
bucket : '你自己的'
})
resolve(client)
})
},

4. 引入Element-UI

1
npm install element-ui --save
在main.js中注入
1
2
3
4
// 引入element-ui 全局引入
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);

5. 在quill组件中添加上传组件并 稍微修改了下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
  <div class="upload" style='display:none'>
<el-upload
class="avatar-uploader"
action
ref="upload"
name="img"
:show-file-list="false"
:auto-upload="true"
:on-error="handleImageErrorQuill"
:on-exceed="beyondFileQuill"
:on-success="handleImageSuccessQuill"
:http-request="fnUploadRequestQuill"
:before-upload="beforeAvatarUploadQuill"
:limit="3"
multiple
>
<i class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</div>

// 引入新的包
<script>
const ossURL = '你的上传路径'
import oss from "@/config/oss";
export default {···}
</script>
// 在methods中添加方法
methods: {
// * 上传失败的回调
handleImageErrorQuill() {
this.$message({ message: "上传失败", type: "error" });
},
// * 文件超出个数限制时的钩子
beyondFileQuill(files, fileList) {
this.$message({ message: "只能上传" + this.limit, type: "error" });
},
// * 上传成功
handleImageSuccessQuill(response, file, fileList) {
if (response) {
// 向富文本插入图片链接
let quill = this.$refs.myQuillEditor.quill;
let length = quill.getSelection().index;
// 插入图片 res.info为服务器返回的图片地址
// 获取光标 设置属性 图片链接
quill.insertEmbed(length, `image`, `${ossURL + response.name}`);
quill.setSelection(length + 1); // 调整光标到最后
}
},
// * 自定义上传覆盖默认上传
async fnUploadRequestQuill(option) {
oss.ossUploadFile(option,'image/');
},
// * 上传前对图片进行验证
beforeAvatarUploadQuill(file) {
const isJPG = file.type === "image/jpeg" || file.type === "image/png" || file.type === "image/gif";
const isLtM = file.size / 1024 / 1024 < 20;
if (!isJPG) {
this.$message.error("上传头像图片只能是 JPG,PNG,GIF 格式!");
}
if (!isLtM) {
this.$message.error("上传头像图片大小不能超过 20MB!");
}
return isJPG && isLtM;
}
},

6. 再次修改editorOption(富文本配置参数) 这次我们只需要修改toolbar属性 ,别忘了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
toolbar: {  // 如果不上传图片到服务器,此处不必配置
container: container, // container为工具栏,此次引入了全部工具栏 也可自行配置
handlers: {
'image': function (value) { // 劫持原来的图片点击按钮事件 自定义图片上传
// 触发input框选择图片文件
if (value) {
// 这里触发的是 element-ui上传组件
console.log(11111111111111)
document.querySelector('.avatar-uploader input').click()
} else {
this.quill.format('image', false);
}
// QuillWatch.emit(this.quill.id)
}
}
},

7. 封装完成,但是有个小bug

成功

六. 多个富文本,添加图片报错

一开始我也不知道会有这样的问题,直到我司另一前端对付产品的需求一个页面需要二个富文本时。

  1. 分析错误原因

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    handleImageSuccessQuill(response, file, fileList) {
    if (response) {
    this.imageUrl = ossURL + response.name;
    // 向富文本插入图片链接
    let quill = this.$refs.myQuillEditor.quill;
    let length = quill.getSelection().index;
    // 插入图片 res.info为服务器返回的图片地址
    // 获取光标 设置属性 图片链接
    quill.insertEmbed(length, `image`, `${ossURL + response.name}`);
    quill.setSelection(length + 1); // 调整光标到最后
    }
    },
    1. 可以可看到报错信息 index of ‘null’
    2. index 是我们通过quill.getSelection()获取到的 那也就是 quill.getSelection()里面没有index属性
    3. 我们将this.$refs.myQuillEditor.quill 打印出来看下
      错误
      在选择第一 个上传组件并上传 没有任何问题
      当选择第二个 上传组件并上传 找不到是null??? 这是什么鬼???
    4. 解析:获取到的富文本实例 quill 获取不到光标的位置。
  2. 如何解决 且要保证已经完成代码不会出现问题 也不用去修改原来的代码,降低返工率

    1. 我们添加一个新的自定义属性props 用于获取$refs

      1
      2
      3
      4
      5
      6
      7
      8
      9
      props: {
      value:{
      type:String,
      },
      quillEditorName:{
      type:String,
      default:'myQuillEditor',// 没有传值 默认就是myQuillEditor 有就是传过来的值
      }
      },
    2. 我们将页面部分修改下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
         <template>
      <div class="quill-wrap">
      <quill-editor v-model="content" :ref="quillEditorName" :options="editorOption" ></quill-editor>

      <el-upload
      class="avatar-uploader"
      action
      ref="upload"
      name="img"
      :show-file-list="false"
      :auto-upload="true"
      :on-error="handleImageErrorQuill"
      :on-exceed="beyondFileQuill"
      :on-success="handleImageSuccessQuill"
      :http-request="fnUploadRequestQuill"
      :before-upload="beforeAvatarUploadQuill"
      :limit="limit"
      multiple
      >
      <i class="el-icon-plus avatar-uploader-icon" :id="quillEditorName+'Img'"></i>
      </el-upload>
      </div>
      </template>

      toolbar: {
      // 如果不上传图片到服务器,此处不必配置
      container: container, // container为工具栏,此次引入了全部工具栏,也可自行配置
      handlers: {
      image: (value)=> {
      // 劫持原来的图片点击按钮事件 自定义图片上传
      if (value) {
      // 触发input框选择图片文件
      document.querySelector('#' + this.quillEditorName + 'Img').click();
      } else {
      this.quill.format("image", false);
      }
      }
      }
      }


      handleImageSuccessQuill(response, file, fileList) {
      // 上传成功
      if (response) {
      this.imageUrl = ossURL + response.name;
      // 向富文本插入图片链接
      let quill = this.$refs[this.quillEditorName].quill;
      let length = quill.getSelection().index;
      // 插入图片 res.info为服务器返回的图片地址
      // 获取光标 设置属性 图片链接
      quill.insertEmbed(length, `image`, `${ossURL + response.name}`);
      quill.setSelection(length + 1); // 调整光标到最后
      }
      },
    3. 修改好了 我们在试下
      成功了

    4. 最后贴上全部的源码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      <template>
      <div>
      <quill-editor
      v-model="context"
      :ref="quillEditorName"
      :options="editorOption"
      @input="$emit('update:value', $event)"
      >
      </quill-editor>

      <div class="upload" style='display:none'>
      <el-upload
      class="avatar-uploader"
      action
      ref="upload"
      name="img"
      :show-file-list="false"
      :auto-upload="true"
      :on-error="handleImageErrorQuill"
      :on-exceed="beyondFileQuill"
      :on-success="handleImageSuccessQuill"
      :http-request="fnUploadRequestQuill"
      :before-upload="beforeAvatarUploadQuill"
      :limit="limit"
      multiple
      >
      <i class="el-icon-plus avatar-uploader-icon" :id="quillEditorName+'Img'"></i>
      </el-upload>
      </div>
      </div>
      </template>

      <script>
      const ossURL = 'http://你自己的.com/'
      import {quillEditor, Quill} from 'vue-quill-editor';
      import {container, ImageExtend, QuillWatch,} from 'quill-image-extend-module';
      import {quillRedefine} from 'vue-quill-editor-upload';
      import ImageResize from 'quill-image-resize-module';
      Quill.register('modules/ImageResize', ImageResize )
      Quill.register('modules/ImageExtend', ImageExtend);
      import oss from "@/config/oss";
      export default {
      mounted () {
      this.context = this.value;
      },
      props:{
      value:{
      type:String,
      },
      quillEditorName:{
      type:String,
      default:'myQuillEditor',// 没有传值 默认就是myQuillEditor 有就是传过来的值
      }
      },
      data() {
      return {
      limit:3,
      context:'',
      // 富文本框参数设置
      editorOption: {
      modules: {
      ImageExtend: { // 如果不作设置,即{} 则依然开启复制粘贴功能且以base64插入
      name: '', // 图片参数名
      size: 5, // 可选参数 图片大小,单位为M,1M = 1024kb
      action:"", // 服务器地址, 如果action为空,则采用base64插入图片
      // response 为一个函数用来获取服务器返回的具体图片地址
      // 例如服务器返回{code: 200; data:{ url: 'baidu.com'}} 则 return res.data.url
      response: (res) => {// 图片上传成功或错误 回调方法 成功后将图片地址return出去
      return res.filePath;
      },
      headers: (xhr) => { // 可选参数 设置请求头部
      // xhr.setRequestHeader('Content-Type','multipart/form-data')
      },
      sizeError: () => {}, // 图片超过大小的回调
      start: () => {}, // 可选参数 自定义开始上传触发事件
      end: () => {}, // 可选参数 自定义上传结束触发的事件,无论成功或者失败
      error: () => {}, // 可选参数 上传失败触发的事件
      success: () => {
      }, // 可选参数 上传成功触发的事件
      change: (xhr, formData) => {
      // xhr.setRequestHeader('myHeader','myValue')
      // formData.append('token', 'myToken')
      } // 可选参数 每次选择图片触发,也可用来设置头部,但比headers多了一个参数,可设置formData
      },
      toolbar: { // 如果不上传图片到服务器,此处不必配置
      container: container, // container为工具栏,此次引入了全部工具栏,也可自行配置
      handlers: {
      'image': function (value) { // 劫持原来的图片点击按钮事件 自定义图片上传
      // 触发input框选择图片文件
      if (value) {
      document.querySelector('#' + this.quillEditorName + 'Img').click();
      } else {
      this.quill.format('image', false);
      }
      }.bind(this)
      // 或者你可以使用箭头函数
      //'image': (value) => { // 劫持原来的图片点击按钮事件 自定义图片上传
      // // 触发input框选择图片文件
      // if (value) {
      // document.querySelector('#' + this.quillEditorName + 'Img').click();
      // } else {
      /// this.quill.format('image', false);
      // }
      //}
      }
      },
      ImageResize: { //调整上传过后图片大小配置。
      displayStyles: {
      backgroundColor: 'black',
      border: 'none',
      color: 'white'
      },
      modules: [ 'Resize', 'DisplaySize','Toolbar' ]
      },
      },
      }
      }
      },
      methods: {
      // * 上传失败的回调
      handleImageErrorQuill() {
      this.$message({ message: "上传失败", type: "error" });
      },
      // * 文件超出个数限制时的钩子
      beyondFileQuill(files, fileList) {
      this.$message({ message: "只能上传" + this.limit, type: "error" });
      },
      // * 上传成功
      handleImageSuccessQuill(response, file, fileList) {
      if (response) {
      this.imageUrl = ossURL + response.name;
      // 向富文本插入图片链接
      let quill = this.$refs[this.quillEditorName].quill;
      let length = quill.getSelection().index;
      // 插入图片 res.info为服务器返回的图片地址
      // 获取光标 设置属性 图片链接
      quill.insertEmbed(length, `image`, `${ossURL + response.name}`);
      quill.setSelection(length + 1); // 调整光标到最后
      }
      },
      // * 自定义上传覆盖默认上传
      async fnUploadRequestQuill(option) {
      oss.ossUploadFile(option,'image/');
      },
      // * 上传前对图片进行验证
      beforeAvatarUploadQuill(file) {
      const isJPG = file.type === "image/jpeg" || file.type === "image/png" || file.type === "image/gif";
      const isLtM = file.size / 1024 / 1024 < 20;
      if (!isJPG) {
      this.$message.error("上传头像图片只能是 JPG,PNG,GIF 格式!");
      }
      if (!isLtM) {
      this.$message.error("上传头像图片大小不能超过 20MB!");
      }
      return isJPG && isLtM;
      }
      },
      watch: {
      // 页面
      value(newVAlue){
      this.context = newVAlue;
      },
      context(newValue){
      console.log(newValue);
      }
      },
      filters: {},
      computed: {},
      components: {
      quillEditor,
      quillRedefine,
      Quill
      }
      }
      </script>