express+mysql+vue,从零搭建一个商城管理系统7--文件上传,大文件分片上传

提示:学习express,搭建管理系统

文章目录


前言

需求:主要学习express,所以先写service部分

一、安装multer,fs-extra

c 复制代码
npm install multer --save
npm install fs-extra --save

二、新建config/upload.js

upload.js

c 复制代码
const fs = require('fs');
const fsExtra = require('fs-extra');
const path = require('path');
const rootDir = path.resolve(__dirname,'../upload/');
const temporaryDir = path.resolve(__dirname,'../upload/temporary/');
const errFun = (msg,code)=>{
    return {
        code:code||500,
        success:false,
        msg:msg||'操作失败'
    }
}
const sucFun = (data,msg)=>{
    return {
        code:200,
        success:true,
        msg:msg||'操作成功',
        data,
    }
}
const uploadUtil = {
    upload:(req)=>{
        const { fileMD5,chunkMD5 } = req.body;
        return new Promise ((resolve,reject)=>{
            const folderPath = temporaryDir+'/'+fileMD5;
            const filePath = temporaryDir+'/'+fileMD5+'/'+chunkMD5
            if(!fs.existsSync(folderPath))fs.mkdirSync(folderPath);
            fs.writeFile(filePath,req.file.buffer,(err)=>{
                if(err)reject(errFun('切片上传失败'));
                resolve(sucFun({},'上传成功'));
            })
        });
    },
    merge:async(req)=>{
        const {fileMD5,chunkMD5Arr,type} = req.body;
        const folderPath = temporaryDir+'/'+fileMD5;
        let chunkBufferArr = [];
        for(let i=0;i<chunkMD5Arr.length;i++){
            let chunkBuffer = await fs.readFileSync(folderPath+'/'+chunkMD5Arr[i]);
            chunkBufferArr.push(chunkBuffer);
        }
        console.log(chunkBufferArr)
        let fileurl = rootDir+'/images/'+fileMD5+'-'+(new Date().getTime())+'.'+type;
        fs.writeFile(fileurl,Buffer.concat(chunkBufferArr),(err)=>{
            if(err)errFun(fileMD5+'文件切片merge失败');
            sucFun({url:fileurl},'文件切片merge成功');
            //删除临时文件夹以及文件夹下的所有文件
            fsExtra.removeSync(folderPath);
        });
    }
}
module.exports = uploadUtil;

三、新建routes/upload.js

c 复制代码
const uploadUtil = require('../config/upload');
//文件流的key与multer的single方法的参数param,必须保持一致,不一致接收不到文件流
 //fd.append('xxx',chunk);
 //multer().single('xxxx');

const multer = require('multer');
const chunk = multer().single('chunk');

const uploadRoutes = (router)=>{
    router.post('/upload/upload',chunk,async (req,res)=>{
        const result = await uploadUtil.upload(req);
        res.json(result);
    });
    router.post('/upload/merge',async (req,res)=>{
        const result = await uploadUtil.merge(req);
        res.json(result);
    });
}
module.exports = uploadRoutes;

四、修改routes下的index.js

c 复制代码
const userRoutes = require('./user');
const uploadRoutes = require('./upload');
const routes = (router)=>{
    //user路由
    userRoutes(router);
    //upload路由
    uploadRoutes(router);
}

module.exports = routes;

五、修改index.js

注释jwt的token验证,方便测试。如果打开验证,需要通过login接口生成token,上传文件request请求头部携带

c 复制代码
const express = require('express');
const app = express();
const router = express.Router();
const jwt = require('./config/jwt');

const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

const port = 1990;

app.all('*',  (req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*');
    res.header("Access-Control-Allow-Headers", "Authorization,token,content-type");
    res.header('Access-Control-Allow-Methods', '*');
    res.header('Content-Type', 'application/json;charset=utf-8');
    next();
});

//全局验证token
// app.use('/*',(req,res,next)=>{
//     let notValidateData = ['/user/login','/user/register'];
//     if(notValidateData.indexOf(req.baseUrl)>-1)return next();
//     if((jwt.verify(req.headers.authorization||'')||{}).success)return next();
//     return res.json({success:false,code:500,msg:'token验证失败'});
// })

//初始化路由
require('./routes/index')(router);

app.use('/', router);
app.listen(port,()=>{
    console.log('http://localhost:'+port);
})

六、新建上传文件test.html

c 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test upload</title>
</head>
<body>
    <input type="file" onchange="goUpload(this.files)">
    <script type="text/javascript" src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.js"></script>
    <script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/blueimp-md5/2.16.0/js/md5.js"></script>
    <script>
        const reader = new FileReader();
        const instance = axios.create({
            baseURL: 'http://localhost:1990/', //后台接口url+port
            timeout: 5000,
            headers: {
                'Content-Type': 'multipart/form-data',
                "authorization":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyTmFtZSI6Imxvbmdsb25nYWdvMSIsInBhc3N3b3JkIjoibG9uZzEyMzQ1NiIsImlhdCI6MTcwOTIwMjQ1NiwiZXhwIjoxNzA5MjA2MDU2fQ.XztBbjm2BbeQB7-OIXX040xuNxR5gnioCCgNV2c5NGI",
            },
            withCredentials: false, // default
            responseType: 'json', // default
            maxContentLength: 2000,
        });
        //上传文件计数对象
        let postFileObj = {};
        //上传切片计数对象
        let postChunkObj = {};
        //组件上传文件
        const goUpload = (files)=>{
            postFileObj = {};
            postChunkObj = {};
            upload(files[0])
        }
        //上传文件
        const upload = async (file)=>{
            //文件blob
            const fileBlob = file.slice(0,file.size);
            //文件hash,用作后端新建临时文件夹名和上传完成的文件命名参数  并且,相同文件内容(文件名可不同)进行上传,生成的fileMD5是相同的,如有需要,可根据存储的文件名判断,不需要重新上传
            const fileMD5 = await blodToString(fileBlob);
            //切片数组
            const chunks = fileToChunks(file);
            //切片hash 用来命名生成的临时文件,并在所有切片上传完成,按顺序,读取切片,生成文件,如果不按照顺序,生成文件会因为写入顺序不对而乱码
            const chunkMD5Arr = [];
            //初始化当前文件上传计数
            if(!postFileObj[fileMD5])postFileObj[fileMD5] = {count:1}
            for(let i=0;i<chunks.length;i++){
                const chunkMD5 = await blodToString(chunks[i]);
                chunkMD5Arr.push(chunkMD5);
                //初始化key为fileMD5的切片组
                postChunkObj[fileMD5] = {};
                //初始化key为fileMD5的切片组中key为chunkMD5的切片计数对象
                postChunkObj[fileMD5][chunkMD5] = {success:false,count:1}
                await postChunk(chunks[i],fileMD5,chunkMD5);
            }
            let postAllChunk = true;
            //判断切片是否都已上传完成
            for(let key in postChunkObj[fileMD5]){
                if(!postChunkObj[fileMD5][key].success){
                        postAllChunk = false;
                        break
                }
            }
            // 所有切片都已上传成功
            if(postAllChunk){
                //调用merge方法
                const typeArr = file.name.split('.');
                mergeChunks(fileMD5,chunkMD5Arr,typeArr[typeArr.length-1]);
            }else{
                //有切片上传失败,重新执行上传
                postFileObj[fileMD5].count++;
                if(postFileObj[fileMD5].count>10) return console.log('文件上传失败');
                //重新上传
                await upload(file);
            }
        }
        //blob转换成string
        const blodToString = (blob)=>{
            return new Promise((resolve,reject)=>{
                reader.onloadend = (e)=>{
                    resolve(md5(e.target.result));
                }
                reader.readAsText(blob);
            });
        }
        //file转换成chunks
        const fileToChunks = (file)=>{
            //切片数组
            let chunks = [];
            //每个chunk大小
            const chunkSize = 1024*128;
            //转换成多少个chunk
            const chunkCount = Math.ceil(file.size/chunkSize);
            for(let i=0;i<chunkCount;i++){
                if(i==chunkCount-1){
                    chunks.push(file.slice(i*chunkSize,file.size));
                }else{
                    chunks.push(file.slice(i*chunkSize,(i+1)*chunkSize));
                }
            }
            return chunks;
        }
        //上传切片
        const postChunk = (chunk,fileMD5,chunkMD5)=>{
            let fd = new FormData();
            //文件流的key与multer的single方法的参数param,必须保持一致,不一致接收不到文件流
            /**
             * fd.append('xxx',chunk);
             * multer().single('xxxx');
             * **/
            fd.append('chunk',chunk);   
            fd.append('fileMD5',fileMD5);
            fd.append('chunkMD5',chunkMD5);
            let postCount = 0;
            return new Promise((resolve,reject)=>{
                instance.post('upload/upload',fd).then((res)=>{
                    if(res.data&&res.data.success){
                        postChunkObj[fileMD5][chunkMD5].success = true;
                        resolve(console.log(`上传切片${chunkMD5}成功`));
                    }else{
                        //记录当前切片上传次数
                        postChunkObj[fileMD5][chunkMD5].count++;
                        //当前切片上传超过10次,终止上传
                        if(postChunkObj[fileMD5][chunkMD5].count<11) { 
                            //上传失败,重新上传
                            postChunk(chunk,fileMD5,chunkMD5);
                        }
                    }
                    resolve(console.log(`上传切片${chunkMD5}失败`))
                })
            });
        }
        //合并多个chunk
        mergeChunks = (fileMD5,chunkMD5Arr,type)=>{
            // let fd = new FormData();
            // fd.append('chunk',Buffer.form(''));   
            // fd.append('fileMD5',fileMD5);
            // fd.append('chunkMD5Arr',chunkMD5Arr);
            // fd.append('type',type);
            // instance.post('upload/merge',fd).then((res)=>{

            // });
            axios.post('http://localhost:1990/upload/merge',{fileMD5,chunkMD5Arr,type}).then((res)=>{

            });
        }
    </script>
    
</body>
</html>

七、开启jwt验证token,通过login接口生成token

添加用户

url:http://localhost:1990/user/login

name:/user/login

parans:{

"userName": "longlongago1",

"password": "long123456"

}

总结

踩坑路漫漫长@~@

相关推荐
minji...11 小时前
MySQL数据库 (七) MySQL表的基本查询(上),insert、replace、select、where、order by
数据库·mysql·select·replace·insert·order by·where
英勇无比的消炎药12 小时前
前端提效神器全新AI组件库TinyRobot改写日常开发模式
前端·vue.js
英勇无比的消炎药12 小时前
前端提效神器TinyRobot
前端·vue.js
CDwenhuohuo12 小时前
uni 背景色渐变 全屏
前端·javascript·vue.js
爱怪笑的小杰杰12 小时前
Vue 项目交付第三方开发,如何隐藏核心 JS 源码?
前端·javascript·vue.js
小二·12 小时前
Vue 3 组合式 API 进阶实战
前端·javascript·vue.js
折戟不必沉沙13 小时前
mysql忘记密码
数据库·mysql
kuonyuma13 小时前
MyBatis入门·注解操作
java·spring boot·mysql·spring·mybatis
聪明努力的积极向上13 小时前
【claude code】MySQL MCP 配置完整指南
数据库·mysql·ai编程
DIY源码阁13 小时前
JavaSwing酒店管理系统 - MySQL版
java·mysql·eclipse