input进行文件上传时有关的属性

属性 说明
accept 指定上传文件的类型,例如:accept=”image/*”,只能上传图片文件
multiple 允许上传多个文件
webkitdirectory 是否上传文件夹

单文件上传

后端:

var express = require('express');
var router = express.Router();
const multer  = require('multer')
const upload = multer({ dest: 'uploads/' })

/* GET home page. */
router.post('/upload',upload.single("image"),function(req, res, next) {
  res.send({ok:1})
});

module.exports = router;
  1. form
<form method="post" action="http://localhost:3000/upload" enctype="multipart/form-data">
    <input type="file" name="image"/><br />
    <input type="submit" value="上传"/><br />
</form>

说明:form表单enctype属性的值必需为multipart/form-data,name=”image”与upload.single(“image”)相对应

  1. fetch
<input type="file" id="uploadFile">
<button onclick="upload()">upload</button>
<script>
    const uploadFile = document.querySelector("#uploadFile")

    function upload(){
        const file = uploadFile.files[0]
        const formData = new FormData()
        formData.set("image", file)

        fetch("http://localhost:3000/upload",{
            method: "POST",
            body: formData
        }).then(res=>res.json()).then(res=>{
            console.log(res)
        })
    }
</script>

说明:这里body的值需要利用FormData,其中formData.set(“image”, file)的image对应于upload.single(“image”)

多文件上传

后端:

var express = require('express');
var router = express.Router();
const multer  = require('multer')
const upload = multer({ dest: 'uploads/' })

/* GET home page. */
router.post('/upload',upload.array("image",5),function(req, res, next) {
  res.send({ok:1})
});

module.exports = router;

说明:upload.array(“image”,5)第二个参数指定了最多上传的图片数量

  1. form
<form method="post" action="http://localhost:3000/upload" enctype="multipart/form-data">
    <input id="uploadFile" type="file" name="image" onchange="upload()" multiple/><br />
    <input type="submit" value="上传"/><br />
</form>

<script>
    const uploadFile = document.querySelector("#uploadFile")
    function upload(){
        if(uploadFile.files.length>5){
            // 清空文件选择的输入框
            uploadFile.value = ''
            alert("最多上传5张图片")
        }
    }
</script>
  1. fetch
<input type="file" id="uploadFile" multiple>
<button onclick="upload()">upload</button>
<script>
    const uploadFile = document.querySelector("#uploadFile")

    function upload() {
        const formData = new FormData()
        if (uploadFile.files.length > 5) {
            alert("最多上传5张图片")
        } else {
            for (let item of uploadFile.files) {
                // 将文件数据存入表单
                formData.append("image", item)
            }
            // 上传表单
            fetch("http://localhost:3000/upload", {
                method: "POST",
                body: formData
            }).then(res => res.json()).then(res => {
                console.log(res)
            })
        }
    }
</script>

上传文件夹

给input标签加个webkitdirectory即可

<input type="file" id="uploadFile" webkitdirectory>
<button onclick="upload()">upload</button>
<script>
    const uploadFile = document.querySelector("#uploadFile")

    function upload() {
        const formData = new FormData()
        if (uploadFile.files.length > 5) {
            alert("最多上传5张图片")
        } else {
            for (let item of uploadFile.files) {
                // 将文件数据存入表单
                formData.append("image", item)
            }
            // 上传表单
            fetch("http://localhost:3000/upload", {
                method: "POST",
                body: formData
            }).then(res => res.json()).then(res => {
                console.log(res)
            })
        }
    }
</script>

拖拽上传

const dragArea = document.getElementsByClassName('dragArea')[0]

// 当拖拽文件进入时触发
dragArea.ondragenter = (e) => {
    // 阻止默认行为
    e.preventDefault()
}
// 当拖拽文件停留在此区域不松手会不断触发
dragArea.ondragover = (e) => {
    e.preventDefault()
}
// 当拖拽文件松手时触发
dragArea.ondrop = async (e) => {
    // 创建form表单进行数据存储
    const formData = new FormData();
    // 处理文件
    const files = e.dataTransfer.files;
    for (let file of files) {
        if(file.type){
            formData.append("file", file);
        }
    }

    // 处理文件夹
    const items = e.dataTransfer.items;
    for (let item of items) {
        const entry = item.webkitGetAsEntry();
        if (entry.isDirectory) {
            // 等待
            await directoryDec(entry, formData);
        }
    }

    // 一次性上传所有数据
    fetch("http://localhost:3000/upload", {
        method: "POST",
        body: formData
    }).then(res => res.json()).then(res => {
        console.log(res)
    })

    e.preventDefault()
}
// 处理文件夹函数
function directoryDec(entry, formData) {
    return new Promise((resolve, reject) => {
        const reader = entry.createReader();
        reader.readEntries(async (entries) => {
            // 使用 Promise.all 确保所有递归调用完成
            await Promise.all(entries.map(async (itemEntry) => {
                if (itemEntry.isDirectory) {
                    // 等待处理子目录完成
                    await directoryDec(itemEntry, formData);
                } else {
                    await new Promise(resolve1 => {
                        itemEntry.file(file => {
                            formData.append("file", file);
                            resolve1();
                        });
                    })
                }
            }));
            resolve();
        })
    });
}

大文件分片上传

  • 前端分片,后端进行文件重组
  • 后端校验文件完整性
  • 前端可根据自己的cpu利用worker开启多个进程,提高效率
  1. 前端
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"
        integrity="sha512-iWbxiCA4l1WTD0rRctt/BfDEmDC5PiVqFc6c1Rhj/GKjuj6tqrjrikTw3Sypm/eEgMa7jSOS9ydmDlOtxJKlSQ=="
        crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>

<body>
    <input type="file" id="upload">
    <button onclick="upload()">上传</button>
    <script>
        const uploadInput = document.querySelector("#upload")
        async function upload() {
            // 定义分片大小
            const chunkSize = 1024 * 1024 * 3
            // 文件大小
            const fileSize = uploadInput.files[0].size
            // 记录文件完整的MD5值
            const spark = new SparkMD5.ArrayBuffer()
            spark.append(uploadInput.files[0])
            // 一共要分多少个切片
            const chunkCount = Math.ceil(fileSize / chunkSize)
            // 存储所有切片信息
            const chunkAll = []
            for (let i = 0; i < chunkCount; i++) {
                const chunkItem = await createChunk(uploadInput.files[0], i, chunkSize)
                chunkAll.push(chunkItem)
            }
            
            const formData = new FormData()
            chunkAll.forEach((item)=>{
                formData.append("chunk",item.blob, `chunk${item.index}`)
            })
            formData.append("MD5", spark.end())
            // 发送请求
            fetch("http://localhost:3000/upload", {
                method: "POST",
                body: formData
            }).then(res => res.json()).then(res => {
                console.log(res)
            })
        }
        // 创建每一个分片信息
        function createChunk(file, index, chunkSize) {
            return new Promise(resolve => {
                // 开始位置
                let start = index * chunkSize
                // 结束位置
                let end = start + chunkSize
                // 进行切片
                const blob = file.slice(start, end)
                const spark = new SparkMD5.ArrayBuffer()
                const fileReader = new FileReader()
                // 读取
                fileReader.readAsArrayBuffer(blob)
                // 监听读取完毕
                fileReader.onload = (e) => {
                    // 生成MD5值
                    spark.append(e.target.result)
                    resolve({
                        start,
                        end,
                        index,
                        MD5: spark.end(),
                        blob
                    })
                }
            })
        }
    </script>
</body>

</html>
  1. 后端
var express = require('express');
const fs = require("fs")
const crypto = require('crypto');
const SparkMD5 = require('spark-md5')
const path = require("path")
var router = express.Router();
const multer = require('multer')
const upload = multer({ dest: 'uploads/' })

/* GET home page. */
router.post('/upload', upload.fields([
  { name: "chunk", maxCount: 100 },
]), function (req, res, next) {
  // 分片数组
  const chunks = req.files.chunk;
  // 分片总数
  const totalChunks = chunks.length;
  // 生成随机文件名
  function generateRandomFileName() {
    return crypto.randomBytes(16).toString('hex');
  }

  // 输出文件路径,包含随机文件名
  const randomFileName = generateRandomFileName();
  const outputFile = path.join('outChunks/', randomFileName);

  // 确保 outChunks文件夹存在
  if (!fs.existsSync('outChunks')) {
    fs.mkdirSync('outChunks');
  }
  // 确保输出文件不存在或清空已存在的文件
  fs.writeFileSync(outputFile, '');

  // 使用流进行文件合并
  const outputStream = fs.createWriteStream(outputFile);

  let currentChunk = 0;

  function writeNextChunk() {
    if (currentChunk >= totalChunks) {
      // 所有分片写入完毕,关闭输出流
      outputStream.end();
      return;
    }

    const chunkStream = fs.createReadStream(chunks[currentChunk].path);

    chunkStream.pipe(outputStream, { end: false });

    chunkStream.on('end', () => {
      // console.log(`分片 ${chunks[currentChunk].originalname} 成功写入`);
      currentChunk++;
      writeNextChunk(); // 写入下一个分片
    }).on('error', (err) => {
      console.error(`分片 ${chunks[currentChunk].originalname} 写入错误`, err);
      // 可能需要在这里处理错误,比如删除已写入的部分文件,记录日志等
    });
  }

  // 监听重组结束
  outputStream.on('finish', () => {
    chunks.forEach(chunk => {
      fs.unlink(chunk.path, (err) => {
        if (err) {
          console.error(`删除分片文件 ${chunk.path} 出错:`, err);
        } else {
          // console.log(`分片文件 ${chunk.path} 已成功删除`);
        }
      });
    });
    // 计算文件完整性
    const spark = new SparkMD5.ArrayBuffer()
    spark.append(outputFile)
    if(req.body.MD5 === spark.end()){
      console.log("文件完整")
    }else{
      console.log("文件不完整")
    }
  }).on('error', (err) => {
    console.error('文件合并过程中出现错误:', err);
  });
  // 开始写入第一个分片
  writeNextChunk();
});

module.exports = router;

上传进度跟踪

目前fetch并不能跟踪文件的上传进度,所以这里使用的是XMLHttpRequest

<input type="file" id="file">
<button id="uploadClick">点击上传</button>
<progress id="progress" value="0" max="100"></progress>
<script>
    const progress = document.querySelector("#progress")
    uploadClick.onclick = () => {
        const file = document.querySelector("#file").files[0]
        const formData = new FormData()
        formData.append("file", file)

        // 发送请求
        const xhr = new XMLHttpRequest()
        xhr.open('post', 'http://127.0.0.1:3000/upload')
        xhr.onload = (res) => {
            console.log('上传成功', xhr.responseText)
        }
        xhr.onloadstart = () => {
            console.log("上传开始!")
        };
        xhr.upload.onprogress = (e) => {
            // 当前进度
            const currentPro = e.loaded/file.size * 100
            console.log("当前进度", currentPro)
            progress.value = currentPro
        }
        xhr.send(formData);   
    }
</script>