不知道大家会不会和我一样,看到一篇好的干货,点开前很积极,点开后看一点后就不想看了,最后把它埋没在收藏夹(收藏===学会)里面,日积月累就会积压很多,所以这篇文章的输出——来自《陈旧的收藏集》
我的记忆告诉我好像是去年,有段时间各种的公众号、掘金的帖子都在说实现大文件上传、切片上传、断点续传,尤其是断点续传,听起来就很让人很兴奋,哇,好厉害,怎么做呀?原理是什么?今天我们来动手自己实现一个demo,理解它内部的原理
这篇文章的输出主要是以思路为主,代码贴上去会有很多不齐全,我觉得最好去看一下源码,也不会很多,本来想写小白教程的,写出来的话会很长,而且耐心的看下去也是个问题,所以选择了以思路这种方式去写,github地址
实现功能
- 切片上传
- 开始 / 暂停
- 删除
- 预览
背景
首先我们要了解一下为什么会有切片上传的需求,在网络传输的过程中,我们避免不了网络信号不好而导致请求超时,比如说上传1G的视频,默认上传方式的话网络一不好就会引发超时,那就避免不了要重新上传
但是如果是切片的话,就可以记录在哪一部分出问题了,到时候就可以从这部分开始重新上传,而且传输的速度也会快很多,以上是我的理解
具体实现
背景了解后,我们先来实现前端部分,先了解一下什么是File
File
MDN的描述说File 是特殊的Blob 类型
切片
既然这样Blob 类型里面是有一个slice 方法的,这样就可以实现切片了
// 前端
// 生成每3个一组 chunks
const generateChunks = (file: File, size: number) => {
const totalChunks: chunk[][] = []
const fileChunks: chunk[] = []
// 切片 根据 size 切成想要的大小
// hash 是为了记录切片的位置
for (let [cur, index] = [0, 0]; cur < file.size; cur += size) {
fileChunks.push({ hash: index++, chunk: file.slice(cur, cur + size) })
}
// 分成3组发一次请求,为了点段续传
for (let i = 0; i < fileChunks.length; i += 3) {
totalChunks.push(fileChunks.slice(i, i + 3))
}
return { totalChunks, total: fileChunks.length }
}
最终切出来的格式是这样的,接下来就开始发送请求了
发送请求
这边传的参数稍微有点简单,不过也能实现
先说一下为什么分成一组一组去发送请求
- 为了性能优化,如果切片有100个的话就要
for循环100个请求,如果分成一组一组的话,可以等待一组接受完再去发送下一组减少性能消耗 - 当上传失败的时候,就可以从这一组重新上传,可以当做前端记录失败点,然后再从这个位置去上传,这就是断点上传的
// 发送请求
const chunkRequest = async (map: chunk[][], name: string, total: number) => {
const [firstArr, ...rest] = map
if (!firstArr) return
const requestMap = firstArr.map(item => {
const formData = new FormData()
formData.append('filename', name)
formData.append('hash', String(item.hash))
formData.append('chunk', item.chunk)
formData.append('total', String(total))
return fetch('<http://localhost:3000/upload>', {
method: 'POST',
signal: fileMap[name].controller.signal, // 中断请求
body: formData
})
})
try {
await Promise.all(requestMap)
// 计算进度条
const hash = firstArr.at(-1)!.hash + 1
fileMap[name].propress = Math.floor((100 * hash) / total)
// 递归
chunkRequest(rest, name, total)
} catch (err) {
console.log(err, 'er')
}
}
现在来写一下后端部分, 后端是用koa 写的,这里主要展示一下流程,细节的东西可以去看一下源码,跑起来运行一下,相信你很快就会理解
// 后端
// 上传
router.post('/upload', async ctx => {
// 这里用到了 @koa/cors koa-body
const { filename, hash, total, cur } = ctx.request.body
const filepath = ctx.request.files.chunk.filepath
ctx.body = {
code: 200,
filename,
total,
cur,
msg: '上传成功'
}
// 这里用了 Map储存上传的临时文件路径
addStore(filename, obj => {
obj.total = total
obj.chunk[hash] = filepath
})
})
// --------------------------------- ./utils/store.js
export const store = new Map()
export const addStore = (key, cb) => {
const hasKey = store.has(key)
if (!hasKey) {
store.set(key, {
total: 0,
chunk: {}
})
}
const keyValue = store.get(key)
cb(keyValue)
const { chunk, total } = keyValue
const list = Object.values(chunk)
if (list.length === total * 1) {
// 主要通过管道流来写入
const writer = createWriteStream(initPath(UPLOAD_DIR, key))
streamWrite(key, list, writer)
}
}
// 这是核心 上传文件的拼接
const streamWrite = (key, list, writer) => {
const [path, ...rest] = list
if (!path) return
const reader = createReadStream(path)
// end:false 很重要 能解决文件生成残缺,内存泄漏问题
reader.pipe(writer, { end: false })
reader.on('end', () => {
streamWrite(key, rest, writer)
})
}
到这里就可以实现一个完整的切片上传了,接下来实现断点续传
断点续传
先来实现个开始暂停功能,原理其实就是中断请求,然而fetch 里面是没有自带中断的,只能需要依靠 AbortController ,搞笑的是这里只要中断之后,再重新请求就会发不出,这里是MDN的描述,因为它是只读的,所以需要重新new一下实例,让它的状态消失
// 前端
// 点击开始暂停
const handleStartAndStop = async (data: file) => {
const { name } = data
data.isStop = !data.isStop
if (data.isStop) {
data.controller.abort()
data.controller = new AbortController()
return
}
// 获取到最后上传的 hash位置重新请求 后端的话不需要改
reloadUpload(name)
}
预览
这个比较简单,只需后端返回一个路径,前端打开就好了
// 后端
import serve from 'koa-static'
// 开启静态服务文件
app.use(serve(initPath(UPLOAD_DIR)))
// 预览
router.post('/preview', ctx => {
const { name } = JSON.parse(ctx.request.body)
ctx.body = {
code: 200,
data: {
url: `http://localhost:3000/${name}`
}
}
})
最后
有不懂的可以留言,希望大家可以动手实现,动手===学会,谢谢观看