前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React+NodeJs实现文件切片上传

React+NodeJs实现文件切片上传

作者头像
进击的小进进
发布2020-02-24 11:47:25
2.8K5
发布2020-02-24 11:47:25
举报

前端:

① 全局变量

代码语言:javascript
复制
//切片数量
const chunkNumber = 10;
//用来存储文件
const uploadFileData={
    file: {
      name:'unknow'
    }
}

② 获取文件 jsx

代码语言:javascript
复制
 <input type="file" onChange={(e)=>{getFlie(e)}} />

getFlie():通过 input(type=file) 获取文件

代码语言:javascript
复制
//通过 input(type=file) 获取文件
function getFlie(e:object) {
  //获取文件队列的第一个文件
  //写法等同于 const file = e.target.files[0]
  const [file] = e.target.files;
  if (!file) return;
  uploadFileData.file = file;
}

③ 点击按钮,发送文件至serverjsx

代码语言:javascript
复制
<Button onClick={()=>{uploadFile(fetchBigFileData,)}}>上传</Button>

uploadFile():获取文件切片集合,并将每片文件发送给server

代码语言:javascript
复制
// 获取文件切片集合,并将每片文件发送给`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格式发送片文件的话,需要使用FileReaderreadAsArrayBuffer解析,参考: 使用js FormData传文件流,传json(重点)(https://www.jianshu.com/p/80e133a16d5e)

为图方便,我们使用FormData来直接存储并发送二进制文件。

(3) Object可以存储Blob类型的对象,但在传输的时候Blob类型文件会被序列化成空对象{ }

(4) 后端知道切片上传是否完全的方式有两种: 第一种就是前端塞了chunkNumber属性告知后端切片的数量,让后端自己去计算; 第二种是前端在切片请求都发完后,再发一个请求告知后端。

createFileChunk():生成文件切片,并返回切片集合

代码语言:javascript
复制
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:发起请求:

代码语言:javascript
复制
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,为了方便,我们不连接数据库,直接将片文件存储在文件夹中

② 全局变量:

代码语言:javascript
复制
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格式的数据,并操作文件,需要安装multipartyfs-extra

代码语言:javascript
复制
npm i multiparty --save
npm i fs-extra --save

③ 在 POST 请求中接收并存储文件片:

代码语言:javascript
复制
//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()

代码语言:javascript
复制
//合并文件
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():合并文件

代码语言:javascript
复制
// 合并切片
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) writeFileSyncwriteFile/readdirSyncreaddir 它们的区别是: writeFilereaddir异步方法,它们可以直接获取到方法返回的结果 writeFileSyncreaddirSync同步方法,它们是一个Promise对象,必须在callback中才能获取结果

代码语言:javascript
复制
const chunkPaths=fse.readdirSync(chunkDir)

等同于

代码语言:javascript
复制
const chunkPaths=await new Promise((resolve, reject)=>{
    fse.readdir(chunkDir,(err, chunkPaths) => {
      if(err){
        reject(err)
      }
      resolve(chunkPaths)
      //也可以直接在这里循环
    })
  })

(3) 执行get请求

合并结果

打开音乐播放器听听吧。


小进进还没开通留言功能,觉得不错的话,点「在看」、转发朋友圈都是一种支持 (●'◡'●)ノ

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-01-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 webchen 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前端:
  • 后端:
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档