ruby
title :vue3+element-plus 实现大文件上传(文件分片上传,断点续传)
author:NanYanBeiYu
desc:大文件上传-分片上传
大文件上传
文件分片处理
本文是借助element-ui搭建的页面。使用el-upload组件,因为我们要自定义上传的实现,所以需要将组件的自动上传关掉:auto-upload="false"。
ini
<template>
<el-upload
class="upload-demo"
drag
action=""
:auto-upload="false"
:show-file-list="false"
:on-change="handleChange"
multiple
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip">文件大小不超过4GB</div>
</template>
</el-upload>
</template>
当文件状态发生变化时触发:on-change是文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 handleChange。
获取到文件对象和切片处理
在el-upload组件中,:on-change的参数有file,但这里的file并不是File对象需要通过file.raw
来获取。File由于继承了Blob中的slice方法,可以通过这个方法来将File进行切片。
获取文件对象:
javascript
const handleChange = (file) => {
console.log(file.raw)
}
控制台打印信息:
对获取到的File对象进行切片处理,创建切片函数并调用:
javascript
const chunkSize = 1024 * 1024 * 2
/**
* @description 当文件状态变化时触发
* */
const handleChange = (file) => {
console.log(file.raw)
const fileChunks = createChunks(file.raw,chunkSize)
console.log(fileChunks)
}
/**
* @description 创建文件切片
* @param file 文件
* @param size 切片大小
* @returns fileChunks 切片列表
* */
const createChunks = (file,size) => {
const fileChunks = []
for(let i=0;i < file.size ;i += size){
fileChunks.push(file.slice(i,i+size))
}
return fileChunks
}
观察控制台输出信息:
可以看到已经将这个File对象分片成了1885份。
根据文件内容生成SHA3-256
这里我使用的库是js-sha3 ,详情在github.com/emn178/js-s...。
javascript
const hash = async (chunks) => {
const sha3 = sha3_256.create()
let processedChunks = 0 // 记录已处理的chunks数量
async function _read(i) {
if (i >= chunks.length) {
sha3256.value = sha3.hex()
console.log('sha3256:', sha3256.value)
return // 读取完毕
}
const blob = chunks[i]
const reader = new FileReader()
await new Promise((resolve, reject) => {
reader.onload = (e) => {
e.target.result // 读取到的字节数组
sha3.update(e.target.result)
resolve()
}
reader.onerror = reject
reader.readAsArrayBuffer(blob)
})
await _read(i + 1)
}
await _read(0)
}
控制台输出信息:
将哈希值计算出来后,我们就可以使用这个哈希值来作为文件的唯一标识了。
上传分片信息:
拿到文件的哈希值后,就可以根据服务端的要求来上传分片。
要将分片进行上传,我们就需要对分片列表进行遍历,为每一个分片添加进formData中
markdown
/**
* uploadFile函数负责将文件分片列表(`chunkList.value`)中的所有分片依次上传至服务器。该函数遵循以下步骤:
1. **遍历分片**:
- 使用一个`for`循环,从索引`0`开始,遍历至`chunkList.value.length - 1`。
- 在每次迭代中,获取当前分片`chunk`及其在列表中的索引`index`。
2. **构建FormData对象**:
- 创建一个新的`FormData`实例,用于封装上传所需的各项数据。
- 使用`append`方法向FormData对象添加以下字段:
- `chunk`: 当前遍历到的分片数据。
- `index`: 分片在列表中的索引。
- `sha3256`: 文件的SHA-3256哈希值(存储在`sha3256.value`)。
- `suffix`: 文件的后缀名(存储在`suffix.value`)。
- `totalChunks`: 文件总分片数(存储在`totalChunks.value`)。
3. **调用uploadChunkService进行上传**:
- 使用封装好的FormData对象作为参数,调用`uploadChunkService(formData)`以发起异步请求,将当前分片上传至服务器。
4. **响应处理**:
- 对于上传成功的响应:
- 在控制台输出一条成功消息,包含已成功上传的分片索引及响应数据。
- 对于上传失败的响应:
- 在控制台输出一条错误消息,包含失败的分片索引及具体的错误信息。
**参数**:无
**返回值**:无
**依赖**:
- `chunkList.value`: 存储文件分片数据的数组。
- `sha3256.value`: 文件的SHA-3256哈希值。
- `suffix.value`: 文件的后缀名。
- `totalChunks.value`: 文件总分片数。
- `uploadChunkService(formData)`: 异步服务函数,用于上传单个文件分片。
**注意**:此函数仅负责上传分片,不涉及分片的生成或合并逻辑。实际执行时应确保相关依赖变量已正确初始化。
*/
const uploadFile = () => {
// 遍历分片
for (let i = 0; i < chunkList.value.length; i++) {
const chunk = chunkList.value[i]
const index = i
// 创建FormData对象并添加分片和索引
const formData = new FormData()
formData.append('chunk', chunk) // 添加分片
formData.append('index', index) // 添加索引
formData.append('sha3256', sha3256.value) // 添加sha3256
formData.append('suffix', suffix.value) // 添加后缀名
formData.append('totalChunks', totalChunks.value) // 添加总分片数
uploadChunkService(formData)
.then((res) => {
// 成功上传分片后的处理
console.log(`Chunk ${index} uploaded successfully`, res)
})
.catch((error) => {
// 上传失败后的处理
console.error(`Chunk ${index} upload failed`, error)
})
}
}
file.js
csharp
// 分片上传
export const uploadChunkService = (params) =>
request.post('/cloud/files/upload/chunk', params, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
axios封装
javascript
import { useUserStore } from '@/stores/user.js'
import axios from 'axios'
import router from '@/router'
// import { ElMessage } from 'element-plus'
const baseURL = 'http://127.0.0.1:89/'
const instance = axios.create({
baseURL,
timeout: 100000
})
instance.interceptors.request.use(
(config) => {
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = userStore.token
}
return config
},
(err) => Promise.reject(err)
)
instance.interceptors.response.use(
(res) => {
if (res.status === 200) {
// 添加检查HTTP状态码是否为200
if (res.data.code === 200) {
return res.data // 返回数据而不是整个响应对象
}
}
ElMessage({ message: res.data.msg || '111', type: 'error' })
return Promise.reject(res) // 当HTTP状态码不是200或者服务端返回错误代码时,才reject
},
(err) => {
ElMessage({
message: err.response.data.msg || '222',
type: 'error'
})
console.log(err)
if (err.response?.status === 401) {
router.push('/login')
}
return Promise.reject(err)
}
)
export default instance
export { baseURL }
后端实现:
这里后端使用的是Gin框架。路由这里就不放出来了,可以自行设计。
go
// UploadChunk 分片上传
func UploadChunk(c *gin.Context) {
// 1. 解析请求参数
fileHash := c.PostForm("sha3256") // 文件hash
index := c.PostForm("index") // 分片索引
// 定义一个临时存储路径
tempDir := "./upload/temp/" + fileHash + "/"
filePath := filepath.Join(tempDir, fileHash+"_"+index+".part")
// 使用goroutine处理分片
//创建一个waitgroup,用于等待所有协程执行完毕
var wg sync.WaitGroup
wg.Add(1) // 增加一个协程,表示即将启动一个goroutine
go func() {
defer wg.Done() //确保在goroutine结束时,减少一个计数器
// 确保 tempDir 存在
if _, err := os.Stat(tempDir); os.IsNotExist(err) {
os.MkdirAll(tempDir, os.ModePerm)
}
// 打开文件以追加数据
file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
// 返回错误
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "Failed to open file",
})
return
}
defer file.Close()
// 读取切片数据
part, err := c.FormFile("chunk")
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "Failed to read part",
})
return
}
src, err := part.Open()
if err != nil {
c.AbortWithStatusJSON(500, gin.H{
"code": 500,
"msg": "Failed to open part",
})
return
}
defer src.Close()
// 将切片的内容写入文件
_, err = io.Copy(file, src)
if err != nil {
c.JSON(500, gin.H{
"code": 500,
"msg": "Failed to write part",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "切片上传成功",
})
}()
wg.Wait() // 等待所有协程执行完毕
}
效果:
可以看到这里一个切片了31份,后端存储的切片信息: