WishMeLz

生活其实很有趣

Nodejs+Express+Vue 分片上传文件

Vue

<template>
  <div class="upload">
    <el-upload
      :before-upload="befUpload"
      class="upload-demo"
      drag
      action="/"
      :show-file-list="false"
    >
      <el-icon class="el-icon--upload"><upload-filled /></el-icon>
      <div class="el-upload__text">拖拽或<em>点击上传</em></div>
    </el-upload>
  </div>
</template>

<script setup>
import axios from "axios";
function befUpload(file) {
  console.log(file);
  const eachSize = 2 * 1024 * 1024; // 每个chunks的大小
  const blockCount = Math.ceil(file.size / eachSize); // 分片总数
  const axiosArray = []; // axiosPromise数组
  let ext = file.name.split(".");
  ext = ext[ext.length - 1]; // 获取文件后缀名
  // 通过hash标识文件
  let random = Math.random().toString();
  random = random.split(".");
  random = random[random.length - 1];
  let hash = Date.now() + random + file.lastModified; // 文件 hash 实际应用时,hash需要更加复杂,确保唯一性,可以使用uuid
  // 处理每个分片的上传操作
  for (let i = 0; i < blockCount; i++) {
    let start = i * eachSize,
      end = Math.min(file.size, start + eachSize);
    // 构建表单
    const form = new FormData();
    form.append("file", file.slice(start, end));
    form.append("name", file.name);
    form.append("total", blockCount);
    form.append("ext", ext);
    form.append("index", i);
    form.append("size", file.size);
    form.append("hash", hash);
    // ajax提交 分片,此时 content-type 为 multipart/form-data
    const axiosOptions = {
      onUploadProgress: (e) => {
        handleXhrProgressCallback(blockCount, i, e);
      },
    };
    // 加入到 Promise 数组中
    axiosArray.push(
      axios.post("http://127.0.0.1:51413/v1/upload_chunks", form, axiosOptions)
    );
  }
  // 所有分片上传后,请求合并分片文件
  axios.all(axiosArray).then(() => {
    // 合并chunks
    const data = {
      name: file.name,
      total: blockCount,
      ext,
      hash,
      resetname:"1"
    };

    // resetname: 后台做个判断是否自定义名字
    axios
      .post("http://127.0.0.1:51413/v1/merge_chunks", data)
      .then((res) => {
        handleXhrSuccessCallback(res.data);
      })
      .catch((err) => {
        handleXhrErrorCallback(err);
      });
  });
}

function handleXhrProgressCallback(total, index, e) {
  console.log(`当前上传第 ${index + 1} 个chunk,共计 ${total}`);
}
// 上传成功处理
function handleXhrSuccessCallback(data) {
  console.log("上传成功处理");
  console.log(data);
}

// 上传失败处理
function handleXhrErrorCallback(err) {
  console.log(err);
}

</script>

<style>
</style>

Node

chunk.js

const multer = require('multer');

const chunksBasePath = '~uploads/';

const storage = multer.diskStorage({
    destination:chunksBasePath,
});

const baseUpload = multer({storage});
const upload = baseUpload.single('file');

/**
 * @name uploadMiddleware
 * @description 文件上传中间件,upload 方法调用的时候 会有 err 进行错误判断
 * @param {*} req
 * @param {*} res
 * @param {*} next
 */
const uploadMiddleware = (req,res,next)=>{
    upload(req,res,(err)=>{
        if(err){
            // 进行错误捕获
            res.json({code:-1,msg:err.toString()});
        }else{
            next();
        }
    });
};

module.exports = uploadMiddleware;

app.js


const express = require('express');
const app = express();
const port = 51413;
const bodyParser = require('body-parser');
app.use(require('cors')())
app.use(bodyParser.urlencoded({
    extended: false
}));
app.use(bodyParser.json())
app.use(express.static('./public'))
app.use(express.static('./uploads'))
app.use('/v1', require('./route/upload'))
app.listen(port,()=>{
    console.log('sever run to http://127.0.0.1:' + port);
})

upload.js

const express = require('express')
const router = express.Router();
const uploadChunksMiddleware = require('../chunk.js');
const fs = require('fs');
const path = require('path');
const fileBasePath = 'uploads/';
const chunkBasePath = '~uploads/';

router.post('/upload_chunks', uploadChunksMiddleware, (req, res) => {
    // 创建chunk的目录
    const chunkTmpDir = chunkBasePath + req.body.hash + '/';
    // 判断目录是否存在
    if (!fs.existsSync(chunkTmpDir)) fs.mkdirSync(chunkTmpDir);
    // 移动切片文件
    fs.renameSync(req.file.path, chunkTmpDir + req.body.hash + '-' + req.body.index);
    res.send(req.file);
})

// 文件分片
router.post('/merge_chunks', (req, res) => {
    const total = req.body.total;
    const hash = req.body.hash;
    const resetname = req.body.resetname;
    // const saveDir = '2344'
    const saveDir = fileBasePath + new Date().getFullYear() + (new Date().getMonth() + 1) + new Date().getDate() + '/';
    let savePath = '';
    if (resetname == '1') {
        savePath = saveDir + Date.now() + hash + '.' + req.body.ext;

    }else {
        savePath = saveDir + resetname + '.' + req.body.ext;
    }
    const chunkDir = chunkBasePath + '/' + hash + '/';
    console.log(savePath);
    try {
        // 创建保存的文件夹(如果不存在) 
        if (!fs.existsSync(saveDir)) fs.mkdirSync(saveDir);
        // 创建文件
        fs.writeFileSync(savePath, '');
        // 读取所有的chunks 文件名存放在数组中
        const chunks = fs.readdirSync(chunkBasePath + '/' + hash);
        // 检查切片数量是否正确
        if (chunks.length !== total || chunks.length === 0) return res.send({ code: -1, msg: '切片文件数量不符合' });
        for (let i = 0; i < total; i++) {
            // 追加写入到文件中
            fs.appendFileSync(savePath, fs.readFileSync(chunkDir + hash + '-' + i));
            // 删除本次使用的chunk
            fs.unlinkSync(chunkDir + hash + '-' + i);
        }
        // 删除chunk的文件夹
        fs.rmdirSync(chunkDir);
        // 返回uploads下的路径,不返回uploads
        res.json({
            code: 0, msg: '文件上传成功', data: {
                path: savePath.split(fileBasePath)[savePath.split(fileBasePath).length - 1]
            }
        });
    } catch (err) {
        res.json({ code: -1, msg: '出现异常,上传失败' });
    }
});
module.exports = router

Github: https://github.com/WishMelz/Express-chunk-upload