阿里云OSS简单上传、分片上传、断点续传
文章主要为阿里云不支持STS方式实现分片上传、断点续传、普通上传等。
·
阿里云OSS直传、分片上传、断点续传。
本文以VUE2、axios为例,后端通过签名的方式与OSS交互
OSS的STS 方式更为简单,封装了好多方法,支持STS的OSS可查看Browser.js完成上传等相关需求
- 定义FileUploader类:
import axios from 'axios'
import TrainingService from '后端提供签名服务文件'
export class FileUploader {
constructor (file, options) {
this.client = axios.create({
timeout: 5000,
withCredentials: false // 跨域请求时不携带cookie
})
this.file = file // 文件对象
this.progress = 0 // 进度
this.isStop = false // 是否停止上传
this.chunkIndex = 0 // 当前分片索引
this.chunkSize = options.chunkSize || 1024 * 1024 * 5 // 5MB
this.chunkNumber = options.chunkSize ? Math.ceil(this.file.size / this.chunkSize) : 1 // 计算分片数量
this.categoryId = options.categoryId // 分类id
this.uploadId = options.uploadId // 上传id
this.objectName = options.objectName // 文件名(分片获取ID时,文件名由后端生成)
this.extension = options.extension // 文件后缀
this.originalFilename = options.originalFilename
this.totalPages = options.totalPages // 总页数
this.duration = options.duration // 视频时长(秒)
this.fileSize = this.file.size // 文件大小
this.uploadStatus = 0
this.unloadRemark = '' // 卸载备注
this.uploadedChunks = options.uploadId ? JSON.parse(localStorage.getItem(options.uploadId)) || [] : [] // 已上传的分片
this.retryCount = 0 // 重试次数
this.maxRetryCount = options.maxRetryCount || 3 // 最大重试次数
this.partETags = [] // 新增partETags数组
this.isChunkUpload = !!options.chunkSize // 是否使用分片上传
}
on (event, handle) {
if (event === 'error') {
this.onError = handle
} else if (event === 'success') {
this.onSuccess = handle
} else if (event === 'progress') {
this.onProgress = handle
} else if (event === 'retry') {
this.onRetry = handle
}
}
async start () {
// 如果已经停止,则不执行上传
if (this.isStop) {
return
}
if (this.isChunkUpload) {
// 检测是否已经上传完成
if (this.chunkIndex >= this.chunkNumber) {
this.uploadCompleted()
return
}
// 跳过已上传的分片
while (this.uploadedChunks.includes(this.chunkIndex)) {
this.chunkIndex += 1
}
// 读取分片数据
const start = this.chunkIndex * this.chunkSize // 计算当前分片起始位置
const chunkData = this.file.slice(start, start + this.chunkSize) // 读取当前分片数据
const boolname = this.file.name + '-' + this.chunkIndex // 临时文件名
const tmpFile = new File([chunkData], boolname) // 创建临时文件
await this.getSignUrlFun(tmpFile, true) // 后端对分片、普通,区分两种签名
} else {
await this.getSignUrlFun(this.file, false) // 后端对分片、普通,区分两种签名
}
}
// 获取签名url并上传数据
async getSignUrlFun (file, isChunk) {
let params
if (isChunk) {
params = {
objectName: this.objectName,
uploadId: this.uploadId,
partNumber: this.chunkIndex + 1
}
} else {
params = {
extension: this.extension
}
}
console.log('获取签名参数:', params)
try {
const resSign = isChunk ? await TrainingService.getSignUrl(params) : await TrainingService.getsimpleSignUrl(params)
console.log(resSign)
if (resSign.code === 200) {
const config = {
headers: {
'Content-Type': 'multipart/form-data'
}
}
if (!isChunk) {
config.onUploadProgress = (progressEvent) => {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
this.uploadProgressUpdated(percentCompleted)
}
}
return this.client.put(resSign.result.url, file, config).then((res) => {
// 上传成功
if (res.status === 200) {
console.log('上传成功')
if (isChunk) {
// 记录已上传的分片
this.uploadedChunks.push(this.chunkIndex)
localStorage.setItem(this.uploadId, JSON.stringify(this.uploadedChunks))
// 记录eTag
const eTag = res.headers.etag
this.partETags.push({
partNumber: this.chunkIndex + 1,
etag: eTag
})
this.chunkIndex++ // 更新分片索引
this.uploadProgressUpdated() // 更新进度
this.retryCount = 0 // 重置重试次数
return this.start()
} else {
this.objectName = resSign.result.objectName
this.uploadCompleted()
}
} else {
throw new Error('上传失败')
}
}).catch((err) => {
// 上传失败,重试
console.error(err)
})
}
} catch (error) {
console.log(error)
}
}
isOver () {
return this.uploadStatus === 5 || this.uploadStatus === 7
}
cancel () {
this.isStop = true
this.onError && this.onError('已取消')
}
// 重试上传
retry () {
this.isStop = false
this.uploadStatus = 0
this.start()
this.onRetry && this.onRetry()
}
// 更新上传进度
uploadProgressUpdated (percentCompleted) {
if (this.uploadStatus === 0) {
this.uploadStatus = 3
}
if (this.isChunkUpload) {
percentCompleted = parseInt((this.chunkIndex / this.chunkNumber) * 100 + '')
}
this.onProgress && this.onProgress(percentCompleted)
}
// 分片需要最后有merge接口、来完成所传分片的合并
async merge () {
const params = {
uploadId: this.uploadId,
objectName: this.objectName,
partETags: this.partETags
}
console.log('归类合并参数:', params)
try {
const res = await TrainingService.mergeFile(params)
console.log(res)
if (res.code === 200) {
console.log('文件合并成功')
} else {
throw new Error('文件合并失败')
}
} catch (error) {
console.error('文件合并出错:', error)
this.uploadStatus = 5
this.onError && this.onError('文件合并失败: ' + error.message)
}
}
async pushFileData (fileInfo) {
const { totalPages, extension, size, objectName, originalFilename, duration } = fileInfo
const params = {
categoryId: this.categoryId, // 此处可删,我做了分类需求
totalPages,
extension,
size,
objectName,
originalFilename,
duration
}
try {
const res = await TrainingService.createFile(params)
if (res.code === 200) {
this.$message.success('数据新增成功')
}
} catch (error) {
this.$message.error(error)
return false
}
}
// 上传完成
async uploadCompleted () {
this.uploadStatus = 7
if (this.isChunkUpload) {
localStorage.removeItem(this.uploadId) // 上传完成后清除记录信息
this.merge()
}
this.onSuccess && this.onSuccess()
// 业务代码===》〉上传成功告诉后端
this.pushFileData({
totalPages: this.totalPages, // PDF所需页码
duration: this.duration, // video所需时长
extension: this.extension,
size: this.fileSize,
objectName: this.objectName,
originalFilename: this.originalFilename
})
}
// 上传失败
uploadedFail (e) {
console.log('上传失败,错误信息:', e)
this.uploadStatus = 5
this.onError && this.onError('失败.2')
}
// 获取上传状态
getUploadStatus () {
return this.uploadStatus
}
// 获取上传进度
getUploadProgress () {
if (this.isChunkUpload) {
if (this.chunkNumber === 0) {
return 0
}
return (this.chunkIndex / this.chunkNumber) * 100
} else {
return this.progress
}
}
// 获取上传备注
getUploadRemark () {
return this.unloadRemark
}
}
如何使用?以ant-deisgn-vue的upload组件为例,定义上传组件并用之前定义的FileUploader
// html代码
<template>
<div class="wrapper">
<a-upload-dragger
:customRequest="customRequest"
:beforeUpload="beforeUpload"
:showUploadList="false"
name="file"
:multiple="true"
>
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">
请将PDF拖拽到/点击此处上传文件
</p>
<p class="ant-upload-hint">
支持一次上传多个 / 支持2G以内的PDF文件
</p>
</a-upload-dragger>
<div style="height: 20px;"></div>
<a-table :columns="columns" :data-source="data">
<span slot="progress" slot-scope="text">
<a-progress :percent="text" />
</span>
<span slot="uploadStatus" slot-scope="text,record">
<a-tag color="#108ee9" v-if="text == '3'">上传中</a-tag>
<a-tag color="#f50" @click="retryUpload(record)" v-if="text == '5'"> 失败重试</a-tag>
<a-tag color="#108ee9" v-if="text == '7'">上传成功</a-tag>
</span>
</a-table>
</div>
</template>
// js代码
data () {
return {
isChunk: false,
columns: [
{
title: '文件名',
dataIndex: 'originalFilename'
},
{
title: '文件大小(MB)',
width: '140px',
dataIndex: 'size',
customRender: (text) => {
const sizeInMB = (text / (1024 * 1024)).toFixed(2)
return `${sizeInMB} MB`
}
},
{
title: '进度',
dataIndex: 'progress',
width: '130px',
scopedSlots: { customRender: 'progress' }
},
{
title: '操作',
width: '100px',
dataIndex: 'uploadStatus',
scopedSlots: { customRender: 'uploadStatus' }
}
],
allowedTypes: ['application/pdf'],
data: []
}
},
methods: {
// 解析 PDF 文件并获取页数 引用 import pdf from 'vue-pdf'
async getPdfPageCount (file) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
const loadingTask = pdf.createLoadingTask(reader.result)
loadingTask.promise
.then((pdf) => {
resolve(pdf.numPages) // 返回 PDF 页数
})
.catch((error) => {
reject(error)
})
}
reader.onerror = (error) => {
reject(error)
}
reader.readAsDataURL(file) // 读取文件为 Data URL
})
},
async customRequest ({ file }) {
let uploader = null // 初始化上传器实例为 null
// 获取文件后缀名和原始文件名
const fileName = file.name
const lastDotIndex = fileName.lastIndexOf('.')
const extension = lastDotIndex !== -1 ? fileName.slice(lastDotIndex + 1) : ''
const originalFilename = lastDotIndex !== -1 ? fileName.slice(0, lastDotIndex) : fileName
// 创建文件数据对象
const fileData = {
categoryId: this.categoryId,
originalFilename,
size: file.size, // 文件大小(字节)
extension, // 文件后缀名
totalPages: 0, // PDF 页数,默认为 0(稍后赋值)
progress: 0, // 上传进度
uploadStatus: 0, // 上传状态
file, // 文件对象
uploadId: '', // 上传ID(稍后赋值)
objectName: '' // 后端返回对象名(稍后赋值)
}
fileData.totalPages = await this.getPdfPageCount(file)
if (this.isChunk) {
// 如果是分片上传,则执行相关逻辑 // 获取上传ID和文件名等信息
try {
const res = await TrainingService.getUploadId({ extension })
fileData.uploadId = res.uploadId
fileData.objectName = res.objectName
} catch (error) {
console.error('获取上传ID失败:', error)
this.$message.error('获取上传ID失败')
return
}
uploader = new FileUploader(file, {
chunkSize: 5 * 1024 * 1024,
uploadId: fileData.uploadId,
objectName: fileData.objectName || '',
extension: fileData.extension,
originalFilename,
totalPages: fileData.totalPages
})
} else {
// 如果是非分片上传,则执行相关逻辑
uploader = new FileUploader(file, {
extension: fileData.extension,
originalFilename,
totalPages: fileData.totalPages
})
}
this.data.push(fileData) // 将文件数据添加到列表中
const index = this.data.length - 1 // 获取最新添加的文件数据索引
this.data[index].uploader = uploader // 将上传器实例保存到文件数据中
// 监听上传事件
uploader.on('error', error => {
console.error('上传错误:', error)
fileData.uploadStatus = 5 // 上传失败
})
uploader.on('success', async () => {
console.log('上传成功函数:上传成功!Wxz')
this.$set(this.data[index], 'uploadStatus', 7) // 上传成功状态更新,确保响应式地更新视图
this.$set(this.data[index], 'progress', 100) // 使用 Vue.set 更新进度
})
uploader.on('progress', progress => {
console.log('上传进度:', progress + '%')
this.$set(this.data[index], 'progress', progress) // 使用 Vue.set 更新进度
this.$set(this.data[index], 'uploadStatus', 3) // 使用 Vue.set 更新状态
})
uploader.on('retry', () => {
console.log('正在重试上传...')
this.$set(this.data[index], 'uploadStatus', 5) // 上传成功状态更新,确保响应式地更新视图
})
// 开始上传
uploader.start()
},
beforeUpload (file) {
if (!this.allowedTypes.includes(file.type)) {
this.$message.error('只支持上传PDF文件')
return false
}
const fileSize = file.size
// 2G 对应的字节数
const maxSize = 2 * 1024 * 1024 * 1024
// 判断文件大小是否超过 2G
if (fileSize > maxSize) {
this.$message.error('文件大小超过 2G,无法上传')
// 阻止文件上传
return false
}
if (fileSize > 5 * 1024 * 1024) {
this.isChunk = true
} else {
this.isChunk = false
}
// 允许文件上传
return true
},
async retryUpload (record) {
const index = this.data.findIndex(item => item.originalFilename === record.originalFilename)
if (index !== -1) {
this.data[index].uploadStatus = 3
const uploader = this.data[index].uploader
uploader.retry()
}
}
},
引用组件,完成上传
<a-modal
title="PDF上传"
:visible="visible"
width="700px"
@ok="handleOk"
@cancel="handleCancel"
>
<upload-pdf :categoryId="categoryId"></upload-pdf>
</a-modal>
import uploadPdf from '@/iop-ntsp/views/training/components/uploadPdf'
components: {
uploadPdf
},
// 注 categoryId为分类ID,对于上传文件的类型有规划
- 后端返回样例:
普通上传
分片上传
整体效果
更多推荐
所有评论(0)