基于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 相关文章推荐
Prototype 工具函数 学习
Jul 23 Javascript
jQuery Select(单选) 模拟插件 V1.3.62 改进版
Jul 17 Javascript
jQuery判断checkbox是否选中的3种方法
Aug 12 Javascript
jQuery判断指定id的对象是否存在的方法
May 22 Javascript
jQuery实现下拉框选择图片功能实例
Aug 08 Javascript
Bootstrap编写导航栏和登陆框
May 30 Javascript
JavaScript中数据类型转换总结
Dec 25 Javascript
import与export在node.js中的使用详解
Sep 28 Javascript
jquery实现联想词搜索框和搜索结果分页的示例
Oct 10 jQuery
axios使用拦截器统一处理所有的http请求的方法
Nov 02 Javascript
JS函数进阶之prototy用法实例分析
Jan 15 Javascript
JavaScript实现动态留言板
Mar 16 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
可以在线执行PHP代码包装修正版
2008/03/15 PHP
Yii学习总结之数据访问对象 (DAO)
2015/02/22 PHP
Laravel 5+ .env环境配置文件详解
2020/04/06 PHP
PHP如何通过带尾指针的链表实现'队列'
2020/10/22 PHP
javascript 写类方式之四
2009/07/05 Javascript
javascript下判断一个对象是否具有指定名称的属性的的代码
2010/01/11 Javascript
EXTJS记事本 当CompositeField遇上RowEditor
2011/07/31 Javascript
jQuery EasyUI API 中文文档 - Tree树使用介绍
2011/11/19 Javascript
jquery.messager.js插件导致页面抖动的解决方法
2013/07/14 Javascript
基于jquery异步传输json数据格式实例代码
2013/11/23 Javascript
jquery仿搜索自动联想功能代码
2014/05/23 Javascript
Nodejs极简入门教程(二):定时器
2014/10/25 NodeJs
表单元素值获取方式js及java方式的简单实例
2016/10/15 Javascript
jquery心形点赞关注效果的简单实现
2016/11/14 Javascript
原生js实现addclass,removeclass,toggleclasss实例
2016/11/24 Javascript
微信小程序动态的加载数据实例代码
2017/04/14 Javascript
Bootstrap 表单验证formValidation 实现表单动态验证功能
2017/05/17 Javascript
gulp教程_从入门到项目中快速上手使用方法
2017/09/14 Javascript
利用js将ajax获取到的后台数据动态加载至网页中的方法
2018/08/08 Javascript
layui获取选中行数据的实例讲解
2018/08/19 Javascript
webpack结合express实现自动刷新的方法
2019/05/07 Javascript
layui.tree组件的使用以及搜索节点功能的实现
2019/09/26 Javascript
JS数组方法reverse()用法实例分析
2020/01/18 Javascript
python 内置函数filter
2017/06/01 Python
python中scikit-learn机器代码实例
2018/08/05 Python
python框架中flask知识点总结
2018/08/17 Python
Python设计模式之外观模式实例详解
2019/01/17 Python
使用TFRecord存取多个数据案例
2020/02/17 Python
将不规则的Python多维数组拉平到一维的方法实现
2021/01/11 Python
小区门卫岗位职责
2013/12/31 职场文书
自我介绍演讲稿范文
2014/08/21 职场文书
党员干部民主生活会议批评与自我批评材料
2014/09/20 职场文书
公司仓管员岗位职责
2015/04/01 职场文书
python基础之错误和异常处理
2021/10/24 Python
《艾尔登法环》发布最新「战技」宣传片
2022/04/03 其他游戏
vue router 动态路由清除方式
2022/05/25 Vue.js