
前端:
① 全局变量
//切片数量
const chunkNumber = 10;
//用来存储文件
const uploadFileData={
file: {
name:'unknow'
}
}
② 获取文件
jsx:
<input type="file" onChange={(e)=>{getFlie(e)}} />
getFlie():通过 input(type=file) 获取文件
//通过 input(type=file) 获取文件
function getFlie(e:object) {
//获取文件队列的第一个文件
//写法等同于 const file = e.target.files[0]
const [file] = e.target.files;
if (!file) return;
uploadFileData.file = file;
}
③ 点击按钮,发送文件至server
端
jsx:
<Button onClick={()=>{uploadFile(fetchBigFileData,)}}>上传</Button>
uploadFile():获取文件切片集合,并将每片文件发送给server
端
// 获取文件切片集合,并将每片文件发送给`server`端
function uploadFile(fetchBigFileData:(item:object) => void,) {
if (!uploadFileData.file){
return
}
//获取切片集合
const fileChunkList = createFileChunk(uploadFileData.file,chunkNumber);
//将每一个切片封装进 obj,并发送给server
fileChunkList.forEach((item,index) => {
//没有用 json 的原因是读取 Blob 对象需要使用FileReader的readAsArrayBuffer解析读取,
//而使用 FormData 的最大优点就是可以存储二进制文件
//详情请参考:https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader/readAsArrayBuffer
// const obj:object={}
//这么写无效,文件流被序列化之后,传给 server 是空对象{}
//参考:https://www.jianshu.com/p/80e133a16d5e
// obj.chunk=item
// obj.hash=uploadFileData.file.name + "-" + index
// obj.fileName=uploadFileData.file.name
// obj.chunkNumber=chunkNumber+''
const obj=new FormData()
//二进制的片文件
obj.append('chunk',item)
//hash 码,标识每一个文件
obj.append('hash',uploadFileData.file.name + "-" + index)
//上传的文件名称
obj.append('fileName',uploadFileData.file.name)
//文件片数,方便后端标识并合并文件
obj.append('chunkNumber',chunkNumber+'')
//请求 server
fetchBigFileData(obj)
});
}
注意:
(1) 文件类型是Blob
,是二进制格式,参考:
https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader/readAsArrayBuffer
(2) 如果要用Json
格式发送片文件的话,需要使用FileReader
的readAsArrayBuffer
解析,参考:
使用js FormData传文件流,传json(重点)
为图方便,我们使用FormData
来直接存储并发送二进制文件。
(3) Object
可以存储Blob
类型的对象,但在传输的时候Blob
类型文件会被序列化成空对象{ }
(4) 后端知道切片上传是否完全的方式有两种:
第一种就是前端塞了chunkNumber
属性告知后端切片的数量,让后端自己去计算;
第二种是前端在切片请求都发完后,再发一个请求告知后端。
createFileChunk():生成文件切片,并返回切片集合
function createFileChunk(file: any, length: number = chunkNumber) {
const fileChunkList = [];
//向上取整
const chunkSize = Math.ceil(file.size / length);
let cur = 0;
while (cur < file.size) {
//将文件切成片
const fileChunk= file.slice(cur, cur + chunkSize)
//将每一片文件存进数组中(音频文件是Blob类型)
fileChunkList.push(fileChunk);
//是否继续循环的判断
cur += chunkSize;
}
return fileChunkList;
}
fetchBigFileData
:发起请求:
function fetchBigFileData(payload: object) {
dispatch({
type: "uploadFile/fetchBigFileData",
payload
});
}
*fetchBigFileData({ payload }, { call, put }) {
yield call(queryBigFileData, payload );
}
export async function queryBigFileData(payload:any) {
return request(`${API_SERVER}uploadbigfile`, {
method: "POST",
data: payload
});
}
由于用的 ant-design-pro,里面已封装好了,可能会对读者造成疑惑,但本质就是一个 post 请求,注意 body 类型是 form-data。
后端:
① 接口定义为/uploadbigfile
,为了方便,我们不连接数据库,直接将片文件存储在文件夹中
② 全局变量:
const {Router} = require('express');
const router = new Router();
const path = require("path");
const fse = require("fs-extra");
const multiparty = require("multiparty");
// 文件片的存储目录
const ChunkFileDir = path.resolve(__dirname, "../../", "uploadFile/chunkFile");
//合成的文件的存储目录
const TotalFileDir = path.resolve(__dirname, "../../", "uploadFile/totalFile");
let fileName = ''
let serverChunkNumber = 0
let clientChunkNumber = 0
let chunkDir = ''
注意:
由于需要解析FormData
格式的数据,并操作文件,需要安装multiparty
和fs-extra
npm i multiparty --save
npm i fs-extra --save
③ 在 POST 请求中接收并存储文件片:
//post 方法接收文件片
router.post("/", (req, res, next) => {
try {
//关于multiparty的讲解,请看:https://www.cnblogs.com/wangyinqian/p/7811719.html
const multipart = new multiparty.Form();
// 解析FormData数据
multipart.parse(req, (err, fields, files) => {
if (err) {
return;
}
//chunk:{
// path:存储临时文件的路径,
// size:临时文件的大小,
// }
const [chunk] = files.chunk;
const [hash] = fields.hash;
//获取切片总数量
clientChunkNumber = +fields.chunkNumber[0];
//获取文件名称
[fileName] = fields.fileName;
//本次文件的文件夹名称,如 xx/xx/uploadFile/chunkFile/梁博-出现又离开.mp3
chunkDir = `${ChunkFileDir}/${fileName}`;
// 切片目录不存在,创建切片目录chunkDir
if (!fse.existsSync(chunkDir)) {
fse.mkdirs(chunkDir);
}
//将每片文件移动进chunkDir下
fse.move(chunk.path, `${chunkDir}/${hash}`);
//server 端计算切片数量,
serverChunkNumber = serverChunkNumber + 1
//当到数时,自动合并文件
if (clientChunkNumber === serverChunkNumber) {
//这里方便测试,用 get 方法单独来 merge 文件
// mergeFileChunk(chunkDir)
serverChunkNumber = 0
}
//这么写返回 client 会出现乱码
// res.end("已接收文件片 "+hash);
res.status(200).json("已接收文件片 "+hash);
});
} catch (err) {
res.status(400).json(err)
}
});
注意:
(1) 关于multiparty
的讲解,请看:
https://www.cnblogs.com/wangyinqian/p/7811719.html
(2) /uploadFile/chunkFile
为存储切片文件的文件夹,/uploadFile/totalFile
为合成切片文件的文件夹

(3) 前端上传文件并发送请求后,会生成如下切片文件:



④ 在 GET 请求中合并文件片:
为方便测试,我们将uploadFile()
中的mergeFileChunk()
注释掉,写一个简单的GET
请求来调用mergeFileChunk()
//合并文件
router.get("/", async (req, res, next) => {
try {
mergeFileChunk(ChunkFileDir+'/'+fileName,TotalFileDir,fileName)
res.status(200).json("合并文件成功!");
} catch (err) {
res.status(400).json(err, 'err104')
}
});
mergeFileChunk():合并文件
// 合并切片
const mergeFileChunk =async (chunkDir,totalDir,fileName) => {
//指定合成的文件名及位置
const totalPaths=totalDir+'/'+'合成-'+fileName
// await fse.writeFile(totalDir+'/'+'合成-'+fileName,'')
//
// const chunkPaths=await new Promise((resolve, reject)=>{
// console.log(chunkDir,'chunkDir26')
// fse.readdir(chunkDir,(err, chunkPaths) => {
// if(err){
// reject(err)
// }
// resolve(chunkPaths)
// //也可以直接在这里循环
//
//
// })
// })
//生成合成的空文件
fse.writeFileSync(totalPaths,'')
//注意:readFile是异步方法,有 callback,同步的方法-readFileSync 没有回调
// fse.readFile(`${chunkDir}/${chunkPath}`,(err,data)=>{
// fse.appendFileSync(a+'/a.mp3', data);
// })
//读取切片文件目录,返回切片文件集合
const chunkPaths=fse.readdirSync(chunkDir)
//循环读取切片文件内容并合并进totalPaths中
chunkPaths.forEach(chunkPath => {
//获取单个切片文件目录
const chunkFilePath=`${chunkDir}/${chunkPath}`
//xxx/xxx/uploadFile/chunkFile/梁博-出现又离开.mp3-0
//同步按顺序读取文件切片,这样才能保证是按顺序将切片合成一整首歌
const data = fse.readFileSync(chunkFilePath)
//xxx/xxx/uploadFile/totalFile/梁博-出现又离开.mp3
//将每个文件片合进单一文件中
fse.appendFileSync(totalPaths, data);
//删除文件
// fse.unlinkSync(chunkFilePath);
});
//删除切片的目录
// fse.rmdirSync(chunkDir);
}
注意:
(1) 如果不调用readFileSync()
,而是readFile()
的话,会导致合成的文件顺序错误:

(2) writeFileSync
和writeFile
/readdirSync
和readdir
它们的区别是:
writeFile
和readdir
是异步方法,它们可以直接获取到方法返回的结果
writeFileSync
和readdirSync
是同步方法,它们是一个Promise
对象,必须在callback
中才能获取结果
const chunkPaths=fse.readdirSync(chunkDir)
等同于
const chunkPaths=await new Promise((resolve, reject)=>{
fse.readdir(chunkDir,(err, chunkPaths) => {
if(err){
reject(err)
}
resolve(chunkPaths)
//也可以直接在这里循环
})
})
(3) 执行get
请求

合并结果

打开音乐播放器听听吧。

(完)
网友评论