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

很多时候我们在处理文件上传时,如视频文件,小则几十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');
})

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

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

(0)

相关推荐

  • js实现图片上传并正常显示

    项目经常会用到头像上传,那么怎么实现呢? 首先是HTML布局: <label for="thumbnail" class="col-md-3 control-label">缩略图</label> <div class="col-md-6"> <input type="file" class="form-control" id="thumbnail"

  • 上传图片预览JS脚本 Input file图片预览的实现示例

    在深圳做项目的时候,需要一个用户上传头像预览的功能!是在网上找了好多,都不太满意.要么是flash的,要么是Ajax上传后返回图片路径的,要么压根就是不能用的.幸运的是在这个项目以前有人写过一个图片预览的功能,还被我给翻了出来,在这里做个记录,方便自己以后用,也方便其他需要的朋友! 代码很简单,如下: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/

  • Servlet+Jsp实现图片或文件的上传功能具体思路及代码

    现在不管是博客论坛还是企业办公,都离不开资源的共享.通过文件上传的方式,与大家同分享,从而达到大众间广泛的沟通和交流,我们既可以从中获得更多的知识和经验,也能通过他人的反馈达到自我改进和提升的目的. 下面我就为大家介绍 web项目中的这一上传功能,那么文件是如何从本地发送到服务器的呢?看我慢慢道来: 首先,我们创建一个新的web工程,在工程的WebRoot目录下新建一个upload文件夹,这样当我们将该工程部署到服务器上时,服务器便也生成个upload文件夹,用来存放上传的资源. 然后,在Web

  • JS实现上传图片的三种方法并实现预览图片功能

    在常见的用户注册页面,需要用户在本地选择一张图片作为头像,并同时预览. 常见的思路有两种:一是将图片上传至服务器的临时文件夹中,并返回该图片的url,然后渲染在html页面:另一种思路是,直接在本地内存中预览图片,用户确认提交后再上传至服务器保存. 这两种方法各有利弊,方法一很明显,浪费流量和服务器资源:方法二则加重了浏览器的负担,并且对浏览器的兼容性要求更高(在某些低版本中的IE浏览器不支持). 以下是实现上述思路的方法: 1. 模板文件 <!DOCTYPE html> <html l

  • JS中使用FormData上传文件、图片的方法

    关于FormData XMLHttpRequest Level 2添加了一个新的接口  ---- FormData 利用FormData对象,可以通过js用一些键值对来模拟一系列表单控件,可以使用XMLHttpRequest的 send( ) 方法来异步提交表单与普通的ajax相比,使用FormData的最大优点就是可以异步上传二进制文件 FormData对象 FormData对象,可以把所有表单元素的name与value组成一个queryString,提交到后台. 在使用ajax提交时,使用F

  • js 实现 input type="file" 文件上传示例代码

    在开发中,文件上传必不可少,<input type="file" /> 是常用的上传标签,但是它长得又丑.浏览的字样不能换,我们一般会用让,<input type="file" />隐藏,点其他的标签(图片等)来时实现选择文件上传功能. 看代码: 复制代码 代码如下: <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <he

  • js实现图片上传并预览功能

    本文为大家分享了js实现图片上传并预览的具体代码,供大家参考,具体内容如下 思路:完成这个功能,首先需要美化上传图片的按钮,然后添加一个<img/>标签,在图片上传之后,用新图片的src替换原来<img/>标签中的src. 如下图所示,是原始的按钮样式: 美化步骤: (1)将上传图片标签采用绝对定位,使之位于一个图片,按钮,div等标签上.或者给图片,按钮或div设置绝对定位,总之,是要让上传文件按钮和用户指定的按钮重合. (2)给上传图片标签设置大大小,使之和与它重叠的图片,按钮

  • 简单实现js上传文件功能

    本文实例为大家分享了js实现上传文件功能的具体代码,供大家参考,具体内容如下 一.用input完成上传,效果图如 选择文件后,提交后出现图片url 二.传输格式采用form-data形式. html代码 <form id="upload" enctype="multipart/form-data" method="post"> <input type="file" name="file"

  • js实现上传图片预览的方法

    本文实例讲述了js实现上传图片预览的方法.分享给大家供大家参考.具体实现方法如下: 复制代码 代码如下: function PreviewImage(imgFile) {     var filextension=imgFile.value.substring(imgFile.value.lastIndexOf("."),imgFile.value.length);     filextension=filextension.toLowerCase();     if ((filext

  • js获取上传文件的绝对路径实现方法

    在html中 <input type="file" id="importFile" /> <input type="button" onclick="upload()"/> <script> function upload() { var filename = document.getElementById("importFile").value; // 这时的filen

随机推荐