最近项目中要做一个大文件上传的功能,既然是大文件(以G为单位),那么肯定不能使用常规的上传方式,最先想到的就是分片上传。又因为我们项目中使用的是阿里云 oss 储存服务,所以分片上传使用其官方提供的接口即可,我们只要做好对接就行了。
阿里云分片上传官方文档: https://help.aliyun.com/zh/oss/user-guide/multipart-upload
这个功能其实前后端都可以做,因为官方都提供了对应的 SDK,经过讨论最终决定让前端来实现。既然如此,那么我们 Vue 项目中需要先安装阿里云 oss 的 SDK。SDK 文档:https://help.aliyun.com/zh/oss/developer-reference/browser-js
npm install ali-oss其次是需要后端配合的一些东西:
1- 后端需要提供一个接口,用于生成 oss 的上传凭证(AccessKeyId、AccessKeySecret、Bucket 等),因为并不是谁都可以往你的 oss 服务器上传文件的,需要进行身份验证。具体的可以查看授权方式章节,我们选用的是方式一:STS 临时访问凭证。
2- 后端需要去阿里云 oss 后台创建一个 Bucket,也就是存放文件的容器。并且需要配置 Bucket 的访问权限,允许前端访问,不然会有跨域问题。
上面这些都准备好之后,前端就可以开始实现分片上传了。为了方便复用,所以我写了一个大文件上传的组件,可以在 Vue 项目中方便地使用。
big-file-upload.vue
<template>
<div class="big-file-box">
<div class="big-file" :style="{width: width + 'px', height: height + 'px'}">
<input type="file" :accept="accept" ref="file" style="opacity: 0;" class="file-input" @change="handleFileChange">
<el-button type="primary">{{btnText}}</el-button>
</div>
<div v-if="isError" style="color: red;">上传中断,请点击进度条后面的按钮【继续上传】</div>
<div v-if="isUploading && !isFinished && !isError" style="color: #666;">正在上传,请等待…</div>
<div style="display: flex; align-items: center; gap:5px;">
<el-progress :percentage="progress" v-if="isUploading && !isFinished" style="flex: 1;"></el-progress>
<el-button type="text" @click="resumeUpload" v-if="abortCheckpoint && isError">继续上传</el-button>
</div>
</div>
</template>
<script>
import OSS from 'ali-oss'
export default {
name: "BigFileUpload",
props: {
btnText: {
type: String,
default: "点击上传文件"
},
width: {
type: Number,
default: 180
},
height: {
type: Number,
default: 38
},
accept: {
type: String,
default: "*"
}
},
data() {
return {
file: null,
progress: 0,
isUploading: false, // 是否正在上传
isError: false, // 是否上传错误
isFinished: false, // 是否上传完成
abortCheckpoint: null,
client: null,
}
},
mounted() {
this.getSign()
},
methods: {
// 请求后端接口获取阿里云 oss 临时凭证 STS Token
getSign() {
$request.api('common/get_sts_token', {}, (status, data, msg) => {
if (status != 1) {
this.$alert(msg)
return
}
this.initOssClient(data)
})
},
// 初始化oss客户端
initOssClient(sign) {
this.client = new OSS({
region: 'oss-cn-hangzhou', // 这里填写你的 oss 区域,我们服务器位于杭州,所以填的杭州
authorizationV4: true,
accessKeyId: sign.AccessKeyId,
accessKeySecret: sign.AccessKeySecret,
stsToken: sign.SecurityToken,
bucket: '这里填写后端创建的 bucket 的名称'
})
},
async uploadFile(checkpoint) {
if (!this.file) {
this.$alert("请选择文件")
return
}
this.isUploading = true
try {
const suffix = this.getFileSuffix(this.file.name)
const result = await this.client.multipartUpload(
'big-file/' + Date.now() + '.' + suffix, // 将文件以时间戳命名,避免重复。 big-file 是文件存放的层级。(注意: oss 是没有文件夹概念的)
this.file,
{
checkpoint: checkpoint,
progress: (p, cpt, res) => {
this.abortCheckpoint = cpt // 将断点信息保存下来,用于后续的断点续传
this.progress = Math.round(p * 100) // 进度条显示百分比
},
mime: "text/plain",
parallel: 4, // 设置并发上传的分片数量。
partSize: 1024 * 1024 * 1, // 设置每个分片的大小,默认值是 1MB
}
)
// 上传成功后清理状态
this.abortCheckpoint = null;
this.progress = 0;
this.isError = false
// 进度条延迟1秒后消失
setTimeout(() => { this.isUploading = false;this.isFinished = true }, 1000)
// 上传成功,将结果回传给父组件
this.$emit('success', result.res)
} catch(e) {
if (e.code === 'SecurityTokenExpired' || e.code === 'InvalidSecurityToken') {
this.getSign()
}
this.isError = true // 上传错误
this.isFinished = false
console.log(e.message)
}
},
// 获取文件后缀
getFileSuffix (fileName) {
return fileName.split('.').pop()
},
async handleFileChange(e) {
this.file = e.target.files[0]
// 选择新文件后,重置所有状态
this.abortCheckpoint = null;
this.progress = 0;
this.isUploading = false
this.isFinished = false
this.isError = false
this.uploadFile(null)
},
// 断点续传
resumeUpload() {
if (this.abortCheckpoint) {
this.isError = false
this.isFinished = false
this.isUploading = true
this.uploadFile(this.abortCheckpoint)
} else {
this.$toast('没有可用的断点信息,请重新开始上传')
}
},
}
}
</script>
<style lang="less" scoped>
.big-file {
display: flex;
align-items: center;
justify-content: flex-start;
position: relative;
min-width: 100px;
.file-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
}
</style>组件写好了,下面就是在 Vue 项目中使用它了
<template>
<div>
<BigFileUpload btnText="点击上传案例视频(mp4格式)" :width="200" accept=".mp4" @success="handleSuccessVideo" />
</div>
</template>
<script>
import BigFileUpload from './big-file-upload.vue'
export default {
components: {
BigFileUpload
},
data() {
return {
video_list: []
}
},
methods: {
handleSuccessVideo(result) {
const url = new URL(result.requestUrls[0]);
this.video_list.push(url.pathname)
},
}
}
</script>页面效果截图
1- 正常状态
2- 中断状态
最后需要注意的是,阿里云 oss 分片上传是收费的,按实际请求数进行收费,具体的收费标准见下图
关于收费方面的官方说明如下,分片上传主要用到的是接口请求的费用
对象存储 OSS 按实际使用量付费。将数据存储到 OSS 后,会按存储时长产生存储费用;每次上传、下载或访问数据产生请求费用;通过公网分发数据则会产生外网流出流量费用。此外,启用 CDN 加速、图片处理、传输加速、DDoS 防护等增值服务,会产生对应的增值计费项。如果业务的访问模式稳定,具备可预期的存储量、请求量和外网流出流量,建议根据账单中的具体计费项购买资源包进行抵扣,以降低成本。资源包必须与计费项精准对应(如下行流量包仅能抵扣外网流出流量费用),买错将导致无法抵扣。