原生js+express分片上传大文件

原生js+express分片上传大文件

原理

  • 前端靠blob切好每个部分,发送至服务端
  • 服务端收到文件后保存唯一部分,全部上传完成后合并文件

依赖包

要点

  • 前端判断文件大小
let end = start + chunkSize >= f.size ? f.size : start + chunkSize;
  • 前端切片部分
// blob 和 file 相互转换
let aimfile = new File(
    [f.slice(start, end)],
    `${f.name.split('.')[0]}.${index}.${f.name.split('.')[1]}`, {
        type: f.type,
        lastModified: Date.now()
    }
)
  • 前端ajax方法封装
function ajax(file, cb, index, method = 'POST', url = 'http://localhost:5000/upload') {
    let xhr = new XMLHttpRequest()
    xhr.open(method, url+'?current='+index, true)
    xhr.send(file)
    xhr.onreadystatechange = () => {
        if (xhr.readyState === 4 && xhr.status === 200) {
            cb(xhr.responseText)
        }
    }
}
  • 后端接口请求处理

最关键的信息,storage
磁盘存储引擎 (DiskStorage)
磁盘存储引擎可以让你控制文件的存储。

//官方介绍
var storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, '/tmp/my-uploads')
  },
  filename: function (req, file, cb) {
    cb(null, file.fieldname + '-' + Date.now())
  }
})

var upload = multer({ storage: storage })

有两个选项可用,destination 和 filename。他们都是用来确定文件存储位置的函数。

destination 是用来确定上传的文件应该存储在哪个文件夹中。也可以提供一个 string (例如 ‘/tmp/uploads’)。如果没有设置 destination,则使用操作系统默认的临时文件夹。

注意: 如果你提供的 destination 是一个函数,你需要负责创建文件夹。当提供一个字符串,multer 将确保这个文件夹是你创建的。

filename 用于确定文件夹中的文件名的确定。 如果没有设置 filename,每个文件将设置为一个随机文件名,并且是没有扩展名的。

注意: Multer 不会为你添加任何扩展名,你的程序应该返回一个完整的文件名。

每个函数都传递了请求对象 (req) 和一些关于这个文件的信息 (file),有助于你的决定。

注意 req.body 可能还没有完全填充,这取决于向客户端发送字段和文件到服务器的顺序。

  • 加工处理
let timer = null
let storage = multer.diskStorage({
    destination: function (req, file, cb) {
        // 新增目录存放文件
        const chunksPath = path.join('public/upload', file.originalname.split('.')[0]);
        if (!fs.existsSync(chunksPath)) {
            fs.mkdirSync(chunksPath, { recursive: true }); // 新建文件夹
        }

        // 配置文件保存路径
        cb(null, path.join('public/upload', file.originalname.split('.')[0]))

        // 合并文件并删除目录
        clearTimeout(timer)
        timer = setTimeout(async () => {
            // 合并文件(获取子文件名称)
            await fs.readdir(chunksPath, (err, files) => {
                if (err) throw err
                files.map((res) => {
                    // 同步合并文件
                    fs.appendFileSync(
                        `public/upload/${file.originalname.split('.')[0]}.${file.originalname.split('.')[2]}`,
                        fs.readFileSync(path.join(chunksPath, res)), // 读取文件
                        (err) => { if (err) throw err }
                    )
                });
            })

            // 删除文件
            const delFile = () => {
                fs.readdir(chunksPath, (err, files) => {
                    if (err) throw err
                    if (!files.length) {
                        delFile()
                    } else {
                        files.map(res => {
                            fs.unlinkSync(path.join(chunksPath, res), (e) => { if (err) throw err });
                        })
                    }
                })
            }
            await delFile()

            // 删除文件夹
            const del = () => {
                // 判断子文件是否为空
                fs.readdir(chunksPath, (err, files) => {
                    if (files.length != 0) {
                        del()
                    } else {
                        // 为空则删除文件夹
                        fs.rmdir(chunksPath, (err) => { if (err) throw err })
                    }
                })
            }
            await del()
        }, 500)
    },
    filename: function (req, file, cb) {
        const split = file.originalname.split('.')
        cb(null, `${split[0]}-${split[1]}`) // 文件名
    }
});

let upload = multer({ storage: storage });
app.use('/upload', upload.single('file'), function (req, res, next) {
    if (req.file) res.send('file ' + req.query.current + ' upload success!')
})

使用中间件处理请求文件。

全部

前端部分

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #pro {
            position: relative;
            margin-top: 10px;
            width: 500px;
            height: 10px;
            background-color: #ccc;
            border-radius: 10px;
            overflow: hidden;
        }
        #pro-current {
            position: absolute;
            left: 0;
            top: 0;
            width: 0;
            height: 10px;
            border-radius: 10px;
            background-color: #f0f;
            transition: all .5s;
        }
    </style>
</head>

<body>
    <input type="file" id="file">
    <!-- 进度条 -->
    <div id="pro">
        <div id="pro-current">
        </div>
    </div>
    <span id="pro-current-num"></span>
    <script>
        let [file, pro, num] = [document.getElementById('file'), document.getElementById('pro-current'), document.getElementById('pro-current-num')]

        file.onchange = () => {
            upload()
        }

        function upload(start = 0, index = 0, chunkSize = 1024 * 1024) {
            let f = file.files[0]

            if (start >= f.size) return
            let end = start + chunkSize >= f.size ? f.size : start + chunkSize

            // blob 和 file 相互转换
            let aimfile = new File(
                [f.slice(start, end)],
                `${f.name.split('.')[0]}.${index}.${f.name.split('.')[1]}`, {
                    type: f.type,
                    lastModified: Date.now()
                }
            )

            let form = new FormData()
            form.append('file', aimfile)

            ajax(form, (flag) => {
                if (flag) {
                    pro.style.width = end / f.size * 500 + 'px';
                    num.innerText = end / f.size * 500 / 5 + '%';
                    upload(end, ++index)
                }
            }, index)
        }

        function ajax(file, cb, index, method = 'POST', url = 'http://localhost:5000/upload') {
            let xhr = new XMLHttpRequest()
            xhr.open(method, url+'?current='+index, true)
            xhr.send(file)
            xhr.onreadystatechange = () => {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    cb(xhr.responseText)
                }
            }
        }
    </script>
</body>
</html>

后端部分

const express = require('express')
const path = require('path')
const multer = require('multer')
const fs = require('fs')

let timer = null
let storage = multer.diskStorage({
    destination: function (req, file, cb) {
        // 新增目录存放文件
        const chunksPath = path.join('public/upload', file.originalname.split('.')[0]);
        if (!fs.existsSync(chunksPath)) {
            fs.mkdirSync(chunksPath, { recursive: true }); // 新建文件夹
        }

        // 配置文件保存路径
        cb(null, path.join('public/upload', file.originalname.split('.')[0]))

        // 合并文件并删除目录
        clearTimeout(timer)
        timer = setTimeout(async () => {
            // 合并文件(获取子文件名称)
            await fs.readdir(chunksPath, (err, files) => {
                if (err) throw err
                files.map((res) => {
                    // 同步合并文件
                    fs.appendFileSync(
                        `public/upload/${file.originalname.split('.')[0]}.${file.originalname.split('.')[2]}`,
                        fs.readFileSync(path.join(chunksPath, res)), // 读取文件
                        (err) => { if (err) throw err }
                    )
                });
            })

            // 删除文件
            const delFile = () => {
                fs.readdir(chunksPath, (err, files) => {
                    if (err) throw err
                    if (!files.length) {
                        delFile()
                    } else {
                        files.map(res => {
                            fs.unlinkSync(path.join(chunksPath, res), (e) => { if (err) throw err });
                        })
                    }
                })
            }
            await delFile()

            // 删除文件夹
            const del = () => {
                // 判断子文件是否为空
                fs.readdir(chunksPath, (err, files) => {
                    if (files.length != 0) {
                        del()
                    } else {
                        // 为空则删除文件夹
                        fs.rmdir(chunksPath, (err) => { if (err) throw err })
                    }
                })
            }
            await del()
        }, 500)
    },
    filename: function (req, file, cb) {
        const split = file.originalname.split('.')
        cb(null, `${split[0]}-${split[1]}`) // 文件名
    }
});

let upload = multer({ storage: storage });

let app = express()

// 解决跨域
app.all('*', (req, res, next) => {
    res.header('Access-Control-Allow-Credentials', 'true')
    res.header('Access-Control-Allow-Origin', req.headers.origin)
    res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS')
    res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Token, Accept, X-Requested-With')
    next()
})

app.use('/public', express.static(path.join(__dirname, 'public')))

app.use('/upload', upload.single('file'), function (req, res, next) {
    if (req.file) res.send('file ' + req.query.current + ' upload success!')
})

app.listen(5000, () => console.log('Example app listening on port 5000!'))

参考文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值