基于Node.js的大文件分片上传示例


Posted in Javascript onJune 19, 2019

我们在做文件上传的时候,如果文件过大,可能会导致请求超时的情况。所以,在遇到需要对大文件进行上传的时候,就需要对文件进行分片上传的操作。同时如果文件过大,在网络不佳的情况下,如何做到断点续传?也是需要记录当前上传文件,然后在下一次进行上传请求的时候去做判断。

先上代码:代码仓库地址

前端

1. index.html

<!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="./spark-md5.min.js"></script>

  <script>

    $(document).ready(() => {
      const chunkSize = 1 * 1024 * 1024; // 每个chunk的大小,设置为1兆
      // 使用Blob.slice方法来对文件进行分割。
      // 同时该方法在不同的浏览器使用方式不同。
      const blobSlice =
        File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;

      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();
              // 如果单纯的使用result 作为hash值的时候, 如果文件内容相同,而名称不同的时候
              // 想保留两个文件无法保留。所以把文件名称加上。
              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);
        });
      }

      const submitBtn = $('#submitBtn');
      submitBtn.on('click', async () => {
        const fileDom = $('#file')[0];
        // 获取到的files为一个File对象数组,如果允许多选的时候,文件为多个
        const files = fileDom.files;
        const file = files[0];
        if (!file) {
          alert('没有获取文件');
          return;
        }
        const blockCount = Math.ceil(file.size / chunkSize); // 分片总数
        const axiosPromiseArray = []; // axiosPromise数组
        const hash = await hashFile(file); //文件 hash 
        // 获取文件hash之后,如果需要做断点续传,可以根据hash值去后台进行校验。
        // 看看是否已经上传过该文件,并且是否已经传送完成以及已经上传的切片。
        console.log(hash);
        
        for (let i = 0; i < blockCount; i++) {
          const start = i * chunkSize;
          const end = Math.min(file.size, start + chunkSize);
          // 构建表单
          const form = new FormData();
          form.append('file', blobSlice.call(file, start, end));
          form.append('name', file.name);
          form.append('total', blockCount);
          form.append('index', i);
          form.append('size', file.size);
          form.append('hash', hash);
          // ajax提交 分片,此时 content-type 为 multipart/form-data
          const axiosOptions = {
            onUploadProgress: e => {
              // 处理上传的进度
              console.log(blockCount, i, e, file);
            },
          };
          // 加入到 Promise 数组中
          axiosPromiseArray.push(axios.post('/file/upload', form, axiosOptions));
        }
        // 所有分片上传后,请求合并分片文件
        await axios.all(axiosPromiseArray).then(() => {
          // 合并chunks
          const data = {
            size: file.size,
            name: file.name,
            total: blockCount,
            hash
          };
          axios
            .post('/file/merge_chunks', data)
            .then(res => {
              console.log('上传成功');
              console.log(res.data, file);
              alert('上传成功');
            })
            .catch(err => {
              console.log(err);
            });
        });
      });

    })
    
    window.onload = () => {
    }

  </script>

</head>
<body>
  <h1>大文件上传测试</h1>
  <section>
    <h3>自定义上传文件</h3>
    <input id="file" type="file" name="avatar"/>
    <div>
      <input id="submitBtn" type="button" value="提交">
    </div>
  </section>

</body>
</html>

2. 依赖的文件
axios.js
jquery
spark-md5.js

后端

1. app.js

const Koa = require('koa');
const app = new Koa();
const Router = require('koa-router');
const multer = require('koa-multer');
const serve = require('koa-static');
const path = require('path');
const fs = require('fs-extra');
const koaBody = require('koa-body');
const { mkdirsSync } = require('./utils/dir');
const uploadPath = path.join(__dirname, 'uploads');
const uploadTempPath = path.join(uploadPath, 'temp');
const upload = multer({ dest: uploadTempPath });
const router = new Router();
app.use(koaBody());
/**
 * single(fieldname)
 * Accept a single file with the name fieldname. The single file will be stored in req.file.
 */
router.post('/file/upload', upload.single('file'), async (ctx, next) => {
  console.log('file upload...')
  // 根据文件hash创建文件夹,把默认上传的文件移动当前hash文件夹下。方便后续文件合并。
  const {
    name,
    total,
    index,
    size,
    hash
  } = ctx.req.body;

  const chunksPath = path.join(uploadPath, 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 {
    size, name, total, hash
  } = ctx.request.body;
  // 根据hash值,获取分片文件。
  // 创建存储文件
  // 合并
  const chunksPath = path.join(uploadPath, 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('合并成功');
})
app.use(router.routes());
app.use(router.allowedMethods());
app.use(serve(__dirname + '/static'));
app.listen(9000);

2. utils/dir.js

const path = require('path');
const fs = require('fs-extra');
const mkdirsSync = (dirname) => {
  if(fs.existsSync(dirname)) {
    return true;
  } else {
    if (mkdirsSync(path.dirname(dirname))) {
      fs.mkdirSync(dirname);
      return true;
    }
  }
}
module.exports = {
  mkdirsSync
};

操作步骤说明

服务端的搭建

我们以下的操作都是保证在已经安装node以及npm的前提下进行。node的安装以及使用可以参考官方网站。

1、新建项目文件夹file-upload

2、使用npm初始化一个项目:cd file-upload && npm init

3、安装相关依赖

npm i koa
  npm i koa-router --save  // Koa路由
  npm i koa-multer --save  // 文件上传处理模块
  npm i koa-static --save  // Koa静态资源处理模块
  npm i fs-extra --save   // 文件处理
  npm i koa-body --save   // 请求参数解析

4、创建项目结构

file-upload
    - static
      - index.html
      - spark-md5.min.js
    - uploads
      - temp
    - utils
      - dir.js
    - app.js

5、复制相应的代码到指定位置即可

6、项目启动:node app.js (可以使用 nodemon 来对服务进行管理)

7、访问:http://localhost:9000/index.html

其中细节部分代码里有相应的注释说明,浏览代码就一目了然。

后续延伸:断点续传、多文件多批次上传

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

Javascript 相关文章推荐
javascript 进阶篇1 正则表达式,cookie管理,userData
Mar 14 Javascript
js实现在字符串中提取数字
Nov 05 Javascript
通过pjax实现无刷新翻页(兼容新版jquery)
Jan 31 Javascript
jquery实现的导航固定效果
Apr 28 Javascript
JavaScript前端图片加载管理器imagepool使用详解
Dec 29 Javascript
JS实现常见的TAB、弹出层效果(TAB标签,斑马线,遮罩层等)
Oct 08 Javascript
个人网站留言页面(前端jQuery编写、后台php读写MySQL)
May 03 Javascript
JavaScript无阻塞加载和defer、async详解
Feb 26 Javascript
ES6新特性六:promise对象实例详解
Apr 21 Javascript
webpack4 入门最简单的例子介绍
Sep 05 Javascript
vue实现拖拽的简单案例 不超出可视区域
Jul 25 Javascript
微信小程序实现首页弹出广告
Dec 03 Javascript
详解在Angular4中使用ng2-baidu-map的方法
Jun 19 #Javascript
了解Javascript中函数作为对象的魅力
Jun 19 #Javascript
利用vue-i18n实现多语言切换效果的方法
Jun 19 #Javascript
使用JQuery自动完成插件Auto Complete详解
Jun 18 #jQuery
使用异步controller与jQuery实现卷帘式分页
Jun 18 #jQuery
使用jQuery mobile NuGet让你的网站在移动设备上同样精彩
Jun 18 #jQuery
如何使用CSS3和JQuery easing 插件制作绚丽菜单
Jun 18 #jQuery
You might like
laravel 使用事件系统统计浏览量的实现
2019/10/16 PHP
JS 容错处理代码, 屏蔽错误信息
2021/03/09 Javascript
javascript检查表单数据是否改变的方法
2013/07/30 Javascript
使用JavaScript实现Java的List功能(实例讲解)
2013/11/07 Javascript
node+express+jade制作简单网站指南
2014/11/26 Javascript
jQuery中size()方法用法实例
2014/12/27 Javascript
input输入框鼠标焦点提示信息
2015/03/17 Javascript
js简单实现表单中点击按钮动态增加输入框数量的方法
2015/08/18 Javascript
[原创]Javascript 实现广告后加载 可加载百度谷歌联盟广告
2016/05/11 Javascript
jQuery检查元素存在性(推荐)
2016/09/17 Javascript
微信小程序 Video API实例详解
2016/10/02 Javascript
react实现pure render时bind(this)隐患需注意!
2017/03/09 Javascript
Jquery+Ajax+xml实现中国地区选择三级联动菜单效果(推荐)
2017/06/09 jQuery
JS轮播图实现简单代码
2021/02/19 Javascript
基于vue cli重构多页面脚手架过程详解
2018/01/23 Javascript
详解VUE-地区选择器(V-Distpicker)组件使用心得
2018/05/07 Javascript
使用async await 封装 axios的方法
2018/07/09 Javascript
使用js在layui中实现上传图片压缩
2019/06/18 Javascript
nodejs中的异步编程知识点详解
2021/01/17 NodeJs
[01:24]2014DOTA2 TI第二日 YYF表示这届谁赢都有可能
2014/07/11 DOTA
[04:27]2014DOTA2国际邀请赛 NAVI战队官方纪录片
2014/07/21 DOTA
[03:22]DAC最前线(第二期)—DOTA2亚洲邀请赛主赛场周边及线路探访
2015/01/24 DOTA
在Python的web框架中编写创建日志的程序的教程
2015/04/30 Python
django框架如何集成celery进行开发
2017/05/24 Python
Python基于Socket实现的简单聊天程序示例
2017/08/05 Python
python操作excel的方法
2018/08/16 Python
python读取csv文件指定行的2种方法详解
2020/02/13 Python
python实现信号时域统计特征提取代码
2020/02/26 Python
Python编程快速上手——PDF文件操作案例分析
2020/02/28 Python
在Keras中实现保存和加载权重及模型结构
2020/06/15 Python
css3制作彩色边线3d立体按钮的示例(css3按钮)
2014/05/06 HTML / CSS
档案接收函
2014/01/13 职场文书
董事长新年致辞
2015/07/29 职场文书
关于艺术节的开幕致辞
2016/03/04 职场文书
PyTorch 如何自动计算梯度
2021/05/23 Python
Win11无法安装更新补丁KB3045316怎么办 附KB3045316补丁修复教程
2022/08/14 数码科技