欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JavaScript实现大文件分片上传处理

程序员文章站 2022-06-22 10:05:54
很多时候我们在处理文件上传时,如视频文件,小则几十m,大则 1g+,以一般的http请求发送数据的方式的话,会遇到的问题:1、文件过大,超出服务端的请求大小限制;2、请求时间过长,请求超时;3、传输中...

很多时候我们在处理文件上传时,如视频文件,小则几十m,大则 1g+,以一般的http请求发送数据的方式的话,会遇到的问题:

1、文件过大,超出服务端的请求大小限制;
2、请求时间过长,请求超时;
3、传输中断,必须重新上传导致前功尽弃

这些问题很影响用户的体验感,所以下面介绍一种基于原生javascript进行文件分片处理上传的方案,具体实现过程如下:

1、通过dom获取文件对象,并且对文件进行md5加密(文件内容+文件标题形式),采用sparkmd5进行文件加密;
2、进行分片设置,文件file基于blob, 继承了blob的功能,可以把file当成blob的子类,利于blob的slice方法进行文件分片处理,并且依次进行上传
3、分片文件上传完成后,请求合并接口后端进行文件合并处理即可

1. 上传文件页面

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <title>文件上传</title>
  <script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
  <script src="https://code.jquery.com/jquery-3.4.1.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.0/spark-md5.js"></script>
  <style>
    /* 自定义进度条样式 */
    .precent input[type=range] {
      -webkit-appearance: none;
      /*清除系统默认样式*/
      width: 7.8rem;
      /* background: -webkit-linear-gradient(#ddd, #ddd) no-repeat, #ddd; */
      /*设置左边颜色为#61bd12,右边颜色为#ddd*/
      background-size: 75% 100%;
      /*设置左右宽度比例*/
      height: 0.6rem;
      /*横条的高度*/
      border-radius: 0.4rem;
      border: 1px solid #ddd;
      box-shadow: 0 0 10px rgba(0,0,0,.125) inset ;
    }

    /*拖动块的样式*/
    .precent input[type=range]::-webkit-slider-thumb {
      -webkit-appearance: none;
      /*清除系统默认样式*/
      height: .9rem;
      /*拖动块高度*/
      width: .9rem;
      /*拖动块宽度*/
      background: #fff;
      /*拖动块背景*/
      border-radius: 50%;
      /*外观设置为圆形*/
      border: solid 1px #ddd;
      /*设置边框*/
    }

  </style>
</head>

<body>
  <h1>大文件分片上传测试</h1>
  <div>
    <input id="file" type="file" name="avatar" />
    <div style="padding: 10px 0;">
      <input id="submitbtn" type="button" value="提交" />
      <input id="pausebtn" type="button" value="暂停" />
    </div>
    <div class="precent">
      <input type="range" value="0" /><span id="precentval">0%</span>
    </div>
  </div>
  <script type="text/javascript" src="./js/index.js"></script>
</body>

</html>

2. 大文件分片上传处理

$(document).ready(() => {
  const submitbtn = $('#submitbtn');  //提交按钮
  const precentdom = $(".precent input")[0]; // 进度条
  const precentval = $("#precentval");  // 进度条值对应dom
  const pausebtn = $('#pausebtn');  // 暂停按钮
  // 每个chunk的大小,设置为1兆
  const chunksize = 1 * 1024 * 1024;
  // 获取slice方法,做兼容处理
  const blobslice = file.prototype.slice || file.prototype.mozslice || file.prototype.webkitslice;
  // 对文件进行md5加密(文件内容+文件标题形式)
  const hashfile = (file) => {
    return new promise((resolve, reject) => {
      const chunks = math.ceil(file.size / chunksize);
      let currentchunk = 0;
      const spark = new sparkmd5.arraybuffer();
      const filereader = new filereader();
      function loadnext() {
        const start = currentchunk * chunksize;
        const end = start + chunksize >= file.size ? file.size : start + chunksize;
        filereader.readasarraybuffer(blobslice.call(file, start, end));
      }
      filereader.onload = e => {
        spark.append(e.target.result); // append array buffer
        currentchunk += 1;
        if (currentchunk < chunks) {
          loadnext();
        } else {
          console.log('finished loading');
          const result = spark.end();
          // 通过内容和文件名称进行md5加密
          const sparkmd5 = new sparkmd5();
          sparkmd5.append(result);
          sparkmd5.append(file.name);
          const hexhash = sparkmd5.end();
          resolve(hexhash);
        }
      };
      filereader.onerror = () => {
        console.warn('文件读取失败!');
      };
      loadnext();
    }).catch(err => {
      console.log(err);
    });
  }

  // 提交
  submitbtn.on('click', async () => {
    var pausestatus = false;
    var nowuploadnums = 0
    // 1.读取文件
    const filedom = $('#file')[0];
    const files = filedom.files;
    const file = files[0];
    if (!file) {
      alert('没有获取文件');
      return;
    }
    // 2.设置分片参数属性、获取文件md5值
    const hash = await hashfile(file); //文件 hash 
    const blockcount = math.ceil(file.size / chunksize); // 分片总数
    const axiospromisearray = []; // axiospromise数组
    // 文件上传
    const uploadfile = () => {
      const start = nowuploadnums * chunksize;
      const end = math.min(file.size, start + chunksize);
      // 构建表单
      const form = new formdata();
      // blobslice.call(file, start, end)方法是用于进行文件分片
      form.append('file', blobslice.call(file, start, end));
      form.append('index', nowuploadnums);
      form.append('hash', hash);
      // ajax提交 分片,此时 content-type 为 multipart/form-data
      const axiosoptions = {
        onuploadprogress: e => {
          nowuploadnums++;
          // 判断分片是否上传完成
          if (nowuploadnums < blockcount) {
            setprecent(nowuploadnums, blockcount);
            uploadfile(nowuploadnums)
          } else {
            // 4.所有分片上传后,请求合并分片文件
            axios.all(axiospromisearray).then(() => {
              setprecent(blockcount, blockcount); // 全部上传完成
              axios.post('/file/merge_chunks', {
                name: file.name,
                total: blockcount,
                hash
              }).then(res => {
                console.log(res.data, file);
                pausestatus = false;
                alert('上传成功');
              }).catch(err => {
                console.log(err);
              });
            });
          }
        },
      };
      // 加入到 promise 数组中
      if (!pausestatus) {
        axiospromisearray.push(axios.post('/file/upload', form, axiosoptions));
      }

    }
    // 设置进度条
    function setprecent(now, total) {
      var prencentvalue = ((now / total) * 100).tofixed(2)
      precentdom.value = prencentvalue
      precentval.text(prencentvalue + '%')
      precentdom.style.csstext = `background:-webkit-linear-gradient(top, #059cfa, #059cfa) 0% 0% / ${prencentvalue}% 100% no-repeat`
    }
    // 暂停
    pausebtn.on('click', (e) => {
      pausestatus = !pausestatus;
      e.currenttarget.value = pausestatus ? '开始' : '暂停'
      if (!pausestatus) {
        uploadfile(nowuploadnums)
      }
    })
    uploadfile();
  });
})

3. 文件上传和合并分片文件接口(node)

const router = require('koa-router');
const multer = require('koa-multer');
const fs = require('fs-extra');
const path = require('path');
const router = new router();

const { mkdirssync } = require('../utils/dir');
const uploadpath = path.join(__dirname, 'upload');
const chunkuploadpath = path.join(uploadpath, 'temp');
const upload = multer({ dest: chunkuploadpath });

// 文件上传接口
router.post('/file/upload', upload.single('file'), async (ctx, next) => {
  const { index, hash } = ctx.req.body;
  const chunkspath = path.join(chunkuploadpath, hash, '/');
  if(!fs.existssync(chunkspath)) mkdirssync(chunkspath);
  fs.renamesync(ctx.req.file.path, chunkspath + hash + '-' + index);
  ctx.status = 200;
  ctx.res.end('success');
}) 
// 合并分片文件接口
router.post('/file/merge_chunks', async (ctx, next) => {
  const { name, total, hash } = ctx.request.body;
  const chunkspath = path.join(chunkuploadpath, hash, '/');
  const filepath = path.join(uploadpath, name);
  // 读取所有的chunks
  const chunks = fs.readdirsync(chunkspath);
  // 创建存储文件
  fs.writefilesync(filepath, ''); 
  if(chunks.length !== total || chunks.length === 0) {
    ctx.status = 200;
    ctx.res.end('切片文件数量不符合');
    return;
  }
  for (let i = 0; i < total; i++) {
    // 追加写入到文件中
    fs.appendfilesync(filepath, fs.readfilesync(chunkspath + hash + '-' +i));
    // 删除本次使用的chunk    
    fs.unlinksync(chunkspath + hash + '-' +i);
  }
  fs.rmdirsync(chunkspath);
  // 文件合并成功,可以把文件信息进行入库。
  ctx.status = 200;
  ctx.res.end('success');
})

以上就是文件分片上传的基本过程,过程中加入了上传进度条、暂停和开始上传操作,见

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

相关标签: js 上传