vue 上传图片到腾讯云对象存储
- 1、 引入cos-js-sdk-v5
- 2、封装`uploadcos.js`
- 3、封装图片上传组件、调用上传方法
- 4、页面使用组件
之前总结过 vue
封装图片上传组件到腾讯云对象存储,后来又加了一些功能,在图片过大时进行压缩,压缩完成之后,再上传到腾讯云对象存储;并且,对上传方法进行了优化,所以重新记录一下。
1、 引入cos-js-sdk-v5
安装 JavaScript SDK
:
npm install cos-js-sdk-v5
安装成功后会有如下信息:
2、封装uploadcos.js
新建文件uploadcos.js
,封装上传文件方法。
/*** 本文件为腾讯云对象存储相关工具类方法注意:桶的访问权限需要设置指定域名(不然会出现跨域问题),现在设置允许访问的域名是:http://localhost:8080 https://xxx.com.cn/所以本地调试时,需要用http://localhost:8080,不可用其他端口。跨域配置:桶:指定域名 + 指定子账号能上传;外部不能访问,统一通过cdn访问;CDN:设置为无跨域限制---- COD自主诊断工具:https://cloud.tencent.com/login?s_url=https%3A%2F%2Fconsole.cloud.tencent.com%2Fcos5%2Fdiagnose -----*/// https://cloud.tencent.com/document/product/436/11459
import COS from 'cos-js-sdk-v5'
import { Message } from 'element-ui'
import { getCOSSecretKey } from '@/api/index'// 存储桶所在地域
const BUCKET_REGION = 'ap-beijing'
// 使用分片上传阈值10(M)
const SLICE_SIZE = 10const BUCKET_TYPE_CONFIG = {video: 'video-b-123456',image: 'image-b-123456',file: 'file-b-123456'
}const BUCKET_DOMAIN = {video: 'https://abcd-video.xxx.com.cn',image: 'https://abcd-image.xxx.com.cn'
}const FOLDER_PATH_NAME = {// 内容图片ART: {prod: '/art/',test: '/test/art/'},// 日常活动图片ACT: {prod: '/act/',test: '/test/act/'},// 产品图片WARE: {prod: '/ware/',test: '/test/ware/'},// 广告&宣传图片ADV: {prod: '/adv/',test: '/test/adv/'}
}/*** options @param {Object}* sliceSize:使用切片上传阈值 默认10(M)* bucketType:桶类型 video,image,file 三种类型* busiType:业务类型* needLaoding:是否需要loading遮罩层* bucketEnv:桶的环境 测试、生产* bucketName:桶的名称* bucketDomain:桶的域名 用来拼接key* bucketPrefix:自定义桶地址前缀片段* credentials:后台返回凭证信息* keyBackData:后台返回密钥信息 为credentials父级*/
class Cos {constructor(options) {this.bucketEnv =window.msBaseUrl === 'https://xxx.com.cn/' ? 'prod' : 'test'this.bucketType = options?.bucketType || BUCKET_TYPE_CONFIG.filethis.bucketName = BUCKET_TYPE_CONFIG[this.bucketType]this.bucketDomain = BUCKET_DOMAIN[this.bucketType]this.sliceSize = options?.sliceSize || SLICE_SIZEthis.busiType = options?.busiType || 'ART'this.bucketPrefix = FOLDER_PATH_NAME[this.busiType][this.bucketEnv]this.credentials = nullthis.keyBackData = null}/*** 获取密钥* @returns Object*/async getKey () {try {const res = await getCOSSecretKey({bucket: this.bucketName})if (res?.result?.credentials &&Object.keys(res?.result?.credentials).length) {this.keyBackData = res.resultthis.credentials = res.result.credentialsreturn this.credentials}return null} catch (error) {return null}}/*** 生成上传资源名称 6位随机数+uid+文件名*/generateKey (file) {const timeStamp = file.uid ? file.uid : new Date().getTime() + ''const suffix = file.name.split('.')[file.name.split('.').length - 1]return `${this.randomString()}_${timeStamp}.${suffix}`}/*** 获取随机数* @param {*} len* @returns*/randomString (len = 6) {const chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz0123456789'const maxPos = chars.lengthlet res = ''for (let i = 0; i < len; i++) {res += chars.charAt(Math.floor(Math.random() * maxPos))}return res}/*** 创建COS对象实例* @returns Object*/async getCosInstance () {const getKey = await this.getKey()if (getKey) {const { tmpSecretId, tmpSecretKey, sessionToken } = this.credentialsconst { startTime, expiredTime } = this.keyBackDataconst params = {TmpSecretId: tmpSecretId,TmpSecretKey: tmpSecretKey,SecurityToken: sessionToken,StartTime: startTime,ExpiredTime: expiredTime}const _cos = new COS({getAuthorization: function (options, callback) {callback(params)}})return _cos}return null}/*** 单个文件上传到腾讯云cos* @param {*} file* @returns*/async uploadHandle (file) {const cos = await this.getCosInstance()if (cos) {const KEY = `${this.bucketPrefix}${this.generateKey(file)}`console.log('KEY', KEY)return new Promise((resolve, reject) => {// if (this.needLoading) {// var loadingInstance = Loading.service({ fullscreen: true })// }cos.uploadFile({Bucket: this.bucketName /* 填入您自己的存储桶,必须字段 */,Region: BUCKET_REGION /* 存储桶所在地域,例如ap-beijing,必须字段 */,Key: KEY /* 存储在桶里的对象键(例如1.jpg,a/b/test.txt),必须字段 */,Body: file /* 必须,上传文件对象,可以是input[type="file"]标签选择本地文件后得到的file对象 */,SliceSize:1024 *1024 *this.sliceSize /* 触发分块上传的阈值,超过5MB使用分块上传,非必须 */,onTaskReady: function (taskId) {/* 非必须 */// console.log(taskId)},onProgress: function (progressData) {/* 非必须 */// console.log(JSON.stringify(progressData))const percent = parseInt(progressData.percent * 10000) / 100const speed =parseInt((progressData.speed / 1024 / 1024) * 100) / 100console.log('进度:' + percent + '%; 速度:' + speed + 'Mb/s;')},onFileFinish: function (err, data, options) {/* 非必须 */console.log(options.Key + '上传' + (err ? '失败' : '完成'))}},(err, data) => {// loadingInstance && loadingInstance.close()if (err) {Message.error(err)reject(err)}const url = `${this.bucketDomain}${KEY}`if (this.bucketType === 'video') {const fileName = file.name || ''const name = fileName.split('.').slice(0, fileName.split('.').length - 1).join('.') // 获取文件名称resolve({ url, name })} else {resolve(url)}})})}}/*** 媒体信息接口*/async getMediaInfoHandle (key) {const cos = await this.getCosInstance()if (cos) {return new Promise((resolve, reject) => {cos.request({Bucket: this.bucketName /* 填入您自己的存储桶,必须字段 */,Region: BUCKET_REGION /* 存储桶所在地域,例如ap-beijing,必须字段 */,Method: 'GET',Key: key /* 存储桶内的媒体文件,必须字段 */,Query: {'ci-process': 'videoinfo' /** 固定值,必须 */}},function (err, data) {if (err) {Message.error(err)reject(err)}resolve(data)})})}}
}export default Cos
3、封装图片上传组件、调用上传方法
新建image-upload.vue
封装图片上传组件,调用上传方法:
<template><div class="common-image-upload-cos-container"><div class="image-upload-cos-content" :class="{'limit-num': fileList.length>=limit, 'mini': size === 'small'}"><el-upload ref="upload" :file-list="fileList" list-type="picture-card" action="#" :http-request="uploadImageHandle" v-loading="uploadLoading" :on-preview="handlePictureCardPreview" :on-remove="handleRemove" :on-exceed="exceedTips" :on-success="handeSuccess" :before-upload="beforeAvatarUpload" :on-change="onChangeHandle"><i class="el-icon-plus"></i><p class="el-upload__tip" slot="tip" v-if="tips">{{tips}}</p><div slot="file" slot-scope="{file}" class="img-con"><img crossorigin class="el-upload-list__item-thumbnail" :src="file.url" alt=""><span class="el-upload-list__item-actions"><span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)"><i class="el-icon-zoom-in"></i></span><span class="el-upload-list__item-delete" @click="handleRemove(file)" v-if="!disabled"><i class="el-icon-delete"></i></span><span v-if="size === 'small' && !disabled" style="display:block;marginLeft:0px" @click="onChangeHandle(file)"><i class="el-icon-edit"></i></span><span v-if="size !== 'small' && !disabled" @click="onChangeHandle(file)"><i class="el-icon-edit"></i></span></span></div></el-upload></div><div class="img-preview-dialo"><el-dialog :visible.sync="dialogVisibleShow" :append-to-body="append_to_body" :modal-append-to-body="modal_append_to_body"><img style="width:100%" :src="dialogImageUrl" crossorigin alt></el-dialog></div></div>
</template><script>
import Cos from '@/utils/uploadcos'
import ImageCompressor from '@/assets/js/image-compressor.min'
export default {// 上传图片到腾讯云对象存储name: 'ImageUpload',componentName: 'ImageUpload',data () {return {uploadLoading: false,imgWidth: 0,imgHeight: 0,picIndex: -1,dialogImageUrl: '',dialogVisibleShow: false,fileList: [],vmodelType: '',cos: new Cos({busiType: this.busiType,bucketType: this.bucketType})}},props: {// 接收 String, Array类型,默认为 String 类型value: {type: [String, Array],default: ''},tips: {type: String,default: ''},size: {type: String,default: 'medium' // small},limit: {// 限制上传图片张数type: Number,default: 1},limitSize: {// 限制上传图片大小type: Number,default: 10},valueType: {type: String,default: 'String' // Object},bucketType: {type: String,default: 'image'},// 是否校验图片尺寸,默认不校验isCheckPicSize: {type: Boolean,default: false},checkWidth: {type: Number,default: 0 // 图片限制宽度},checkHeight: {type: Number,default: 0 // 图片限制高度},topLimitWidth: {type: Number,default: 0 // 图片限制宽度上限(有时需要校验上传图片宽度在一个范围内)},topLimitHeight: {type: Number,default: 0 // 图片限制高度上限(有时需要校验上传图片高度在一个范围内)},index: {type: Number,default: -1 // 当前图片index,限制可以上传多张时,针对某一张进行操作,需要知道当前的index},limitType: {type: String,default: '' // (限制上传图片格式)传入格式:png,jpg,gif png,jpg,webp png,jpg,gif,webp},busiType: {type: String,default: 'ART'},// 禁用开关disabled: {type: Boolean,default: false},isGzip: {// 是否压缩图片,默认不压缩(false);传入 true 时,图片大小大于80KB且不是gif格式时进行压缩type: Boolean,default: false},append_to_body: {type: Boolean,default: false},modal_append_to_body: {type: Boolean,default: true}},components: {},created () {if (this.valueType === 'Object') {this.vmodelType = 'array'}if (this.value) {this.modifyValue()}},watch: {value: {deep: true,handler: function (val, oldVal) {if (val) {this.modifyValue()} else {this.fileList = []}}}},methods: {findItem (uid) {this.fileList.forEach((ele, i) => {if (uid === ele.uid) {this.picIndex = i}})},onChangeHandle (file, fileList) {// console.log('onChangeHandle file, fileList', file, fileList)this.findItem(file.uid)this.$refs.upload.$refs['upload-inner'].handleClick()},handleRemove (file) {// console.log('handleRemove file', file)this.findItem(file.uid)this.fileList.splice(this.picIndex, 1)this.exportImg()},exportImg () {if (this.fileList.length !== 0) {if (this.imgWidth && this.imgHeight) {if (this.valueType === 'Object') {const imgs = this.fileList.map(item => {return {url: item.url,name: item.fileName ? item.fileName : item.name}})this.$emit('input', imgs)this.$emit('imgChange', this.index)} else {if (this.vmodelType === 'array') {const imgs = this.fileList.map(item => {if (item.url) {return item.url}})this.$emit('input', imgs)this.$emit('imgChange', this.index)} else {const resUrl = this.fileList[0].urlthis.$emit('input', resUrl)this.$emit('imgChange', this.index)}}} else {this.$message.error('当前未获取到图片宽高数据,请重新上传图片!')}} else {this.$emit('input', '')this.$emit('imgChange', this.index)}this.picIndex = -1},uploadImageHandle (file) {if (this.vmodelType === 'string') {if (this.picIndex !== -1 && this.fileList.length) {// 如果是单图编辑替换状态,手动删除被替换的文件this.fileList.splice(this.picIndex, 1)}}// console.log('uploadImageHandle file', file)// return// 不需要压缩、图片小于80KB或是gif图,不压缩直接上传;大于80KB,压缩图片后上传const uploadFile = file.fileif ((!this.isGzip) || (uploadFile.size / 1024 < 80) || (uploadFile.type == 'image/gif')) {this.uploadToCos(uploadFile)} else {let file = uploadFileif (!file) {return}var options = {file: file,quality: 0.6,mimeType: 'image/jpeg',maxWidth: 6000,maxHeight: 6000,// width: 1000, // 指定压缩图片宽度// height: 1000, // 指定压缩图片高度minWidth: 10,minHeight: 10,convertSize: Infinity,loose: true,redressOrientation: true,// 压缩前回调beforeCompress: (result) => {console.log('压缩之前图片尺寸大小: ', result.size / 1024)// console.log('mime 类型: ', result.type);},// 压缩成功回调success: (result) => {console.log(result);console.log('压缩之后图片尺寸大小: ', result.size / 1024)console.log('实际压缩率: ', ((file.size - result.size) / file.size * 100).toFixed(2) + '%');this.uploadToCos(result)},// 发生错误error: (msg) => {console.error(msg);this.$message.error(msg)}};/* eslint-disable no-new */new ImageCompressor(options)}},// 上传文件uploadToCos (file) {// console.log('uploadToCos uploadFile', file)// returnthis.uploadLoading = truethis.cos.uploadHandle(file).then((url) => {if (url) {if (this.imgWidth && this.imgHeight) {const resUrl = url + '?width=' + this.imgWidth + '&height=' + this.imgHeightconst obj = { url: resUrl, name: file.name }if (this.picIndex < 0) {this.fileList.push(obj)} else {this.fileList[this.picIndex] = obj}this.exportImg()} else {this.$message.error('当前未获取到图片宽高数据,请重新上传图片!')}} else {this.fileList.splice(this.picIndex, 1)}this.uploadLoading = false})},modifyValue () {if (this.valueType === 'Object') {this.fileList = this.value.map(item => ({url: item.url,name: item.name}))} else {// 判断是否是Stringconst str = this.valueconst res = ((str instanceof String) || (typeof str).toLowerCase() === 'string')if (res === true) {this.vmodelType = 'string'} else {this.vmodelType = 'array'}if (this.vmodelType === 'array') {this.fileList = this.value.map(item => ({ url: item }))} else {this.fileList = [{ url: this.value }]}}},beforeAvatarUpload (file) {const imgType = file.typeconst isLtSize = file.size / 1024 / 1024 < this.limitSizeconst TYPE_ALL = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp']let isType = true// console.log('this.limitType', this.limitType)// console.log('imgType', imgType)if (this.limitType) {const limitTypeArr = this.limitType.split(',')const limutTypeFlagArr = []const IMG_STATUS = {jpg: 'image/jpeg',jpeg: 'image/jpeg',png: 'image/png',gif: 'image/gif',webp: 'image/webp'}limitTypeArr.forEach(item => {if (IMG_STATUS[item]) limutTypeFlagArr.push(IMG_STATUS[item])})if (limutTypeFlagArr.indexOf(imgType) === -1) {isType = falsethis.$message.error(`仅支持上传 ${this.limitType} 格式的图片!`)}} else {// 默认情况,未传入校验类型格式,则默认可以接受全部格式if (TYPE_ALL.indexOf(imgType) === -1) {isType = falsethis.$message.error('仅支持上传 jpg、png、jpeg、webp、gif 格式的图片!')}}if (!isLtSize) {this.$message.error(`上传图片大小不能超过${this.limitSize}MB!`)}if (this.isCheckPicSize === true) {const width = this.checkWidthconst height = this.checkHeightconst topWidth = this.topLimitWidthconst topHeight = this.topLimitHeightconst that = thisconst isSize = new Promise((resolve, reject) => {// console.log('Promise')// window对象,将blob或file读取成一个urlconst _URL = window.URL || window.webkitURLconst img = new Image()img.onload = () => { // image对象的onload事件,当图片加载完成后执行的函数// console.log('img.onload')that.imgWidth = img.widththat.imgHeight = img.heightif (width && height) { // 校验图片的宽度和高度let valid = falseif (topWidth && topHeight) {// 校验图片宽度和高度范围valid = ((width <= img.width) && (img.width <= topWidth)) && ((height <= img.height) && (img.height <= topHeight))} else if (topHeight) {// 校验图片高度范围valid = img.width === width && ((height <= img.height) && (img.height <= topHeight))} else if (topWidth) {// 校验图片宽度范围valid = ((width <= img.width) && (img.width <= topWidth)) && img.height === height} else {// 校验图片宽度、高度固定值valid = img.width === width && height === img.height}valid ? resolve() : reject(new Error('error'))} else if (width) { // 只校验图片的宽度let valid = falseif (topWidth) {// 校验图片宽度范围valid = (width <= img.width) && (img.width <= topWidth)} else {// 校验图片宽度固定值valid = img.width === width}valid ? resolve() : reject(new Error('error'))} if (height) { // 只校验图片的高度let valid = falseif (topHeight) {// 校验图片高度范围valid = (height <= img.height) && (img.height <= topHeight)} else {// 校验图片高度固定值valid = img.height === height}valid ? resolve() : reject(new Error('error'))}}img.src = _URL.createObjectURL(file)}).then(() => {// console.log('then')return file}, () => {// console.log('reject')let text = ''if (width && height) {if (topWidth && topHeight) {text = `图片尺寸限制为:宽度${width}~${topWidth}px,高度${height}~${topHeight}px!`} else if (topHeight) {text = `图片尺寸限制为:宽度${width}px,高度${height}~${topHeight}px!`} else if (topWidth) {text = `图片尺寸限制为:宽度${width}~${topWidth}px,高度${height}px!`} else {text = `图片尺寸限制为:宽度${width}px,高度${height}px!`}} else if (width) {if (topWidth) {text = `图片尺寸限制为:宽度${width}~${topWidth}px!`} else {text = `图片尺寸限制为:宽度${width}px!`}} else if (height) {if (topHeight) {text = `图片尺寸限制为:高度${height}~${topHeight}px!`} else {text = `图片尺寸限制为:高度${height}px!`}}this.$message.error(text)return Promise.reject(new Error('error'))})return isType && isLtSize && isSize} else {// window对象,将blob或file读取成一个urlconst _URL = window.URL || window.webkitURLconst img = new Image()img.onload = () => { // image对象的onload事件,当图片加载完成后执行的函数this.imgWidth = img.widththis.imgHeight = img.height}img.src = _URL.createObjectURL(file)return isType && isLtSize}},handlePictureCardPreview (file) {this.dialogImageUrl = file.urlthis.dialogVisibleShow = true},exceedTips (file, fileList) {this.$message(`最多上传${fileList.length}个文件!`)},handeSuccess (res, file, fileList) {console.log('handeSuccess')}}
}
</script><style lang='less'>
@small-size: 80px;
.common-image-upload-cos-container {.el-dialog {background: transparent;-webkit-box-shadow: none;box-shadow: none;margin-top: 0 !important;width: auto;position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);-webkit-transform: translate(-50%, -50%);.el-dialog__header {display: none;}.el-dialog__body {text-align: center;img {width: auto;max-width: 1000px;}}}
}
.image-upload-cos-content&&.limit-num {.el-upload--picture-card {display: none !important;}
}
.image-upload-cos-content&&.mini {.el-upload {border: 1px dashed #d9d9d9;border-radius: 6px;cursor: pointer;position: relative;overflow: hidden;}.el-upload-list__item {width: @small-size;height: @small-size;text-align: center;/*去除upload组件过渡效果*/transition: none !important;}.el-upload--picture-card {width: @small-size;height: @small-size;line-height: @small-size;text-align: center;}
}
.el-upload-list__item&&.is-success {.img-con {width: 100%;height: 100%;}
}
</style>
action
:上传地址,必填,直接上传后端接口时填入接口链接;自定义上传方法使用 http-request
,所以,填任意字符串即可,没有实际意义。
http-request
:覆盖默认上传行为,这里自定义上传行为。
4、页面使用组件
<template><div> <img-upload v-model="bgImg" :isGzip="true" :size="'small'" :tips="'建议图片宽度为350,JPG、JPGE、PNG 小于5M,仅限上传一张'" :limit="1" :limitSize="5"></img-upload><img-upload v-model="mainImg" tips="宽度750px,高度范围:170px~1334px" :isCheckPicSize="true" :checkWidth="750" :checkHeight="170" :topLimitHeight="1334" :limit="1" size="small"></img-upload> <img-upload v-model="imgArr" :tips="'多图数组'" :limit="6" :limitSize="1"></img-upload><img-upload v-model="imgList" :tips="'多图数组'" :limit="6" :limitSize="1" valueType="Object"></img-upload></div>
</template>
<script>
import ImgUpload from '@/components/image-upload'export default {name: 'demo',components: {ImgUpload},data () {return {bgImg: '',mainImg: '',imgArr: [ 'https://xxx', 'https://xxx']imgList: [{name: '1.jpg',url: 'https://xxx'}] } },props: {},watch: {},created () { },mounted () {},methods: { }
}
</script>
上传大小小于 80M 的图片,过程中打印进度信息如下:
上传大小大于 80M 的图片,过程中打印进度信息如下: