美文网首页
React+NodeJs实现文件切片上传

React+NodeJs实现文件切片上传

作者: 小进进不将就 | 来源:发表于2020-01-29 23:22 被阅读0次

    前端:
    ① 全局变量

    //切片数量
    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格式发送片文件的话,需要使用FileReaderreadAsArrayBuffer解析,参考:
    使用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格式的数据,并操作文件,需要安装multipartyfs-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) writeFileSyncwriteFile/readdirSyncreaddir
    它们的区别是:
    writeFilereaddir异步方法,它们可以直接获取到方法返回的结果
    writeFileSyncreaddirSync同步方法,它们是一个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请求

    合并结果

    打开音乐播放器听听吧。


    (完)

    相关文章

      网友评论

          本文标题:React+NodeJs实现文件切片上传

          本文链接:https://www.haomeiwen.com/subject/jjsezctx.html