分片上传,有手就行

1,073 阅读4分钟

最近学习node在用到fs模块的时候了解到stream流的概念,突然想到可以试着实践一下node server端接收文件

思路

  1. 前端根据自定义的chunk大小切割文件后并发上传
  2. 后端接收文件数据存下文件并根据顺序进行拼接

准备

talk is cheap,直接上手

前端部分需要一个上传文件的input,所以直接使用了@vue/cli快速创建项目,简单写了一个input

<div>
	<input type="file" :accept="accept" @input="getFile" />
	<div @click="upload">点击上传</div>
</div>

为了发送请求,当然要下载咱们最常用的请求库axios,安装过后进行引入,前端部分需要的工具已经简单的准备好了

后端部分我选择了express框架快速搭建node服务加上中间件multiparty接受处理文件,加上node自带的fs模块一切就绪了

具体实现

回到前端,我们要做的就是把带有二进制文件的请求发送给后端,有几点需要实现

拿到二进制文件

文件本身我们可以通过input框的input事件对象获取

this.file = e.target.files[0];

这里之所以拿files[0]是因为files是一个数组

只上传一个文件就拿第一项就好

分成文件切片

拿到文件本身之后我们需要切割分片,主要是使用blob原型对象的slice方法,根据设置的单片文件大小将文件分割成一个数组

	createFileChunkList(file, size) {
		const fileChunkList = [];
		let curSize = 0;
		while (curSize < file.size) {
			fileChunkList.push({
				file: file.slice(curSize, (curSize + size))
			})
		curSize += size;
		}
		return fileChunkList
	}

正确发送请求

我们现在已经拿到了文件的数组,将数组遍历后,给每个切片命名以方便服务端接收切片后按正确顺序拼接,并且使用formData将文件切片和相应信息存入

	this.fileChunkList = this.createFileChunkList(this.file, this.chunkSize)
	.map((file, index) => {
		const formData = new FormData();
		formData.append('chunk', file.file);
		formData.append('hashname', this.file.name + '_' + index);
		formData.append('filename', this.file.name);
		return formData
	})

注意发送请求的时候需要在请求头中设置content-typemultipart/form-data, 然后通过axios发送请求

	const headerConfig = {
		headers: {
			'Content-Type': 'multipart/form-data'
		}
	};
	return axios.post('http://localhost:4000/upload/chunk', formData, headerConfig);

axios请求返回一个promise,可以调用Promise.all方法在所有切片上传成功后执行拼接切片的请求

	Promise.all(requestList)
   	.then(() => {
		this.mergeChunk();
	})

拼接切片的请求比较简单,只要把文件名和切片大小发送给服务端,这里就不再展示代码了

到这里的话,前端已经把切片发送给服务端,接着我们来看服务端

node部分

node server这边通过express建立一个端口为4000的服务,需要提供两个接口,一个是/upload/chunk获取文件切片,还一个/upload/merge完成切片的拼接

/upload/chunk接口需要实现的主要有功能三个部分

  1. 首先在服务端upload文件夹里创建一个temp文件夹(作用就是存放切片),创建之前需要看这个文件夹是否被创建过,否则就跳过
	const tempDir = path.resolve(__dirname, '../upload/temp');
	if (!fs.existsSync(tempDir)) {
		fs.mkdirSync(tempDir)
	}
  1. 第二步通过multipart中间件处理请求发送的文件
	var multiparty = require('multiparty');
   	router.post('/upload/chunk', function(req, res, next) {
		const tempDir = path.resolve(__dirname, '../upload/temp');
		if (!fs.existsSync(tempDir)) {
			fs.mkdirSync(tempDir)
		}
		const form = new multiparty.Form();
		form.parse(req,function(err,fileds,files){
			const [hashname] = fileds.hashname;
			const [filename] = fileds.filename;
			const [chunk] = files.chunk;
		})
})

在中间件的parse方法的回调函数中可以拿到fileds(formData里存入的字段)

并且在files里拿到文件

  1. 最后是把切片内容写成文件存放在temp文件夹下,
	const chunkReadSteam = fs.createReadStream(chunk.path); // 创建切片的可读流
	const chunkPath = tempDir + '/' + hashname; // 定义chunk的存储位置
	fs.writeFileSync(chunkPath, null); // 在chunk的存储位置写一个空文件
	const chunkWriteSteam = fs.createWriteStream(chunkPath); // 创建空文件的可写流
	chunkReadSteam.pipe(chunkWriteSteam); // 通过管道将切片写入空文件

在这值得一提的是,在创建可读流和可写流之后,通过管道按照如图的形式把内容写入文件,对stream流有兴趣的朋友可以自行去了解

这三步完成后得到切片文件,可以发现总大小和上传的文件大小基本一致

到这里/upload/chunk接口已经成功实现,剩下的就是合并这些切片了,在理解了上面stream流得读写以后,实现起来也比较简单,就是在temp文件夹读取所有切片后,按文件名排序并写入同一个文件

  router.post('/upload/merge', function(req, res, next) {
      const tempDir = path.resolve(__dirname, '../upload/temp');
      const filename = req.body.filename;
      const chunkSize = req.body.chunkSize;
      const size = req.body.size;
      const filepath = path.resolve(tempDir, '../', filename);
      const fileChunkList = fs.readdirSync(tempDir).filter(name => name.match(new RegExp(filename))).sort((a, b) => a.split(
          '_')[1] - b.split('_')[1]);
          
      fs.writeFileSync(filepath, null);
      fileChunkList.forEach((name, index) => {
          const chunkReadStream = fs.createReadStream(tempDir + '/' + name);
          const fileWriteStream = fs.createWriteStream(filepath, {
              flags: 'w',
              start: index * chunkSize,
          })
          chunkReadStream.pipe(fileWriteStream);
      })
      res.send(JSON.stringify({
          msg: '合并成功'
      }));
  })

最后在文件夹惊喜的发现完整的缩略图

总结

在写的过程中也遇到了一些问题,查看了不少其他同类文章,最后还是小伙伴定位到了,主要还是基础不扎实导致的,比如blob文件的形式就是一个(binary),还有在请求头信息里的content-length可以看出发送请求数据的大小,这些知识都能让自己在出现问题的时候更快的定位到 好了,就写到这里,怪累人的,第一次写文章,以后希望能继续更新实践的内容和学习的心得吧