后台管理系统中如何进行大文件(视频)上传?

当我们需要上传比较大的文件的时候,容易碰到以下问题:1) 上传时间比较久;2)中间一旦出错就需要重新上传;3)一般服务端会对文件的大小进行限制。这些问题都会导致上传时候的用户体验是很不好的,针对存在的这些问题,我们可以通过分片上传来解决。
分片上传原理:
分片上传的原理就像是把一个大蛋糕切成小块一样。
首先,我们将要上传的大文件分成许多小块,每个小块大小相同,比如每块大小为2MB。然后,我们逐个上传这些小块到服务器。上传的时候,可以同时上传多个小块,也可以一个一个地上传。上传每个小块后,服务器会保存这些小块,并记录它们的顺序和位置信息。
所有小块上传完成后,服务器会把这些小块按照正确的顺序拼接起来,还原成完整的大文件。最后,我们就成功地上传了整个大文件。

口口亚口一

凸凸口凸凸

口凸凸亚亚

凸凸亚亚亚

分片组装

分片上传

文件分片

服务器

image.png


分片上传的好处在于它可以减少上传失败的风险。如果在上传过程中出现了问题,只需要重新上传出错的那个小块,而不需要重新上传整个大文件。
此外,分片上传还可以加快上传速度。因为我们可以同时上传多个小块,充分利用网络的带宽。这样就能够更快地完成文件的上传过程。
分片上传实现:
1读取文件
通过监听 input 的 change 事件,当选取了本地文件后,可以在回调函数中拿到对应的文件

2文件分片
文件分片的核心是用Blob对象的slice方法,我们在上一步获取到选择的文件是一个File对象,它是继承于Blob,所以我们就可以用slice方法对文件进行分片,用法如下:
start 和 end 代表 Blob 里的下标,表示被拷贝进新的 Blob 的字节的起始位置和结束位置。contentType 会给新的 Blob 赋予一个新的文档类型,在这里我们用不到。接下来就来使用slice方法来实现下对文件的分片。

3hash计算
先来思考一个问题,在向服务器上传文件时,怎么去区分不同的文件呢?如果根据文件名去区分的话可以吗?
答案是不可以,因为文件名我们可以是随便修改的,所以不能根据文件名去区分。但是每一份文件的文件内容都不一样,我们可以根据文件的内容去区分,具体怎么做呢?
可以根据文件内容生产一个唯一的 hash 值,大家应该都见过用 webpack 打包出来的文件的文件名都有一串不一样的字符串,这个字符串就是根据文件的内容生成的 hash 值,文件内容变化,hash 值就会跟着发生变化。我们在这里,也可以用这个办法来区分不同的文件。而且通过这个办法,我们还可以实现秒传的功能,怎么做呢?
就是服务器在处理上传文件的请求的时候,要先判断下对应文件的 hash 值有没有记录,如果A和B先后上传一份内容相同的文件,所以这两份文件的 hash 值是一样的。
当A上传的时候会根据文件内容生成一个对应的 hash 值,然后在服务器上就会有一个对应的文件,B再上传的时候,服务器就会发现这个文件的 hash 值之前已经有记录了,说明之前已经上传过相同内容的文件了,所以就不用处理B的这个上传请求了,给用户的感觉就像是实现了秒传。
那么怎么计算文件的hash值呢?可以通过一个工具:spark-md5,所以我们得先安装它。
在上一步获取到了文件的所有切片,我们就可以用这些切片来算该文件的 hash 值,但是如果一个文件特别大,每个切片的所有内容都参与计算的话会很耗时间,所有我们可以采取以下策略:
1)第一个和最后一个切片的内容全部参与计算
2)中间剩余的切片我们分别在前面、后面和中间取2个字节参与计算
这样就既能保证所有的切片参与了计算,也能保证不耗费很长的时间

4文件上传
前端实现
前面已经完成了上传的前置操作,接下来就来看下如何去上传这些切片。
我们以1G的文件来分析,假如每个分片的大小为1M,那么总的分片数将会是1024个,如果我们同时发送这1024个分片,浏览器肯定处理不了,原因是切片文件过多,浏览器一次性创建了太多的请求。
这是没有必要的,拿 chrome 浏览器来说,默认的并发数量只有 6,过多的请求并不会提升上传速度,反而是给浏览器带来了巨大的负担。因此,我们有必要限制前端请求个数。
怎么做呢?
我们要创建最大并发数的请求,比如6个,那么同一时刻我们就允许浏览器只发送6个请求,其中一个请求有了返回的结果后我们再发起一个新的请求,依此类推,直至所有的请求发送完毕。
上传文件时一般还要用到 FormData 对象,需要将我们要传递的文件还有额外信息放到这个 FormData 对象里面。
后端实现
后端我们处理文件时需要用到 multiparty 这个工具,所以也是得先安装,然后再引入它。
我们在处理每个上传的分片的时候,应该先将它们临时存放到服务器的一个地方,方便我们合并的时候再去读取。为了区分不同文件的分片,我们就用文件对应的那个hash为文件夹的名称,将这个文件的所有分片放到这个文件夹中。

5文件合并
上一步我们已经实现了将所有切片上传到服务器了,上传完成之后,我们就可以将所有的切片合并成一个完整的文件了,下面就一块来实现下。
前端实现
前端只需要向服务器发送一个合并的请求,并且为了区分要合并的文件,需要将文件的hash值给传过去
后端实现
在之前已经可以将所有的切片上传到服务器并存储到对应的目录里面去了,合并的时候需要从对应的文件夹中获取所有的切片,然后利用文件的读写操作,就可以实现文件的合并了。合并完成之后,我们将生成的文件以hash值命名存放到对应的位置就可以了。
到这里,我们就已经实现了大文件的分片上传的基本功能了,但是我们没有考虑到如果上传相同的文件的情况,而且如果中间网络断了,我们就得重新上传所有的分片,这些情况在大文件上传中也都需要考虑到,下面,我们就来解决下这两个问题。

6秒传&断点续传
我们在上面有提到,如果内容相同的文件进行hash计算时,对应的hash值应该是一样的,而且我们在服务器上给上传的文件命名的时候就是用对应的hash值命名的,所以在上传之前是不是可以加一个判断,如果有对应的这个文件,就不用再重复上传了,直接告诉用户上传成功,给用户的感觉就像是实现了秒传。接下来,就来看下如何实现的。
前端实现
前端在上传之前,需要将对应文件的hash值告诉服务器,看看服务器上有没有对应的这个文件,如果有,就直接返回,不执行上传分片的操作了。
后端实现
因为我们在合并文件时,文件名时根据该文件的hash值命名的,所以只需要看看服务器上有没有对应的这个hash值的那个文件就可以判断了。
完成上面的步骤后,当我们再上传相同的文件,即使改了文件名,也会提示我们秒传成功了,因为服务器上已经有对应的那个文件了。
上面我们解决了重复上传的文件,但是对于网络中断需要重新上传的问题没有解决,那该如何解决呢?
如果我们之前已经上传了一部分分片了,我们只需要再上传之前拿到这部分分片,然后再过滤掉是不是就可以避免去重复上传这些分片了,也就是只需要上传那些上传失败的分片,所以,再上传之前还得加一个判断。
前端实现
我们还是在那个 verify 的接口中去获取已经上传成功的分片,然后在上传分片前进行一个过滤

JavaScript复制代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

const uploadChunks = async (fileChunks: Array<{file: Blob}>, uploadedList: Array<string>) => {

const formDatas = fileChunks

.filter((chunk, index) => {

// 过滤服务器上已经有的切片

return !uploadedList.includes(`${fileHash.value}-${index}`)

})

.map(({ file }, index) => {

const formData = new FormData()

// 切片文件

formData.append('file', file)

// 切片文件hash

formData.append('chunkHash', `${fileHash.value}-${index}`)

// 大文件的文件名

formData.append('fileName', fileName.value)

// 大文件hash

formData.append('fileHash', fileHash.value)

return formData

})

// ...

}

后端实现
只需要在 /verify 这个接口中加上已经上传成功的所有切片的名称就可以,因为所有的切片都存放在以文件的hash值命名的那个文件夹,所以需要读取这个文件夹中所有的切片的名称就可以。

JavaScript复制代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

/**

* 返回已经上传切片名

* @param {*} fileHash

* @returns

*/

const createUploadedList = async fileHash => {

return fse.existsSync(path.resolve(UPLOAD_DIR, fileHash))

? await fse.readdir(path.resolve(UPLOAD_DIR, fileHash)) // 读取该文件夹下所有的文件的名称

: []

}

// 根据文件hash验证文件有没有上传过

app.post('/verify', async (req, res) => {

const { fileHash, fileName } = req.body

const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${extractExt(fileName)}`)

if (fse.existsSync(filePath)) {

// 文件存在服务器中,不需要再上传了

res.status(200).json({

ok: true,

data: {

shouldUpload: false,

}

});

} else {

// 文件不在服务器中,就需要上传,并且返回服务器上已经存在的切片

res.status(200).json({

ok: true,

data: {

shouldUpload: true,

uploadedList: await createUploadedList(fileHash)

}

});

}

});

若有收获,就点个赞吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值