左手前端,右手后端,浅谈前后端如何实现图片以及音频和文本之类的上传(超详细~手把手教会)?

862 阅读3分钟

注:本文更适合于动手去敲(针对于新手而言),这样更能理解每一行代码所存在的意义,同时也更能理解API


刚开始写一些全栈小项目的人都有一些苦恼,就是如何将前端传图片传给后端,前端该怎么和后端强强联手将这个图片妥妥的放好?下面就为大家展示一个demo,去实现如何前端传图片给后端。

前端部分:(用的是vite去构建的React项目,然后代码是截取的其中一个片段)

import React from 'react';

import axios from 'axios';

export const PublicPicture = () => {
    const submitUpload = () => {
        // 切割文件 
        let chunkSize = 2 * 1024 * 1024 // 限制单个最大为2M
        // 拿到文件 
        let file = document.getElementById('f1').files[0] // 这里展示的是一个图片的上传,如果需要多个图片只需要将这个做一个forEach循环就OK了
        //  打标记, 时间戳
        let token = (+ new Date())// 用时间戳来作为传数的唯一标识号
        let name = file.name// 获取文件的名字
        let chunkCount = 0// 传文件时,作为一个累加的数
        let sendChunkCount = 0// 对于传出的文件,作为一个计数
        let chunks = []// 将大于2M的文件分割成为一个数组
        // 拆文件
        if (file.size > chunkSize) {
            let start = 0, end = 0;
            while (true) {
                end += chunkSize
                let blob = file.slice(start, end)// 将file切割成单个为2M的数据流
                start += chunkSize
                if (!blob.size) { //  截不到东西
                    break;
                }
                chunks.push(blob) // 保存片段
            }
        } else {
            chunks.push(file.slice(0))// 文件小于2M直接一次性传给后端
        };
        chunkCount = chunks.length; // 获取数组的长度,依次向后端发起请求
        for (let i = 0; i < chunkCount; i++) {
            let fd = new FormData();//new一个formData [不懂的可以戳这个链接](https://developer.mozilla.org/zh-CN/docs/Web/API/FormData)
            fd.append('token', token)//时间戳 作为唯一标记
            fd.append('f1', chunks[i])//片段
            fd.append('index', i)// 数量
            // 拿到了每次需要的,此时只需要朝后端发起请求就OK了
            AxiosPost(fd, () => {
                sendChunkCount += 1
                if (sendChunkCount === chunkCount) {
                    let formD = new FormData()
                    formD.append('type', 'merge')
                    formD.append('token', token)
                    formD.append('chunkCount', chunkCount)
                    formD.append('filename', name)
                    AxiosPost(formD, undefined)
                }
            })
        }
    }
    // 每次传完之后都会进行一次判断, 如果没有传完则会继续调用AxiosPost() 如果传完了则会发送合并请求
    const AxiosPost = (fd, callback) => {
        post('/', fd).then(res => {
            console.log(res);
            if (res.data.code === 202) {
                callback && callback()
            }
        })
    }
    return <>
        <br />
        <input type="file" name="" id="f1" />
        <br />
        <button id="btn-submit" onClick={submitUpload}>上传</button>
    </>
}

后端部分:(后端我采用的是koa作为框架)

const Koa = require('koa2')
const koaBody = require('koa-body')//koa-body 是一个可以帮助解析 http 中 body 的部分的中间件,包括 json、表单、文本、文件等。
const cors = require('koa2-cors')// 解决跨域
const path = require('path')
const fs = require('fs') // 文件读写

const port = process.env.port || '3001' // 启动默认端口,如果被占用则用第二个选择
const uploadHost = `http://loaclhost:${port}`
const app = new Koa();

app.use(cors());//解决跨域问题

app.use(koaBody({
    formidable: {
        uploadDir: path.resolve(__dirname, './static'),// 可以填一个路径
        maxFileSize: 10 * 1024 * 1024 // 默认2M
    },
    multipart: true ////解析多个文件
}))

app.use((ctx) => {
    // 处理接受文件的片段 为ctx.request.files
    let files = ctx.request.files ? ctx.request.files.f1 : [] // 找前端传过来的f1
    let body = ctx.request.body // 简写
    let result = []
    let fileToken = body.token // 前端传过来的时间戳
    let fileIndex = body.index // 前端传过来的下标
    if (files && !Array.isArray(files)) {
        files = [files] //如果不是数组,将files变成数组
    }
    files && files.forEach(item => {
        let path = item.path.replace(/\\/g, '/') // 正则,将例如C:\workspace 转化为 C:/workspace
        let fname = item.name  // 拿到每一项的名字 blob
        let nextPath = path.slice(0, path.lastIndexOf('/') + 1) + fileIndex + '-' + fileToken; // 下一个路径
        if (item.size > 0 && path) {
            fs.renameSync(path, nextPath) // 改路径的名字。改成自己取的名字 例如  C:/workspace/js/bigFile/server/static/upload_1e387a3f62851216089e83f4c1a5181b' 改成 C:/workspace/js/bigFile/server/static/Index-fileToken;
            console.log(nextPath);
            result.push(uploadHost + nextPath.slice(nextPath.lastIndexOf('/') + 1))// 截取文件片段名字
        }
    })

    // 合并文件片段
    if (body.type === 'merge') {
        let filename = body.filename //拿到最后一次传过来的参数,包含文件名字
        let chunkCount = body.chunkCount // 拿到总数
        let folder = path.resolve(__dirname, './static') + '/'; // 拿到路径
        let writeStram = fs.createWriteStream(`${folder}${filename}`) //创建出写文件的流,接受的参数是文件的路径
        let cindex = 0
        // 合并
        const fnMergeFile = () => {
            let fname = `${folder}${cindex}-${fileToken}`;
            let readStream = fs.createReadStream(fname); // 读文件流
            readStream.pipe(writeStram, { end: false }) // 会自己去找相关的文件,然后将其合并到第一个参数里边去,如果文件不完整,则会抛出错误
            readStream.on('end', () => {
                fs.unlink(fname, (err) => {//错误捕获
                    if (err) {
                        throw err
                    }
                })
                if (cindex + 1 < chunkCount) {
                    cindex += 1
                    fnMergeFile()
                }
            })
        }
        fnMergeFile()

        ctx.body = {
            code: 200,
            msg: `上传成功!`
        }
    } else {
        ctx.body = {
            code: 202,
            fileUrl: `${JSON.stringify(result)}`
        }
    }
})

app.listen(port, () => {
    console.log(`服务启动在${port}`);
})

上边前后端代码可以直接拿去使用,没有任何副作用(除了一些基本的包的安装和react的构建)。 然后最后就是,本人是一枚前端小白,如果上述有不对或者不理解的东西欢迎大家留言或者私聊讨论指出,最后,码字不易,如果这篇文章对你有所帮助请帮忙点个赞吧~