大文件切片上传+断点续传解决方案-前后端实现(附源码)

上传文件大家应该都做过,前端直接把file文件传给后端就ok了,但是大文件这样传就会造成页面假死,体验极差。如果遇到网络不稳定的时候,中途上传失败的话,又要从头开始传,本来文件就大,还慢。所以今天我们用一种新方法-切片上传+断点续传

前端实现:

页面上很简单,我就放了进度条和一个上传文件组件,你可以按照三方ui来进行替换

    <div class="box">
        <progress value="0"></progress>
        <br />
        <input type="file" text="选择文件"/>
    </div>

 主要看逻辑实现

1、获取页面元素,初始化变量(vue中获取元素使用ref即可)

let ipt = doc.querySelector("input");
let progress = doc.querySelector("progress");
const chunkSize = 64 * 1024; //切片大小
let uploaded = 0; //已上传多少

2、 input绑定change事件(如果有专门的上传按钮,就给按钮绑定点击事件,如下图2)

思路:

  • 通过read函数,把blob文件转为字符串,然后打成MD5
  • 然后设置进度条的max为文件的size
  • 判断缓存中是否有上传的进度,有就把切片初始值设置缓存中的值
  • 设置循环,当前上传的大小 < 文件size时,进行切片,使用file.slice(起始值,起始值 + 切片大小)
  • 把当前切片append进formdata,然后传给后端
  • 然后把修改切片起始值,设置缓存进度,设置进度条当前值
  // blob转为为字符串
  const read = (file) => {
    let reader = new FileReader();
    return new Promise((resolve, reject) => {
      reader.onload = function (event) {
        const arrayBuffer = event.target.result;
        resolve(arrayBuffer);
      };
      reader.onerror = reject;
      reader.readAsBinaryString(file);
    });
  };

 ipt.addEventListener("change", async (event) => {
    let file = event.target.files[0];
    if (!file) {
      return;
    }
    ipt.value = null;
    let content = await read(file);
    let hash = md5(content);//转成hash,保证文件唯一
    let { size, name, type } = file;
    const local = localStorage.getItem(uploaded);//断点重传标志
    progress.max = size;
    if (local) {
      uploaded = Number(local);
    }
    while (uploaded < size) {
      //切片大小
      const chunk = file.slice(uploaded, uploaded + chunkSize, type);
      let fd = new FormData();
      fd.append("name", name);
      fd.append("size", size);
      fd.append("type", type);
      fd.append("file", chunk);
      fd.append("offset", uploaded);
      fd.append("hash", hash);
      try {
        await axios.post("http://localhost:5454/upload", fd);
      } catch (error) {
        console.log("上传失败");
      }

      uploaded += chunkSize;
      localStorage.setItem(uploaded, uploaded);//设置传递进度
      progress.value = uploaded;
    }
    console.log("上传成功");
  });
btn.addEventListener('click',async()=>{
    let file = ipt.files[0]
})

后端实现:

思路:

  • 利用express-fileupload插件后,file在req.files中取,其他数据在req.body取
  • 设置文件名,利用extreme获取传来的文件的后缀名,拼接在跟目录的upload文件夹下
  • 判断offset != 0,就是已上传但是本地没有文件,创建文件并写入,否则写入到已经存在的文件中
//app.js
const express = require("express");
const bodyParser = require("body-parser");
const fileUpload = require("express-fileupload");
const { resolve, extname } = require("path");
const { existsSync, appendFileSync, writeFileSync } = require("fs");

const app = express();
const PORT = "5454";

// 跨域
app.all("*", (req, res, next) => {
  res.header("Access-Control-Allow-origin", "*");
  res.header("Access-Control-Allow-Methods", "POST,GET");
  next();
});

app.use(bodyParser.json());
app.use(fileUpload());
app.use('/',express.static('upload'))

// app.get("/", (req, res) => {
//   res.send("Hello World!");
// });

app.post("/upload", (req, res) => {
  const { name, size, type, offset, hash } = req.body;
  const { file } = req.files;
  // console.log(name, size, type, offset, hash);
  //取后缀
  let ext = extname(name);
  let fileName = resolve(__dirname, "./upload/" + hash + ext);
  if (offset != 0) {
    if (!existsSync(fileName)) {
      res.status(400).send({
        msg:'文件不存在'
      })
      return
    }
    appendFileSync(fileName,file.data)
    res.send({
      code:200,
      data:'http://localhost:5454/' + fileName,
      msg:'文件写入成功'
    })
    return
  }
  writeFileSync(fileName, file.data);
    res.send({
      code: 200,
      msg: "文件创建并写入成功",
    });
});

app.listen(PORT, () => {
  console.log(`server running in ${PORT}`);
});

以上就实现了大文件上传的基本方法,像文件类型校验啥的可自行扩展,本demo只是基本示例,如有更好的方法,可在评论区留下讨论,共同进步~

源码地址:大文件上传: 大文件上传demo 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个使用 Node.js 实现文件切片上传断点续传的功能示例: ```javascript const http = require('http'); const fs = require('fs'); const path = require('path'); const PORT = 8000; const UPLOAD_DIR = path.resolve(__dirname, './uploads'); // 创建上传目录 if (!fs.existsSync(UPLOAD_DIR)) { fs.mkdirSync(UPLOAD_DIR); } // 上传文件处理函数 const handleUpload = (req, res) => { const { filename, chunkIndex, totalChunks } = req.headers; const chunkDir = path.resolve(UPLOAD_DIR, filename); // 如果是第一个分片,创建文件夹 if (chunkIndex === '0') { fs.mkdirSync(chunkDir); } // 获取上传的分片数据 const chunksData = []; req.on('data', (chunk) => { chunksData.push(chunk); }); req.on('end', () => { const buffer = Buffer.concat(chunksData); // 写入分片文件 fs.writeFileSync(path.resolve(chunkDir, chunkIndex), buffer); // 如果当前分片是最后一个分片,则合并文件 if (Number(chunkIndex) === Number(totalChunks) - 1) { const filePath = path.resolve(UPLOAD_DIR, filename); const chunks = fs.readdirSync(chunkDir); const writeStream = fs.createWriteStream(filePath); chunks.forEach((chunk) => { const chunkPath = path.resolve(chunkDir, chunk); const chunkBuffer = fs.readFileSync(chunkPath); fs.unlinkSync(chunkPath); // 删除分片文件 writeStream.write(chunkBuffer); }); writeStream.end(() => { res.end('upload success'); }); } else { res.end('chunk upload success'); } }); }; // 断点续传处理函数 const handleResumeUpload = (req, res) => { const { filename } = req.headers; const filePath = path.resolve(UPLOAD_DIR, filename); const fileStat = fs.statSync(filePath); res.setHeader('Content-Length', fileStat.size); res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Accept-Ranges', 'bytes'); const range = req.headers.range || 'bytes=0-'; const positions = range.replace(/bytes=/, '').split('-'); const start = parseInt(positions[0], 10); const end = positions[1] ? parseInt(positions[1], 10) : fileStat.size - 1; const chunkSize = end - start + 1; res.setHeader('Content-Range', `bytes ${start}-${end}/${fileStat.size}`); res.setHeader('Cache-Control', 'no-cache'); const readStream = fs.createReadStream(filePath, { start, end }); readStream.on('open', () => { readStream.pipe(res); }); readStream.on('error', () => { res.end('Error'); }); }; // 创建 HTTP 服务器 const server = http.createServer((req, res) => { if (req.url === '/upload' && req.method === 'POST') { handleUpload(req, res); } else if (req.url === '/resume-upload' && req.method === 'GET') { handleResumeUpload(req, res); } else { res.end('Hello World!'); } }); // 启动服务器 server.listen(PORT, () => { console.log(`Server is listening on port ${PORT}`); }); ``` 使用示例: 1. 开启服务器:`node server.js` 2. 上传文件:使用 POST 请求发送文件分片(每个分片的大小可以自定义),请求头需要包含 `filename`(文件名)、`chunkIndex`(当前分片索引,从 0 开始)、`totalChunks`(总分片数)三个字段。 3. 断点续传:使用 GET 请求获取已上传文件,请求头需要包含 `filename`(文件名)字段和 `range`(请求的字节范围)字段。如果请求头中没有 `range` 字段,则返回整个文件的内容。如果请求头中有 `range` 字段,则只返回指定字节范围内的文件内容。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值