面试题中的大文件上传实现之后端处理

238 阅读3分钟

前言

    书接上回,我们实现了大文件上传前端数据的数据,在本文中将会把后端内容打造后以及完善前端代码,话不多说,各位请看下文

正文

    在前端我们通过axios发送接口请求,并将数据流一表单类型的数据传给后端,那么我们后端就需要拿到前端传输的数据以及将切片合并成完整的数据流.

首先我们先引入http模块,创建一个WebServer于3000端口上.这里有一点非常的重要,就是前端发送请求给后端,这一个过程是会发生跨域的,这里我们使用Cors解决跨域问题,那么也就是直接在后端的响应头中添加字段.

res.writeHead(200, {
    'access-control-allow-origin': '*',
    'access-control-allow-headers': '*',
    'access-control-allow-methods': '*'
  })

下一步就是将前端传过来的数据处理成对象并将对象存起来,这时我们就需要第三方库中的模块了.multiparty将前端传过来的数据转成对象,fs-extrafs系统的扩展,方法会更多一点.

if (req.url === '/upload') {
    const form = new multiparty.Form();
    form.parse(req, (err, fields, files) => {
      // console.log(fields);  // 切片的描述
      // console.log(files);  // 切片的二进制资源被处理成对象
      const [file] = files.file
      const [fileName] = fields.fileName
      const [chunkName] = fields.chunkName
      // 保存切片
      const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`)
      if (!fse.existsSync(chunkDir)) { // 该路径是否有效
        fse.mkdirSync(chunkDir)
      }
      // 存入
      fse.moveSync(file.path, `${chunkDir}/${chunkName}`)

      res.end(JSON.stringify({
        code: 0,
        message: '切片上传成功'
      }))
      
    })
  }

判断是否是发送到/upload接口的请求,如果是的话就会开始将数据存起来.我们来分别看看参数fieldsfiles长什么样. 这是fields也就是对于切片信息的描述

image.png

这是files切片被转成对象

image.png

有几个切片就会有几个这样的对象,然后将这个对象存入提前创建的目录下. 那么最后一个操作就是合并数据了.那么什么时候合并呢?这一点肯定是当切片全部发送完毕再发送信息告诉后端可以合并切片了.所以我们还需要在前端发送一个合并的接口请求.

//合并请求
        function mergeChunks(size = 5 * 1024 * 1024){
            console.log(fileObj);
            
            axios.post('http://localhost:3000/merge',{
                fileName: fileObj.name,
                size
            }).then(res=>{
                console.log(`${fileObj.name}合并成功`);
            })
        }

什么时候调用这个方法呢,我们知道在前端我们写了一个Promise.all()当里面的数组的中元素都完成就会执行,所以我们合并请求调用也是在这里.

image.png

后端部分也肯定是需要判断是不是发送到/merge的请求

if (req.url === '/merge') {
    const { fileName, size } = await resolvePost(req) 
    const filePath = path.resolve(UPLOAD_DIR, fileName) 
    const result = await mergeFileChunk(filePath, fileName, size)
    console.log(result);
    if(result){
        res.end(JSON.stringify({
            code: 0,
            message: '文件合并完成'
        }))
    }
  }

打造了两个方法resolvePost()用于处理数据,将数据转成对象

function resolvePost(req) {
  return new Promise((resolve, reject) => {
    req.on('data', (data) => {
      resolve(JSON.parse(data.toString()))
    })
  })
}

方法mergeFileChunk()它的功能就是将切片合并

async function mergeFileChunk(filePath, fileName, size) {
  // 拿到所有切片所在文件夹的路径
  const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`)
  // 拿到所有切片
  let chunksList = fse.readdirSync(chunkDir)
  console.log(chunksList);
  //万一切片是乱序的 排序
  chunksList.sort((a,b)=>a.split('-')[1]-b.split('-')[1])
  const result = chunksList.map((chunkFileName,index)=>{
    const chunkPath = path.resolve(chunkDir,chunkFileName)
    // console.log(chunkPath);
    //数据合并  创建流
    return pipeStream(chunkPath,fse.createWriteStream(filePath,{
        start: index * size,
        end:(index + 1) * size
    }))
  })
//   console.log(result);
  await Promise.all(result)
  fse.rmdirSync(chunkDir)//删除切片目录
  return true

}

我们拿到所有切片的名字chunksList

image.png

然后我们需要对其进行一个排序,大家会很好奇这里不是排好序了吗为什么还要排序,由于我们这里数据是本地文件不容器出错,如果我们需要的不是本地文件而是其他地方的文件,就会有这个问题,我们不能保证切片是否依旧是按顺序获取到的,所以我们这里就安全处理一下,防止切片合并完后并不是源文件.下一步我们就是需要将切边变成数据流合并起来.所以在外部也打造了一个pipeStream()方法用于合并数据流.

function pipeStream(path,writeStream){
    return new Promise((resolve,reject)=>{
        const readStream = fse.createReadStream(path)
        readStream.on('end',()=>{
            //移除掉被读取完的切片
            fse.removeSync(path)
            resolve()
        })
        //汇入到创建的可写流中
        readStream.pipe(writeStream)
    })
}

完整代码:

const http = require('http');
const path = require('path');
const multiparty = require('multiparty');
const fse = require('fs-extra');

// 存放切片的地方
const UPLOAD_DIR = path.resolve(__dirname, '.', 'chunk')

// 解析post参数
function resolvePost(req) {
  return new Promise((resolve, reject) => {
    req.on('data', (data) => {
      resolve(JSON.parse(data.toString()))
    })
  })
}

//合并
function pipeStream(path,writeStream){
    return new Promise((resolve,reject)=>{
        const readStream = fse.createReadStream(path)
        readStream.on('end',()=>{
            //移除掉被读取完的切片
            fse.removeSync(path)
            resolve()
        })
        //汇入到创建的可写流中
        readStream.pipe(writeStream)
    })
}


// 合并切片
async function mergeFileChunk(filePath, fileName, size) {
  // 拿到所有切片所在文件夹的路径
  const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`)
  // 拿到所有切片
  let chunksList = fse.readdirSync(chunkDir)
  console.log(chunksList);
  //万一切片是乱序的 排序
  chunksList.sort((a,b)=>a.split('-')[1]-b.split('-')[1])
  const result = chunksList.map((chunkFileName,index)=>{
    const chunkPath = path.resolve(chunkDir,chunkFileName)
    // console.log(chunkPath);
    //数据合并  创建流
    return pipeStream(chunkPath,fse.createWriteStream(filePath,{
        start: index * size,
        end:(index + 1) * size
    }))
  })
//   console.log(result);
  await Promise.all(result)
  fse.rmdirSync(chunkDir)//删除切片目录
  return true

}

const server = http.createServer( async (req, res) => {
  res.writeHead(200, {
    'access-control-allow-origin': '*',
    'access-control-allow-headers': '*',
    'access-control-allow-methods': '*'
  })
  if (req.method === 'OPTIONS') { // 请求预检
    res.status = 200
    res.end()
    return
  }

  if (req.url === '/upload') {
    // 接收前端传过来的 formData 
    // req.on('data', (data) => {
    //   console.log(data);
    // })
    const form = new multiparty.Form();
    form.parse(req, (err, fields, files) => {
      // console.log(fields);  // 切片的描述
      console.log(files);  // 切片的二进制资源被处理成对象
      const [file] = files.file
      const [fileName] = fields.fileName
      const [chunkName] = fields.chunkName
      // 保存切片
      const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`)
      if (!fse.existsSync(chunkDir)) { // 该路径是否有效
        fse.mkdirSync(chunkDir)
      }
      // 存入
      fse.moveSync(file.path, `${chunkDir}/${chunkName}`)

      res.end(JSON.stringify({
        code: 0,
        message: '切片上传成功'
      }))
      
    })

  }

  if (req.url === '/merge') {
    const { fileName, size } = await resolvePost(req) // 解析post参数
    const filePath = path.resolve(UPLOAD_DIR, fileName)  // 完整文件的路径
    // 合并切片
    const result = await mergeFileChunk(filePath, fileName, size)
    console.log(result);
    if(result){
        res.end(JSON.stringify({
            code: 0,
            message: '文件合并完成'
        }))
    }
  }
})

server.listen(3000, () => {
  console.log('listening on port 3000');
})

总结

大文件上传后端需要的操作就比较复杂了,首先是需要获取到前端传过来的切片数据,然后再将切片数据保存在某一个文件夹下面,当接收到前端发送的合并请求,就开始进行合并操作.创建文件可写流,将所有的切片读成流类型并汇入到可写流中得到完整的文件资源。这样就实现了数据流的合并