大文件切片上传以及切片合并

前端:vue

后端:nodejs + express

先了解一下为什么需要切片上传,在上传文件的时候,假如文件大小高达几百上千MB的时候,上传的时间就会变得很长,如果出现网络不稳定或者浏览器、程序崩溃等,上传就会失败,而此时文件切片上传以及断点续传就很重要了,当然还有大文件秒传,这是文件的 hash值 来实现的,如果服务器存在相同 hash值 的文件则直接告诉前端文件上传成功,本文先讲切片上传 以及切片合并

先来看前端代码: 首先我们需要一个 input , 并且 type="file",以及一个上传的按钮,具体代码如下

js 复制代码
<template>
  <div>
    <input type="file" @change="onFileChange" />
    <button @click="upload">Upload</button>
  </div>
</template>
<script>
import axios from 'axios';

export default {
  data() {
    return {
      file: null,//上传的文件
      filename: "",//文件名
    };
  },
  methods: {
    onFileChange(event) {
      this.file = event.target.files[0];
      console.log(event.target.files[0]);
      this.filename = this.file.name
    },
    async upload() {
      if (!this.file) {
        return;
      }
      /**
       * chunkSize 为切片大小,单位为字节
       * totalChunks 为切片总数  Math.ceil 为向上取整
       * chunkIndex 为当前切片的索引
       * chunkPromises 为所有切片的 Promise
       */
      const chunkSize = 10 * 1024 * 1024; // 10MB
      const totalChunks = Math.ceil(this.file.size / chunkSize);
      const chunkPromises = [];

      // 进行切片
      for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
        const start = chunkIndex * chunkSize;
        const end = Math.min(start + chunkSize, this.file.size);
        const chunk = this.file.slice(start, end);


        //  FormData 对象,可以存储二进制数据和表单数据,注意append的顺序,chunk放在最后
        //  确保后端能先解析到chunkIndex, totalChunks ,filename  用于文件命名
        const formData = new FormData();
        formData.append('chunkIndex', chunkIndex);  //key:chunkIndex  value:每个切片的索引
        formData.append('totalChunks', totalChunks);  //key:totalChunks  value:切片总数
        formData.append('filename', this.file.name);  //key:filename  value:文件名
        formData.append('file', chunk);  //key:file  value:每个切片的二进制数据
        const uploadPromise = axios.post('http://127.0.0.1:3000/upload', formData);
        chunkPromises.push(uploadPromise);

        // const fileReader = new FileReader();
        // fileReader.onload = (e) => {
        //   console.log(e);
        // }
        // fileReader.readAsDataURL(chunk);
      }
      console.log(chunkPromises);
      const res=await Promise.all(chunkPromises);
      console.log('res',res);
      

      // 所有切片上传完成后,可以发送请求告知服务器文件上传完成
      await axios.post('http://127.0.0.1:3000/upload/complete', { filename: this.filename });

      // 上传完成后的回调
      console.log('文件上传完成');
    }
  }
};
</script>

再来看下nodejs实现的后端代码,先来看下依赖:

js 复制代码
"dependencies": {
    "express": "^4.18.2",
    "multer": "1.4.5-lts.1"
  }

先来引入需要用到的包以及解决跨域问题

js 复制代码
const express = require('express');
const multer = require('multer');
const app = express();
const fs = require('fs');
const path = require('path');

app.use((req, res, next) => {
  // 允许所有域名的跨域请求
  res.header('Access-Control-Allow-Origin', '*');
  // 设置允许的HTTP头部类型
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
  
  // 设置允许的HTTP方法
  if (req.method === 'OPTIONS') {
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE');
    return res.status(200).json({});
  }

  // 继续处理请求
  next();
});

配置中间件解析 JSON 请求

js 复制代码
// express.json() 是一个中间件,用于解析 JSON 格式的请求体
app.use(express.json());

写一下接收文件切片的接口,

js 复制代码
// 很显然,这里使用了个中间件,是multer存储文件的
app.post('/upload',upload.single('file'),(req, res) => {
  console.log('返回数据---');
  
  res.send({
    code: 200,
    msg: '上传成功',
  });
});

其中upload.single('file'),这个中间件是multer提供的,下面来配置一下,每当调用这个接口的时候,都会触发下面的配置。

配置一下 multer 包,这是一个第三方库,可以配置自动将文件存储在服务器指定的目录下,记得使用中间件upload.single('file')

js 复制代码
// 配置文件存储 这个是multer库的配置,当接口使用upload.single('file')中间件时会自动调用这个配置
const storage = multer.diskStorage({
  // 设置文件存储的位置,其中回调函数cb的第一个参数表示错误信息,第二个参数表示文件的存储路径
  destination: function (req, file, cb) {
    // 创建临时文件夹
    try {
      fs.mkdirSync(path.resolve(__dirname, 'temp'), { recursive: true });
    } catch (error) {
      console.log(' 创建临时文件夹失败',error);
    }
    
    cb(null, path.resolve(__dirname, 'temp'));
  },
  // 设置文件名,同上,第一个参数表示错误信息,第二个参数表示文件名
  filename: function (req, file, cb) {
    // console.log('reqbody',req.body);//打印req.body,
    // console.log('file',file);
    cb(null, file.fieldname + '_' + req.body.chunkIndex);
  }
});

//这个应该就是 multer 使用刚刚那个配置需要调用的
const upload = multer({ storage: storage });

这里的 filename回调函数是用于命名的,记得前端传过来的FormData中的切片chunk放在最后,命名格式如下:

这样可以根据后面的数字进行排序。

再来写一下切片上传完成后合并切片的接口

js 复制代码
//当前端所有切片上传完成之后,会调用这个接口进行合并
app.post('/upload/complete', (req, res) => {
  
  // console.log('filename',req.body.filename);
  const filename = req.body.filename;
  combineChunks(path.resolve(__dirname, 'acceptFiles/'+filename),path.resolve(__dirname, 'temp'))


  res.json('文件上传完成');
});

//   combineChunks 方法用于将所有切片合并到一个文件中
//   第一个参数:存放路径,其中工作目录`__dirname`,文件存放目录 `acceptFiles`,文件名 filename
//   第二个参数: 切片存放的目录路径
const combineChunks = (outputFilename, tempDir) => {
  // fs.reddir 读取文件夹中的文件 , files 返回文件列表
  fs.readdir(tempDir, (err, files) => {
    if (err) {
      console.error('Error reading directory:', err);
      return;
    }
   //将文件列表进行排序  因为每个切片命名为,举例: hhh.mp4, 每个切片 为 file_0,file_1 ,依次类推,
   //顺序对于切片合并来说非常重要,顺序错误合并的文件也会错误
    files.sort((a, b) => {
      return parseInt(a.split('_').pop()) - parseInt(b.split('_').pop())
    })
    console.log('排序后files',files);

    // 获取目录路径
    const directoryPath = path.dirname(outputFilename);
    // 创建目录,如果它不存在
    fs.mkdir(directoryPath, { recursive: true }, (err) => {
      if (err) {
        return console.error('创建目录时发生错误:', err);
      }
    });
      // 创建一个可写流用于输出文件
    const output = fs.createWriteStream(outputFilename);
    
    (async function() {
      for (const file of files) {
      console.log('file',file)
    // 对每个文件,创建一个Promise以确保按序写入
        await new Promise((resolve, reject) => {
          // 获取当前文件的完整路径
          const filePath = path.resolve(tempDir, file);
          // 创建一个读取当前文件的流
          const readStream = fs.createReadStream(filePath);
    
      // 如果读取流发生错误,拒绝这个Promise
          readStream.on('error', reject);
          // 如果写入流发生错误,也拒绝这个Promise
          output.on('error', reject);
    
      // 当读取流结束时,解决这个Promise,表示当前文件已经写入完毕
          readStream.on('end', resolve);
          // 使用 pipe 方法将读取流的内容写入 output 可写流中
          // end: false 参数表示在当前文件写入完成后不关闭输出流
          readStream.pipe(output, { end: false });
        });
      }
      // 当所有文件写入完成后,关闭输出流
      output.end();
    })()
    .then( 
      // 删除存放切片文件夹,recursive: true 表示递归删除
      () => fs.rm(tempDir, { recursive: true },
      (err) => {
        if (err) {
          // 处理错误
          console.error(err);
        } else {
          // 删除成功
          console.log('Directory removed successfully.');
        }
      })
    )
    .catch(e=>{
      console.log('发生错误',e)
    });
  })
}

下面来试一下,选择一个文件

点击 upload,可以看到,temp目录下有切片,并且切片合并好放在了 acceptFiles 目录了。

当合并完成之后,会执行 fs.rm 方法删除存放切片的 temp 目录。

后端完整代码:

js 复制代码
const express = require('express');
const multer = require('multer');
const app = express();
const fs = require('fs');
const path = require('path');

app.use((req, res, next) => {
  // 允许所有域名的跨域请求
  res.header('Access-Control-Allow-Origin', '*');
  // 设置允许的HTTP头部类型
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
  
  // 设置允许的HTTP方法
  if (req.method === 'OPTIONS') {
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE');
    return res.status(200).json({});
  }

  // 继续处理请求
  next();
});


// express.json() 是一个中间件,用于解析 JSON 格式的请求体
app.use(express.json());

// 配置文件存储 这个是multer库的配置,当接口使用upload.single('file')中间件时会自动调用这个配置
const storage = multer.diskStorage({
  // 设置文件存储的位置,其中回调函数的第一个参数表示错误信息,第二个参数表示文件的存储路径
  destination: function (req, file, cb) {
    // 创建临时文件夹
    try {
      fs.mkdirSync(path.resolve(__dirname, 'temp'), { recursive: true });
    } catch (error) {
      console.log(' 创建临时文件夹失败',error);
    }
    
    cb(null, path.resolve(__dirname, 'temp'));
  },
  // 设置文件名,同上,第一个参数表示错误信息,第二个参数表示文件名,这里有坑,下面说
  filename: function (req, file, cb) {
    // console.log('reqbody',req.body);
    // console.log('file',file);
    cb(null, file.fieldname + '_' + req.body.chunkIndex);
  }
});

//这个应该就是 multer 使用刚刚那个配置需要调用的
const upload = multer({ storage: storage });

app.post('/upload',upload.single('file'),(req, res) => {
  console.log('返回数据---');
  
  res.send({
    code: 200,
    msg: '上传成功',
  });
});

//当前端所有切片上传完成之后,会调用这个接口进行合并
app.post('/upload/complete', (req, res) => {
  
  // console.log('filename',req.body.filename);
  const filename = req.body.filename;
  combineChunks(path.resolve(__dirname, 'acceptFiles/'+filename),path.resolve(__dirname, 'temp'))


  res.json('文件上传完成');
});

app.listen(3000, () => {
  console.log('服务器已启动');
});


//   这个方法用于将所有切片合并到一个文件中
//   第一个参数:存放路径,其中工作目录`__dirname`,文件存放目录 `acceptFiles`,文件名 filename
//   第二个参数: 切片存放的目录路径
const combineChunks = (outputFilename, tempDir) => {
  // fs.reddir 读取文件夹中的文件 , files 返回文件列表
  fs.readdir(tempDir, (err, files) => {
    if (err) {
      console.error('Error reading directory:', err);
      return;
    }
    /**
      将文件列表进行排序  因为每个切片命名为,举例: hhh.mp4, 每个切片 为 hhh.mp4_0,hhh.mp4_1 ,依次类推,
      顺序对于切片合并来说非常重要,顺序错误合并的文件也会错误
     *  */ 
    files.sort((a, b) => {
      return parseInt(a.split('_').pop()) - parseInt(b.split('_').pop())
    })
    console.log('排序后files',files);

    // 获取目录路径
    const directoryPath = path.dirname(outputFilename);
    // 创建目录,如果它不存在
    fs.mkdir(directoryPath, { recursive: true }, (err) => {
      if (err) {
        return console.error('创建目录时发生错误:', err);
      }
    });
      // 创建一个可写流用于输出文件
    const output = fs.createWriteStream(outputFilename);
    
    (async function() {
      for (const file of files) {
      console.log('file',file)
    // 对每个文件,创建一个Promise以确保按序写入
        await new Promise((resolve, reject) => {
          // 获取当前文件的完整路径
          const filePath = path.resolve(tempDir, file);
          // 创建一个读取当前文件的流
          const readStream = fs.createReadStream(filePath);
    
      // 如果读取流发生错误,拒绝这个Promise
          readStream.on('error', reject);
          // 如果写入流发生错误,也拒绝这个Promise
          output.on('error', reject);
    
      // 当读取流结束时,解决这个Promise,表示当前文件已经写入完毕
          readStream.on('end', resolve);
          // 使用 pipe 方法将读取流的内容写入 output 可写流中
          // end: false 参数表示在当前文件写入完成后不关闭输出流
          readStream.pipe(output, { end: false });
        });
      }
      // 当所有文件写入完成后,关闭输出流
      output.end();
    })()
    .then( 
      // 删除临时文件夹,recursive: true 表示递归删除
      () => fs.rm(tempDir, { recursive: true },
      (err) => {
        if (err) {
          // 处理错误
          console.error(err);
        } else {
          // 删除成功
          console.log('Directory removed successfully.');
        }
      })
    )
    .catch(e=>{
      console.log('发生错误',e)
    });
    
  })
}
相关推荐
hvinsion12 分钟前
HTML 霓虹灯开关效果
前端·html
顾平安13 分钟前
JS 预编译代码实例分析
前端·js
好奇的菜鸟28 分钟前
Vue.js 实现用户注册功能
前端·javascript·vue.js
是程序喵呀29 分钟前
vue安装步骤
前端·javascript·vue.js
前端Hardy29 分钟前
HTML 中 a 标签跳转问题总结:从框架页面跳转的困境与突破
前端·javascript·html
@PHARAOH1 小时前
HOW - React 状态模块化管理和按需加载(一) - react-redux
前端·javascript·react.js·redux
草明1 小时前
在 Flutter 中,Image.asset 从其他包中加载资源
前端·javascript·flutter
Au_ust1 小时前
css:项目
前端·css
大浪淘沙10241 小时前
解决因为数据变化,页面没有变化的情况 , 复习一下使用 vuex 的 modules
前端·javascript·vue.js
秋沐2 小时前
微前端-MicroApp
前端·react.js·webpack·前端框架·npm