vue 上传图片到腾讯云对象存储
需求:上传图片到腾讯云对象存储,返回图片链接地址。
大概流程:
腾讯云对象存储(Cloud Object Storage,COS)是腾讯云提供的一种存储海量文件的分布式存储服务,用户可通过网络随时存储和查看数据。
COS 使所有用户都能使用具备高扩展性、低成本、可靠和安全的数据存储服务。
COS 通过控制台、API、SDK 和工具等多样化方式简单、快速地接入,实现了海量数据存储和管理。通过 COS 可以进行任意格式文件的上传、下载和管理。
官网文档:https://cloud.tencent.com/document/product/436
使用腾讯云对象存储准备工作:
首先是需要开通腾讯云对象存储服务,然后,登录对象存储控制台,创建存储视频对应的存储桶。配置规则,桶的读写权限,允许的域名等等的,都要进行设置,具体官方文档都有说明,这里就不赘述了。
准备工作结束后,接下来,就是正式使用了:
1、 引入cos-js-sdk-v5
安装 JavaScript SDK
:
npm install cos-js-sdk-v5
安装成功后会有如下信息:
2、封装upload.js
新建文件upload.js
,封装上传文件方法,判断上传文件为视频还是图片,视频上传到视频对应的桶,图片上传到图片对应的桶,视频如果大于50M,则分片上传。
步骤:
1)getCOSSecretKey
方法调用获取临时秘钥data;
由于固定密钥放在前端会有安全风险,所以使用临时密钥的方式:前端首先请求服务端,服务端使用固定密钥调用 STS 服务申请临时密钥,然后返回临时密钥到前端使用。
2)初始化;
3)判断上传文件的类型;
4)判断文件大小 是否需要分片上传。
// https://cloud.tencent.com/document/product/436/11459
import COS from 'cos-js-sdk-v5'
import { Message } from 'element-ui'
import { getCOSSecretKey } from '@/api/common/index'
const config = {
videoBucket: 'xxx',
imageBucket: 'xxx',
Region: 'ap-beijing'
}
// 上传到腾讯云cos
export function uploadObject (file, callback) {
/* 1.获取临时秘钥data
2.初始化
3.判断上传文件的类型
4.判断文件大小 是否需要分片上传*/
const type = file.type.substring(0, 5)
let fileName = file.name || ""
const origin_file_name = fileName.split(".").slice(0, fileName.split(".").length - 1).join('.') // 获取文件名称
// console.log('origin_file_name', origin_file_name)
// 获取当前时间戳 与文件类型拼接 为cos.putObject里的参数Key
const upload_file_name = new Date().getTime() + '.' + fileName.split(".")[fileName.split(".").length - 1] // 文件上传名称定义为当前时间戳
// console.log('upload_file_name', upload_file_name)
// 获取密钥
getCOSSecretKey({ bucket: type === 'video' ? config.videoBucket : config.imageBucket })
.then(response => { // 后台接口返回 密钥相关信息
const data = response.result
var credentials = data && data.credentials
if (!data || !credentials) return console.error('credentials invalid')
// 初始化
var cos = new COS({
getAuthorization: (options, callback) => {
callback({
TmpSecretId: credentials.tmpSecretId,
TmpSecretKey: credentials.tmpSecretKey,
XCosSecurityToken: credentials.sessionToken,
StartTime: data.startTime,
ExpiredTime: data.expiredTime,
expiration: data.expiration,
requestId: data.requestId,
})
},
})
// 获取上传文件大小
let size = file.size
let key = '/art/' + upload_file_name
if (type === 'video') {
// console.log(size / (1024 * 2024))
if (size / (1024 * 1024) < 50) {
console.log('视频普通上传')
cos.putObject(
{
Bucket: config.videoBucket, // 存储桶名称
Region: config.Region, // 存储桶所在地域,必须字段
Key: key, // 视频名称
StorageClass: 'STANDARD',
Body: file, // 上传文件对象
onHashProgress: (progressData) => {
// console.log('校验中', JSON.stringify(progressData))
},
onProgress: (progressData) => {
const percent = parseInt(progressData.percent * 10000) / 100;
const speed = parseInt((progressData.speed / 1024 / 1024) * 100) / 100;
console.log('进度:' + percent + '%; 速度:' + speed + 'Mb/s;');
},
},
(err, data) => {
console.log('data', data)
if (err) {
console.log('err', err)
Message({ message: '视频文件上传失败,请重新上传', type: 'error' })
} else {
let fileUrl = 'https://' + config.videoBucket + '.file.myqcloud.com' + key
callback(fileUrl, origin_file_name) // 返回视频链接地址和视频的原始名称
}
}
)
} else {
console.log('视频分块上传')
// 上传分块
cos.sliceUploadFile(
{
Bucket: config.videoBucket, // 存储桶名称
Region: config.Region, // 存储桶所在地域,必须字段
Key: key /* 必须 */,
Body: file,
onTaskReady: (taskId) => {
/* 非必须 */
// console.log(taskId)
},
onHashProgress: (progressData) => {
/* 非必须 */
// console.log(JSON.stringify(progressData))
},
onProgress: function (progressData) {
const percent = parseInt(progressData.percent * 10000) / 100;
const speed = parseInt((progressData.speed / 1024 / 1024) * 100) / 100;
console.log('进度:' + percent + '%; 速度:' + speed + 'Mb/s;');
},
},
(err, data) => {
if (err) {
console.log(err)
Message({ message: '文件上传失败,请重新上传', type: 'error' })
} else {
let fileUrl = 'https://' + config.videoBucket + '.file.myqcloud.com' + key
callback(fileUrl, origin_file_name) // 返回视频链接地址和视频的原始名称
}
}
)
}
} else if (type === 'image') {
cos.putObject(
{
Bucket: config.imageBucket, // 存储桶名称
Region: config.Region, // 存储桶所在地域,必须字段
Key: key, // 图片名称
StorageClass: 'STANDARD',
Body: file, // 上传文件对象
onHashProgress: (progressData) => {
console.log('校验中', JSON.stringify(progressData))
},
onProgress: (progressData) => {
const percent = parseInt(progressData.percent * 10000) / 100;
const speed = parseInt((progressData.speed / 1024 / 1024) * 100) / 100;
console.log('进度:' + percent + '%; 速度:' + speed + 'Mb/s;');
},
},
(err, data) => {
if (data && data.statusCode == 200) {
let fileUrl = 'https://' + config.imageBucket + '.file.myqcloud.com' + key
callback(fileUrl) // 返回图片链接地址和视频的原始名称
} else {
Message({ message: '图片文件上传失败,请重新上传', type: 'error' })
}
}
)
}
})
}
export default {
uploadObject
}
3、封装图片上传组件、调用上传方法
新建image-upload.vue
封装图片上传组件,调用上传方法:
<template>
<div class="single-image-upload-container">
<div class="single-image-upload" :class="{'limit-num': fileList.length>=limit, 'mini': size === 'small'}">
<el-upload ref="upload" :file-list="fileList" list-type="picture-card" action="#" :http-request="uploadToCos" :on-preview="handlePictureCardPreview" :on-remove="handleRemove" :on-exceed="exceedTips" :on-success="handeSuccess" :before-upload="beforeImageUpload" :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 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)">
<i class="el-icon-delete"></i>
</span>
<span v-if="size === 'small'" style="display:block;marginLeft:0" class="el-upload-list__item-delete" @click="onChangeHandle(file)">
<i class="el-icon-edit"></i>
</span>
<span v-else class="el-upload-list__item-delete" @click="onChangeHandle(file)">
<i class="el-icon-edit"></i>
</span>
</span>
</div>
</el-upload>
</div>
<el-dialog :visible.sync="dialogVisibleShow">
<img width="100%" :src="dialogImageUrl" alt="">
</el-dialog>
</div>
</template>
<script>
import { uploadObject } from '@/utils/upload'
export default {
// 上传图片到腾讯云对象存储
name: 'ImgUpload',
data () {
return {
imgWidth: 0,
imgHeight: 0,
picIndex: -1,
dialogImageUrl: '',
dialogVisibleShow: false,
fileList: []
}
},
props: {
value: {
type: String,
default: ''
},
tips: {
type: String,
default: ''
},
size: {
type: String,
default: 'medium' // small
},
limit: {
type: Number,
default: 1
},
limitSize: {
type: Number,
default: 10
},
// 是否校验图片尺寸,默认不校验
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 // 图片限制高度上限(有时需要校验上传图片高度在一个范围内)
},
busiType: {
type: Number,
default: 2
},
index: {
type: Number,
default: -1 // 当前图片index,限制可以上传多张时,针对某一张进行操作,需要知道当前的index
},
limitType: {
type: String,
default: '' // gif,webp/gif/webp(限制上传格式)
}
},
components: {},
created () {
if (this.value) {
this.fileList = [{ url: this.value }]
}
},
watch: {
value (val) {
if (val) {
this.fileList = [{ url: val }]
} else {
this.fileList = []
}
}
},
methods: {
onChangeHandle (file, fileList) {
this.fileList = [file]
// console.log('onChangeHandle file, fileList', file, fileList);
this.$refs.upload.$refs['upload-inner'].handleClick()
},
handleRemove (file, fileList) {
// console.log('handleRemove file, fileList', file, fileList);
this.$emit('input', '')
this.fileList = fileList
},
handlePictureCardPreview (file) {
this.dialogImageUrl = file.url
this.dialogVisibleShow = true
},
beforeImageUpload (file) {
// console.log('beforeImageUpload', file);
const imgType = file.type
const isLtSize = file.size / 1024 / 1024 < this.limitSize
const TYPE_NOGIFWEBP = ['image/png', 'image/jpeg', 'image/jpg']
const TYPE_NOGIF = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp']
const TYPE_NOWEBP = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif']
const TYPE_ALL = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp']
let isType = true
if (this.limitType && this.limitType.indexOf('gif') !== -1 && this.limitType.indexOf('webp') !== -1) {
if (TYPE_NOGIFWEBP.indexOf(imgType) === -1) {
isType = false
this.$message.error('仅支持上传 jpg、png、jpeg、gif、webp 格式的图片!')
}
} else if (this.limitType && this.limitType.indexOf('gif') === -1 && this.limitType.indexOf('webp') === -1) {
if (TYPE_NOGIFWEBP.indexOf(imgType) === -1) {
isType = false
this.$message.error('仅支持上传 jpg、png、jpeg 格式的图片!')
}
} else if (this.limitType && this.limitType.indexOf('webp') !== -1) {
if (TYPE_NOGIF.indexOf(imgType) === -1) {
isType = false
this.$message.error('仅支持上传 jpg、png、jpeg、webp 格式的图片!')
}
} else if (this.limitType && this.limitType.indexOf('gif') !== -1) {
if (TYPE_NOWEBP.indexOf(imgType) === -1) {
isType = false
this.$message.error('仅支持上传 jpg、png、jpeg、gif 格式的图片!')
}
} else {
if (TYPE_ALL.indexOf(imgType) === -1) {
isType = false
this.$message.error('仅支持上传 jpg、png、jpeg、webp、gif 格式的图片!')
}
}
if (!isLtSize) {
this.$message.error(`上传图片大小不能超过${this.limitSize}MB!`)
}
if (this.isCheckPicSize === true) {
const width = this.checkWidth
const height = this.checkHeight
const topWidth = this.topLimitWidth
const topHeight = this.topLimitHeight
const that = this
const isSize = new Promise((resolve, reject) => {
// console.log('Promise')
// window对象,将blob或file读取成一个url
const _URL = window.URL || window.webkitURL
const img = new Image()
img.onload = () => { // image对象的onload事件,当图片加载完成后执行的函数
// console.log('img.onload')
that.imgWidth = img.width
that.imgHeight = img.height
if (width && height) { // 校验图片的宽度和高度
let valid = false
if (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 = false
if (topWidth) {
// 校验图片宽度范围
valid = (width <= img.width) && (img.width <= topWidth)
} else {
// 校验图片宽度固定值
valid = img.width === width
}
valid ? resolve() : reject(new Error('error'))
} if (height) { // 只校验图片的高度
let valid = false
if (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读取成一个url
const _URL = window.URL || window.webkitURL
const img = new Image()
img.onload = () => { // image对象的onload事件,当图片加载完成后执行的函数
this.imgWidth = img.width
this.imgHeight = img.height
}
img.src = _URL.createObjectURL(file)
return isType && isLtSize
}
},
// 上传文件
uploadToCos () {
// console.log('uploadToCos uploadFile', this.fileList)
let files = uploadObject(this.fileList[0].raw, (url) => {
this.$emit('input', url)
})
},
exceedTips (file, fileList) {
this.$message(`最多上传${fileList.length}个文件!`)
},
handeSuccess (res, file, fileList) {
console.log('handeSuccess')
}
}
}
</script>
<style lang='less'>
@small-size: 80px;
.single-image-upload&&.limit-num {
.el-upload--picture-card {
display: none !important;
}
}
.single-image-upload&&.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="img" :size="'small'" :tips="'建议图片宽度为350,比例为4:3 / 3:4 / 1:1 JPG、JPGE、PNG 小于5M,仅限上传一张'" :limit="1" :limitSize="5"></img-upload>
</div>
</template>
<script>
import ImgUpload from '@/components/image-upload'
export default {
name: 'add',
components: {
ImgUpload
},
data () {
return {
img: ''
}
},
props: {
},
watch: {
},
created () {
},
mounted () {
},
methods: {
}
}
</script>
上传过程中打印进度信息: