上传功能
在项目开发中,上传文件的应用场景非常的多,一般情况下,上传的接口都是由公司内部自己开发一套适用于任何场景都可以使用的接口,但是这意味着该接口所涉及到的工作量与技术点就非常的多,且不稳定。于是大公司选择了前者自己研发,小公司则选择了性价比更高的第三方云存储服务,国内知名的云技术服务商基本上是 阿里云 和 腾讯云。以下是腾讯云存储 COS 基本使用流程:
腾讯云存储COS
**对象存储(Cloud Object Storage,COS)**是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
首先进行登录注册 > 实名认证 > 公有读私有写
-
控制台:https://console.cloud.tencent.com/cos/bucket
- 创建存储桶对象,并完成相关配置项
- 存储桶 CORS 跨域访问控制:https://console.cloud.tencent.com/cos/bucket?bucket=hr-1305184603®ion=ap-guangzhou&type=securityconfig&anchorType=corsSetting
-
产品 API 文档:https://cloud.tencent.com/document/product/436
-
SDK 下载列表:https://console.cloud.tencent.com/cos/sdk [也可 maven 配置 / npm 下载]
-
Secret 密钥管理:https://console.cloud.tencent.com/cam/capi
使用示例
- 安装 SDK :
npm install --save cos-js-sdk-v5
- 使用:
index.js
:
import COS from 'cos-js-sdk-v5'
// 实例化 COS 对象
const cos = new COS({
SecretID: 'AKIDWquiGXXGNIyOZU9YicGfohNnxsXw5zd1',
SecretKey: 'A4i6F8mHbfU3MPEn69wUnqdBUXLvFJ7f'
})
// 上传配置
cos.putObject(
{
Bucket: 'hr-1305184603', // 存储桶名称
Region: 'ap-guangzhou', // 存储桶所在地域
Key: file.uid + '', // 上传的文件对象 id 唯一值,必须是字符串
StorageClass: 'STANDARD', // 固定值
Body: file, // 上传的文件对象
onProgress: (progressData) => { // 上传进度事件
/*
progressData: {
loaded: 1005180, // 已上传文件大小
total: 1005180, // 总上传文件大小
speed: 1100963.86, // 总用时
percent: 1 // 进度 1: 完成, 0: 未开始
}
*/
}
},
(err || data) => { // 上传结果回调函数
// err 和 data :二者存其一,有 err 代表上传失败,有 data 代表上传成功,接收一个返回值
/*
data: {
ETag: "", // 有效期标记
Location: "hr-1305184603.cos.ap-guangzhou.myqcloud.com/1659251801441", // 上传成功的图片地址,可添加 http:// || https:// 前缀
headers: {content-length: '0'}, // header 头部
statusCode: 200 // 状态码
}
*/
}
)
上传组件封装
uploadImg.vue
:
<template>
<div class="uploadImg">
<el-upload class="avatar-uploader" action="https://jsonplaceholder.typicode.com/posts/" :show-file-list="false" :before-upload="beforeAvatarUpload" :http-request="httpRequest" >
<img v-if="value" v-globalImgerror="defaultImg" :src="value" class="avatar" @click.stop.prevent="isShow = true" >
<i v-else class="el-icon-plus avatar-uploader-icon" />
</el-upload>
<!-- 删除按钮 -->
<div v-if="value" class="del" @click="del">X</div>
<!-- 预览 -->
<el-dialog title="图片预览" width="500px" :visible.sync="isShow">
<img style="width: 100%" :src="value" alt="">
</el-dialog>
<transition name="fade">
<el-progress v-if="loading" :percentage="percentage" />
</transition>
</div>
</template>
<script>
import COS from 'cos-js-sdk-v5'
const cos = new COS({
SecretId: 'AKIDNXR8bpeSF1JL8EyZopxRfdHgqGUeuEn8',
SecretKey: 'HJ9mAbQEn7jgjY9swEI6yAamk7MN8hty'
})
export default {
name: 'UploadImg',
props: {
value: {
type: String,
default: ''
}
},
data() {
return {
defaultImg: require('@/assets/common/head.jpg'),
loading: false,
percentage: 0,
isShow: false
// imageUrl: ''
}
},
methods: {
beforeAvatarUpload(file) {
return true
},
httpRequest(res) {
this.loading = true
this.percentage = 0
cos.putObject(
{
Bucket:
'dhf481229-1305249343' /* 必须:存储桶,使用自己注册的存储桶 */,
Region: 'ap-guangzhou' /* 存储桶所在地域,必须字段 */,
Key: res.file.uid + '' /* 必须,要求是字符串 */,
StorageClass: 'STANDARD', // 固定,不用修改
Body: res.file, // 上传文件对象
onProgress: (progressData) => {
// 上传进度
this.percentage = progressData.percent * 100
if (progressData.percent === 1) {
// setTimeout(() => {
this.loading = false
// }, 1000)
}
console.log(JSON.stringify(progressData))
}
},
(err, data) => {
// 上传成功的回调 err:代表错误,没有错误就代表成功 data:上传成功的接口返回值
console.log(err || data)
this.$emit('input', 'http://' + data.Location)
// this.imageUrl = 'http://' + data.Location
}
)
},
// 删除
del() {
this.$emit('input', '')
}
}
}
</script>
<style lang="scss" scoped>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
.uploadImg {
position: relative;
width: 178px;
&:hover {
.del {
display: block;
}
}
.del {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
display: none;
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.5s;
}
.fade-enter {
opacity: 0;
// transform: translateY(-100px);
}
.fade-leave-to {
opacity: 0;
// transform: translateY(100px);
}
}
</style>
index.vue
:
<template>
<div>
<h3>上传v-model显示上传地址:{{ imgUrl }}</h3>
<UploadImg v-model="imgUrl" />
<el-upload class="avatar-uploader" action="#" :show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload" :http-request="httpRequest">
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon" />
</el-upload>
</div>
</template>
<script>
import COS from 'cos-js-sdk-v5'
const cos = new COS({
SecretId: 'AKIDWquiGXXGNIyOZU9YicGfohNnxsXw5zd1',
SecretKey: 'A4i6F8mHbfU3MPEn69wUnqdBUXLvFJ7f'
})
export default {
data() {
return {
imgUrl: '',
imageUrl: ''
}
},
methods: {
// 文件上传成功回调
handleAvatarSuccess(res, file) {
console.log(res, file)
},
// 上传之前回调
beforeAvatarUpload(file) {
const isPng = file.type === 'image/png' || file.type === 'image/jpeg'
const isLF2M = file.size <= 1024 * 1024 * 2
if (!isPng) {
this.$message.error('请上传图片文件')
return false
}
if (!isLF2M) {
this.$message.error('文件不得超过 2m')
return false
}
return true
},
//自定义上传配置回调
httpRequest(res) {
// res: 封装的文件对象
console.log(res)
cos.putObject(
{
Bucket: 'hr-1305184603' /* 必须:存储桶 */,
Region: 'ap-guangzhou' /* 存储桶所在地域,必须字段 */,
Key: res.file.uid + '' /* 必须,要求是字符串 */,
StorageClass: 'STANDARD', // 固定,不用修改
Body: res.file, // 上传文件对象
onProgress: progressData => {
// 上传进度
console.log(progressData)
}
},
(err, data) => {
// 上传成功的回调 err:代表错误,没有错误就代表成功 data:上传成功的接口返回值
console.log(err || data)
this.imageUrl = 'http://' + data.Location
}
)
}
}
}
</script>
<style>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
下载文件相关场景
本地同源下载
链接下载
场景:同源下载文件。
解决:只需要一个
<a>
、<iframe>
标签即可。之所以要
<iframe>
标签是因为<a>
标签会进行跳转,所以需要在当前页面创建一个空的、隐藏的<iframe>
标签,让 标签的 target 指向它,让用户无感的在内部进行下载。
downloadFile(obj, name, suffix) {
const url = window.URL.createObjectURL(new Blob([obj]))
const iframe = document.createElement('iframe')
iframe.name = 'iframe'
iframe.style = 'display: none'
document.body.appendChild(iframe)
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.target = 'iframe' // --不刷新、跳转页面
const fileName = parseTime(new Date()) + '-' + name + '.' + suffix
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
link.dispatchEvent(new MouseEvent("click"));
window.URL.revokeObjectURL(url)
document.body.removeChild(link)
},
文件流下载
场景:由后台来返回文件流,需要前台用 ajax
进行异步请求并处理
pdf:
const downloads = () => {
const url = pdf.url
const filename = pdf.title
const x = new XMLHttpRequest()
x.open('GET', url, true)
x.responseType = 'blob'
x.onload = () => {
// 会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。
const url = window.URL.createObjectURL(x.response)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
a.dispatchEvent(new MouseEvent("click")); // 这里会触发两次 click,主要应对 IE
window.URL.revokeObjectURL(url);
}
x.send()
}
图片:
let url = "后端返回文件流的地址"
fetch(url,{method: 'get',headers:{'Content-Type': 'image/jpeg'}}).then(
res=>res.blob().then(blob=>{
var reader = new FileReader();
reader.readAsDataURL(blob); // 转换为base64,可以直接放入a标签href
reader.onload = function (e) {
var down = document.createElement('a'); // 转换完成,创建一个a标签用于下载
down.download = name;
down.href = String(e.target.result);
// document.appendChild(down)
down.click();
down.dispatchEvent(new MouseEvent("click"));
down.remove();
}
})
)
跨域下载文件
场景:后台返回一个不同服务器的文件链接(不限于包括图片、pdf文件),需要前端去下载到本地。这里引发了跨域问题。
解决方案:这个由前端去解决并不是很好,最好是可以让后台先下载好将 blob 数据流返回给前端去处理。
- 1、如果是固定的一个域名,建立一层
nginx
代理,前端访问代理服务器来绕过跨域问题 - 2、最好的方案:由后台去下载好文件,然后返回文件流给前端去处理(跨域 => 同源)。
处理代码如下:
图片文件处理
download(href, name) {
let eleLink = document.createElement('a')
eleLink.download = name
eleLink.href = href
eleLink.click()
eleLink.remove()
},
downloadByBlob(url, name) {
let image = new Image()
image.setAttribute('crossOrigin', 'anonymous')
image.src = url
image.onload = () => {
let canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
let ctx = canvas.getContext('2d')
ctx.drawImage(image, 0, 0, image.width, image.height)
canvas.toBlob(blob => {
let url = URL.createObjectURL(blob)
this.download(url, name)
// 用完释放URL对象
URL.revokeObjectURL(url)
})
}
},