背景
每个COS的用户都会用到上传服务。Web端常见的上传方法是用户在浏览器或App端上传文件到应用服务器,应用服务器再把文件上传到COS。具体流程如下图所示。
和数据直传到COS相比,以上方法有三个缺点:
- 上传慢:用户数据需先上传到应用服务器,之后再上传到COS。网络传输时间比直传到COS一倍。如果用户数据不通过应用服务器中转,而是直传到COS,速度将大大提升。而且COS采用BGP带宽,能保证各地各运营商之间的传输速度。
- 扩展性差:如果后续用户多了,应用服务器会成为瓶颈。
- 费用高:需要准备多台应用服务器。由于COS上传流量是免费的,如果数据直传到COS,不通过应用服务器,那么将能省下几台应用服务器。
后端说明
关于服务端签名,参考链接:
https://cloud.tencent.com/document/product/436/14048
前端实现
关于前端直传,参考链接:
https://cloud.tencent.com/document/product/436/9067
相关依赖
"cos-js-sdk-v5": "^1.2.8",
定义工具类
/**
* 生成随机文件名称
* 规则八位随机字符,加下划线连接时间戳
*/
export const getFileNameUUID = () => {
function rx() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
}
return `${+new Date()}_${rx()}${rx()}`
}
创建实例
<template>
<div>
<!--照片墙样式-->
<el-upload
v-if="uploadStyle===0"
action=""
list-type="picture-card"
:file-list="fileList"
:auto-upload="true"
:on-exceed="handleExceed"
:before-upload="handleBeforeUpload"
:http-request="handleUploadFile"
:on-preview="handleFileCardPreview"
:on-remove="handleRemove">
<i class="el-icon-plus"></i>
</el-upload>
<!--缩略图样式-->
<el-upload
v-if="uploadStyle===1"
action=""
list-type="picture"
:file-list="fileList"
:auto-upload="true"
:on-exceed="handleExceed"
:before-upload="handleBeforeUpload"
:http-request="handleUploadFile"
:on-preview="handleFileCardPreview"
:on-remove="handleRemove">
<el-button style="width: 100%" size="small" type="primary">点击上传</el-button>
</el-upload>
<!--拖拽文件样式 云-->
<el-upload
v-if="uploadStyle===2"
drag
multiple
action=""
:file-list="fileList"
:auto-upload="true"
:on-exceed="handleExceed"
:before-upload="handleBeforeUpload"
:http-request="handleUploadFile"
:on-preview="handleFileCardPreview"
:on-remove="handleRemove">
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
<span v-if="uploadInstructions" style="color: red">
<el-divider direction="vertical"></el-divider>
{{ uploadInstructions }}
</span>
</div>
</el-upload>
<!--进度条-->
<el-progress
v-show="showProgress"
:text-inside="true"
:stroke-width="15"
:percentage="progress"
status="success"
></el-progress>
<!--展示上传的文件-->
<el-dialog :title="dialogFileUrlTitle"
:visible.sync="dialogFileUrlVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
append-to-body>
<div v-if="dialogFileFormat==='image'">
<img width="100%" :src="dialogFileUrl" alt="">
</div>
<div v-else-if="dialogFileFormat==='video'">
<video
controls
controlslist="nodownload"
preload="auto"
style="width: 98%;text-align: center"
:src="dialogFileUrl">
</video>
</div>
<div v-else>
<el-alert
title="当前格式暂不支持预览!"
type="error"
center
show-icon>
</el-alert>
</div>
</el-dialog>
</div>
</template>
数据方法
import COS from 'cos-js-sdk-v5'
// 获取后端鉴权的接口,具体看自己业务实现。
import {cosCredential} from '@/api/common/common'
// 重新生成文件名
import {getSimpleUUID} from "@/utils/cms";
export default {
name: 'CosUpload',
components: {},
created() {
this.initialize()
},
data() {
return {
// 上传样式 0:卡片照片墙 1:缩略图 2:拖拽上传文件
uploadStyle: 0,
// COS
cosData: {},
// 文件列表
fileList: [],
// 进度条的显示
showProgress: false,
// 进度条数据
progress: 0,
// 文件表单
fileParams: {
// 上传的文件目录
folder: '/default/'
},
// 上传说明
uploadInstructions: "",
// 展示上传
dialogFileUrlVisible: false,
dialogFileUrlTitle: '',
dialogFileUrl: '',
dialogFileFormat: ''
}
},
model: {
event: 'complete',
prop: ''
},
// 组件插槽
props: {
// 上传位置
position: {
type: String,
required: true
},
// 使用样式
styleType: {
type: Number
},
// 展示图片
showFile: {
type: String
},
// 限制格式
formatList: {
type: Array
}
},
// 监听事件
watch: {
showFile: {
handler() {
this.onShowFile()
}
}
},
methods: {
/** 组件初始化 */
initialize() {
let position = this.position
if (position === '' || position == null) {
this.$message({
message: '组件初始化失败!缺少[position]',
type: 'warning'
})
return
}
this.onShowFile()
switch (position) {
case 'default':
this.fileParams.folder = '/default/'
break
case 'news':
this.fileParams.folder = '/news/'
break
case 'videos':
this.fileParams.folder = '/videos/'
break
case 'channel':
this.fileParams.folder = '/channel/'
break
default:
this.$message({
message: '组件初始化失败!未知[position]',
type: 'warning'
})
return
}
this.uploadStyle = this.styleType
if (this.formatList && this.formatList.length > 0) {
this.uploadInstructions = `仅支持后缀名为: ${JSON.stringify(this.formatList)} 的文件!`
}
},
/** showFile */
onShowFile() {
console.log(`log- showFileUrl:${this.showFile}`)
if (this.showFile) {
let url = this.showFile
let name = '点击预览文件'
this.fileList = [{name, url}]
} else {
this.fileList = []
}
},
/** 上传文件之前 */
handleBeforeUpload(file) {
console.log(`handleBeforeUpload formatList: ${JSON.stringify(this.formatList)};`)
return new Promise((resolve, reject) => {
// 格式校验格式
if (this.formatList && this.formatList.length > 0) {
let exist = false
let stringFormat = ""
let fileFormat = file.name.replace(/.+\./, "");
for (let format of this.formatList) {
if (format.toLowerCase() === fileFormat.toLowerCase()) {
exist = true
}
if (stringFormat === "") {
stringFormat = format
} else {
stringFormat = stringFormat + " 或 " + format
}
}
if (!exist) {
this.$message({
duration: 0,
showClose: true,
message: `请上传后缀名为:${stringFormat} 的文件!`,
type: 'error'
});
reject(false)
}
}
// 加载OSS配置参数
cosCredential().then(response => {
this.cosData = response.data
resolve(true)
}).catch(error => {
this.$message.error('加载上传配置失败!msg:' + error)
reject(false)
})
})
},
/** 文件超出个数限制 */
handleExceed(files, fileList) {
this.$message({
message: '停!不能再多了~',
type: 'warning'
})
},
/** 文件列表移除文件 */
handleRemove(file, fileList) {
this.$emit('complete', null)
this.$message({
message: '成功移除一个文件',
type: 'success'
})
},
/** 点击文件列表中已上传的文件 */
handleFileCardPreview(file) {
let fileFormat = this.judgeFileFormat(file.url);
switch (fileFormat) {
case 'image':
this.dialogFileUrlTitle = '图片预览'
break;
case "video":
this.dialogFileUrlTitle = '视频预览'
break;
default:
this.$message.error(`当前格式为${fileFormat},暂不支持预览!`)
return;
}
this.dialogFileFormat = fileFormat
this.dialogFileUrl = file.url
this.dialogFileUrlVisible = true
},
/** 根据URL判断文件格式 */
judgeFileFormat(fileUrl) {
// 获取最后一个.的位置
const index = fileUrl.lastIndexOf(".");
// 获取后缀
const suffix = fileUrl.substr(index + 1);
console.log(`当前文件后缀格式为 suffix: ${suffix}`);
// 获取类型结果
let result = '';
// 图片格式
const imgList = ['png', 'jpg', 'jpeg', 'bmp', 'gif'];
// 进行图片匹配
result = imgList.find(item => item === suffix);
if (result) {
return 'image';
}
// 匹配txt
const txtList = ['txt'];
result = txtList.find(item => item === suffix);
if (result) {
return 'txt';
}
// 匹配 excel
const excelList = ['xls', 'xlsx'];
result = excelList.find(item => item === suffix);
if (result) {
return 'excel';
}
// 匹配 word
const wordList = ['doc', 'docx'];
result = wordList.find(item => item === suffix);
if (result) {
return 'word';
}
// 匹配 pdf
const pdfList = ['pdf'];
result = pdfList.find(item => item === suffix);
if (result) {
return 'pdf';
}
// 匹配 ppt
const pptList = ['ppt', 'pptx'];
result = pptList.find(item => item === suffix);
if (result) {
return 'ppt';
}
// 匹配 视频
const videoList = ['mp4', 'm2v', 'mkv', 'rmvb', 'wmv', 'avi', 'flv', 'mov', 'm4v'];
result = videoList.find(item => item === suffix);
if (result) {
return 'video';
}
// 匹配 音频
const radioList = ['mp3', 'wav', 'wmv'];
result = radioList.find(item => item === suffix);
if (result) {
return 'radio';
}
// 其他 文件类型
return 'other';
},
/** 执行文件上传 */
handleUploadFile(file) {
let that = this
console.log(`log- ossData: ${JSON.stringify(that.cosData)}`)
// 获取COS实例
const cos = new COS({
// 必选参数
getAuthorization: (options, callback) => {
const obj = {
TmpSecretId: that.cosData.credentials.tmpSecretId,
TmpSecretKey: that.cosData.credentials.tmpSecretKey,
XCosSecurityToken: that.cosData.credentials.sessionToken,
// 时间戳,单位秒,如:1580000000
StartTime: that.cosData.startTime,
// 时间戳,单位秒,如:1580000900
ExpiredTime: that.cosData.expiredTime
}
callback(obj)
}
})
// 处理文件名称路径
let temporary = file.file.name.lastIndexOf('.')
let fileNameLength = file.file.name.length
let fileFormat = file.file.name.substring(temporary + 1, fileNameLength)
// 文件路径和文件名
let cloudFilePath = this.fileParams.folder + getSimpleUUID() + '.' + fileFormat
console.log(`log- cloudFilePath: ${JSON.stringify(cloudFilePath)}`)
// 执行上传服务
cos.putObject({
// 你的存储桶名称
Bucket: 'xxxxxxxxxxx',
// 你的存储桶地址
Region: 'ap-nanjing',
// key加上路径写法可以生成文件夹
Key: cloudFilePath,
StorageClass: 'STANDARD',
// 上传文件对象
Body: file.file,
onProgress: progressData => {
console.log(`log- progressData: ${JSON.stringify(progressData)}`)
if (progressData.percent) {
that.showProgress = true
that.progress = Math.floor(progressData.percent * 100)
}
}
}, (err, data) => {
console.log(`log- upload data: ${JSON.stringify(data)} | err: ${JSON.stringify(err)}`)
if (data && data.statusCode === 200) {
// 文件上传成功后填充
// 自定义CDN域名
// let uploadResult = 'https://static.files.xxxxxxx.cn' + cloudFilePath
// 未使用自定义CDN域名
let uploadResult = `https://${data.Location}`
that.showProgress = false
console.log(`log- 上传完成 URL:${uploadResult}`)
that.$message({message: '上传成功', type: 'success'})
that.$emit('complete', uploadResult)
} else {
that.$message.error("上传失败,请稍后重试!")
}
})
}
}
}
组件使用
<el-form-item label="新闻封面" prop="coverUrl">
<cos-upload position="news"
:styleType="2"
:showFile="form.coverUrl"
v-model="form.coverUrl">
</cos-upload>
</el-form-item>
以上就是在Vue.js下,腾讯云COS WEB直传的具体应用。
Q.E.D.