基于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 相关文章推荐
jquery简单体验
Jan 10 Javascript
jQuery对象和DOM对象相互转化
Apr 24 Javascript
js选取多个或单个元素的实现代码(用class)
Aug 22 Javascript
动态创建script在IE中缓存js文件时导致编码的解决方法
May 04 Javascript
javascript里使用php代码实例
Dec 13 Javascript
谈谈JavaScript自定义回调函数
Oct 18 Javascript
详解AngularJS验证、过滤器、指令
Jan 04 Javascript
jQuery返回定位插件详解
May 15 jQuery
Vue动态路由缓存不相互影响的解决办法
Feb 19 Javascript
vue.js多页面开发环境搭建过程
Apr 24 Javascript
element-ui 中使用upload多文件上传只请求一次接口
Jul 19 Javascript
vuex Module将 store 分割成模块的操作
Dec 07 Vue.js
详解在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实现生成透明背景的PNG缩略图函数分享
2014/07/08 PHP
PHP Try-catch 语句使用技巧
2016/02/28 PHP
PHP单例模式是什么 php实现单例模式的方法
2016/05/14 PHP
PHP基于回溯算法解决n皇后问题的方法示例
2017/11/07 PHP
Javascript 中介者模式实例
2009/12/16 Javascript
js获取当前日期代码适用于网页头部
2013/06/27 Javascript
jQuery让控件左右移动的三种实现方法
2013/09/08 Javascript
JavaScript中数据结构与算法(一):栈
2015/06/19 Javascript
轻松掌握JavaScript装饰者模式
2016/08/27 Javascript
解析js如何获取css样式
2016/12/11 Javascript
electron demo项目npm install安装失败的解决方法
2018/02/06 Javascript
详解Vue-cli webpack移动端自动化构建rem问题
2018/04/07 Javascript
JavaScript设计模式之构造函数模式实例教程
2018/07/02 Javascript
基于jQuery实现可编辑的表格
2019/12/11 jQuery
JS简单表单验证功能完整示例
2020/01/26 Javascript
mustache.js实现首页元件动态渲染的示例代码
2020/12/28 Javascript
python实现文件分组复制到不同目录的例子
2014/06/04 Python
实例说明Python中比较运算符的使用
2015/05/13 Python
Python选课系统开发程序
2016/09/02 Python
利用Python查看目录中的文件示例详解
2017/08/28 Python
Flask数据库迁移简单介绍
2017/10/24 Python
Python实现简单http服务器
2018/04/12 Python
Python 利用scrapy爬虫通过短短50行代码下载整站短视频
2018/10/29 Python
python在TXT文件中按照某一字符串取出该字符串所在的行方法
2018/12/10 Python
Django url 路由匹配过程详解
2021/01/22 Python
Python环境搭建过程从安装到Hello World
2021/02/05 Python
HTML5 实现一个访问本地文件的实例
2012/12/13 HTML / CSS
html5清空画布方法(三种)
2017/10/16 HTML / CSS
会计学财务管理专业个人的自我评价
2013/10/19 职场文书
大四学生找工作的自荐信
2014/03/27 职场文书
中学生社会实践活动总结
2014/07/03 职场文书
教师纪律作风整顿心得体会
2016/01/23 职场文书
nginx服务器的下载安装与使用详解
2021/08/02 Servers
Vue2.0搭建脚手架
2022/03/13 Vue.js
Python如何将list中的string转换为int
2022/07/15 Ruby
Win11 25163.1010更新补丁KB5016904推送,测试服务验证管道(附更新修复汇总)
2022/07/23 数码科技