实现大文件切片上传,前端+nodejs后端

939 阅读1分钟

最近摸鱼有点多,参考网上资料重写了一份大文件切片上传的demo,包括nodejs的后端部分

本文内容只实现了基础的前端切片、后端接收切片与将多个切片合并成文件的代码。

前端代码实现

  1. 选择文件
 <input ref="inputRef" type="file" @change="fileChange"    >
const file = ref<File>()
const fileChange = function(events:Event) {
  file.value = events.target.files[0]
}
  1. 文件切片
//核心代码:
file.slice(startIndex,endIndex)

完整代码

  • 这里默认了100kb为一片的大小,实际情况根据文件大小灵活修改
const sliceFile = function() {
  if (!file.value) {
    console.error('no file')
    return
  }
  const chunkSize100KB = 100 * 1024
  const totalSize = file.value.size
  let curIndex = 0
  chunkArray.value = []
  while (curIndex <= totalSize) {
    const startIndex = curIndex
    const endIndex = startIndex + chunkSize100KB
    chunkArray.value.push({
      startIndex,
      endIndex,
      fileName: file.value.name,
      type: file.value.type,
      chunk: file.value.slice(startIndex, endIndex)
    })
    curIndex += chunkSize100KB
  }
  console.error('分片结果', chunkArray.value)
}

文件切片后效果 image.png 3. 分片上传
因为是个demo,所以代码中处于方便每次只上传了一块,且通过时间戳来判断是否是同一个文件(偷偷懒)

const uploadFile = async function() {
  const timeStamp = '' + Date.now()
  for (let i = 0; i < chunkArray.value.length; i++) {
    await uploadPart(timeStamp, i, i === chunkArray.value.length - 1)
  }
}
//上传某一部分的文件
const uploadPart = async function(timeStamp, chunkArrayIndex, last = false) {
  const fileFormData = new FormData()
  fileFormData.append('fileContent', chunkArray.value[chunkArrayIndex].chunk)
  fileFormData.append('timeStamp', timeStamp)
  if (last) {
    fileFormData.append('last', 'true')
    fileFormData.append('fileName', chunkArray.value[chunkArrayIndex].fileName)
  }
  return http.post(`/file-upload-slice`, fileFormData)
}

//http.ts
import axios from 'axios'
const instance = axios.create({
  baseURL: 'http://127.0.0.1:1234'
})

export default instance

上传切片文件调用示例。300+KB的文件分成四四个接口上传,最后一个会带上last标识

image.png

后端代码实现

  1. 利用http启动基本的后台服务
    注意,因为demo中会有跨域,所以Access-Control-Allow-Origin:*不可缺少
import api from '../api/index'
let prefix="/node-test" // 在服务端的prefix
var http = require('http');


//创建一个服务器对象
var server = http.createServer(function (req, res) {
  let url = req.url.replace(prefix,'')
  let method = req.method
  if (api[url] && api[url].methods.includes(method)) {
    res.writeHeader(200, {
      'Access-Control-Allow-Origin': '*'
    });
    api[url].handle(...arguments)
  } else {
    api['/request404'].handle(...arguments)
  }
  return

});

//让服务器监听本地8000端口开始运行
server.listen(1234, '127.0.0.1');
console.log("server is runing at 127.0.0.1:1234");
  1. 具体切片接口处理
    • 利用formidable接收前端传入的formData。其中单个文件的切片数据全部记录在filesMap[timeStamp]中,当然这里也是为了简化demo才这么处理的
    • 接收到的切片文件存放在STATIC_PATH = path.join(__dirname, '../../uploadFileStorage')
const formidable = require('formidable')
const path = require('path')
const fs = require('fs')
const STATIC_PATH = path.join(__dirname, '../../uploadFileStorage')
const filesMap = {}


export default {
  name: 'file-upload-slice',
  url: '/file-upload-slice',
  methods: ['GET', 'POST', 'OPTIONS'],
  handle: function (req, res) {
    if (!fs.existsSync(STATIC_PATH)) {
      fs.mkdirSync(STATIC_PATH)
    }
    const form = formidable({ multiples: true, uploadDir: STATIC_PATH })
    form.parse(req, (err, fields, files) => {
      const timeStamp = fields.timeStamp
      res.writeHeader(200, {
        'Access-Control-Allow-Headers': '*',
        'Access-Control-Allow-Origin': '*'
      });
      if (!filesMap[timeStamp]) {
        filesMap[timeStamp] = []
      }
      filesMap[timeStamp].push(files.fileContent.newFilename)
      if (fields.last) {
        mergeFile(filesMap[timeStamp], fields.fileName)
      }
      let json = JSON.stringify({ name: "hahahn", fileContent: files.fileContent })
      res.end(json);
    })

  }
}

分片后被存储的文件

image.png

  • 接下来就是将被切片的文件合并
    1. 依次读取各文件的buffer数据
    2. 利用Buffer.concat(buffers)合并分片后的buffer
    3. 利用fs.writeFile()生成合并后的文件,并删除切片的小文件
const {  Buffer } = require('node:buffer')
const fs = require('fs')

function mergeFile(files, fileName) {
  const buffers = files.map(e => fs.readFileSync(path.join(STATIC_PATH, `${e}`)))
  fs.writeFile(path.join(STATIC_PATH, fileName), Buffer.concat(buffers), {
    encoding: 'utf-8'
  }, (err) => {
    if (!err) {
      //合并成功后删除分块的文件
      files.forEach(e => {
        fs.rmSync(path.join(STATIC_PATH, `${e}`))
      })
    } else {
      console.error('合并错误', err);
    }
  })
}

最终结果

image.png
测试2:用一个70+MB的文件测试,一样能合并成需要的文件(100KB为一份,分成了七百多个请求)

image.png

TODO

由于是demo,且重点本身放在了后端上,因此很多地方都简化了,如:

  • 切片的尺寸写死了
  • 文件来源利用了timeStamp
  • 没有加入断点续传功能(后端完全能通过文件是否上传成功,给前端返回上传状态的数据)
  • 上传文件时一次只上传了一块,可以改成多块