小工具知识网

当前位置: 首页 > 行业知识 > vue+Nodejs+Koa搭建前后端系统(九)-- 上传图片

行业知识

vue+Nodejs+Koa搭建前后端系统(九)-- 上传图片

2026-11-08 10:57:26 来源:互联网转载

web2.0的到来使网页世界正式进入了寒武纪,各式各样的多媒体资源屡见不鲜,上传资源变得刻不容缓!

前言

本文是在该系列的基础上,针对前后端代码的修改。

准备

HTTP上传图片时Content-Type值常见的有2种:application/json和multipart/form-data

前端准备

修改axios配置

/** /src/https.ts */const http: MyAxiosInstance = axios.create({    baseURL: process.env.NODE_ENV === 'production' ? httpHost : '/nodeApi/',    /**New Code Start*/    headers: {        'Content-Type': 'application/json',//将axiox的Content-Type值默认为application/json    },    /**New Code End*/    timeout: 60000,});/**其他代码省略**//**请求拦截器 */http.interceptors.request.use(function (config) {    const token = window.localStorage.getItem('token');    const username = userStore.userName;    if (token) {        config.headers.authorization = token;    }    /**New Code Start*/    //若请求Content-Type值为application/json,则处理请求数据    if (config.headers['Content-Type'] === 'application/json' && username && config.data) {    /**New Code End*/        try {            config.data = { username: username, ...config.data }        } catch (e) {            console.error(`请求拦截error:${e}`)        }    }    return config;}, function (error) {    return Promise.reject(error);});

上述代码修改了请求拦截,即:若请求类型为application/json时,则处理;否则(主要是针对multipart/form-data),不处理。

后端准备

1.安装koa-body

npm install koa-body

2.安装hexoid

npm install --save hexoid

3.安装dayjs

npm install dayjs

4.新建路由文件 /routes/files.js (用于接收上传图片请求并处理)

5.新建 /public/uploads/ 目录,用于存放上传的图片

数据库准备(非必须)

新建一张用于存储上传图片信息的表

CREATE TABLE user_uploads(      src VARCHAR(510) NOT NULL PRIMARY KEY COMMENT '文件地址',    create_time TIMESTAMP NOT NULL COMMENT '上传时间',    title VARCHAR(255) COMMENT '文件标题',    description VARCHAR(1000)  COMMENT '文件描述',    username VARCHAR(20) COMMENT '上传人',    group_by VARCHAR(20) COMMENT '分组',    mimetype VARCHAR(255) COMMENT 'MIME类型') COMMENT '文件上传维护表';

对之前代码修改

为了更加工整一些,作出如下修改:

1.修改create_user 表

#删除create_user 表的id列ALTER TABLE create_user DROP COLUMN id;#设置create_user 表的username列为主键ALTER TABLE create_user ADD PRIMARY KEY (username);

修改后的表结构

2.后端代码新增配置页

为了统一后端的一些配置项,在后端项目根目录新建index.config.js文件

代码为:

/** /index.config.js */module.exports = {//不验证用户名的接口    no_verify: ["/login/loginIn", "/token/refresh", "/users/register", "/files/upload"],    //静态文件夹路径    static_basepath:'./public',    //上传文件存储的目录    static_uploadpath:'uploads'}

然后修改引用的地方

/** /app.js *//** 其他代码省略 */const { verifyToken } = require("./middleware/jwt_copy1.js");const { no_verify, static_basepath } = require("./index.config.js");app.use(require("koa-static")(path.join(static_basepath)));//__dirname + "/public"app.use(verifyToken({ no_verify: no_verify }));

前端处理上传图片的几种方式

使用 FormData 对象

可以使用原生form标签,实现FormData 对象上传图片

         上传

但该方法有默认行为,比如上传成功后会跳转页面,而且不够灵活。鉴于此,可用js配合,使其更加灵活:

        上传import { reactive, ref } from "vue";import http from "@/http";import { ElMessage } from "element-plus";//要上传图片的File对象数组,用于提交后台const files = ref([]);//要上传图片的DataUrl数组,用于预览图片(这里也可以用BlobUrl)const previewImgs = ref([]);const getFiles = (e: any) => {  files.value = files.value.concat([...e.target.files]);  files.value.forEach((file: File, index: number) => {    const reader = new FileReader();    reader.readAsDataURL(file);    reader.addEventListener("load", () => {      previewImgs.value[index] = reader.result;    });  });};const upload = () => {  //创建formData对象  const formData = new FormData();  //给formData对象添加files[]成员,多图片  for (let i in files.value) {    formData.append("files[]", files.value[i]);  }  formData.append("username", 'xyz');  http.post("files/upload", formData, {      headers: {        "Content-Type": "multipart/form-data",      },  }).then((data: any) => {      ElMessage({        message: "上传成功",        type: "success",      });    }).catch((err: any) => {      ElMessage({        message: err.message,        type: "error",      });    });};

使用 Base64 编码

这种方法将文件转换成 Base64 编码的字符串,然后通过普通的 JSON 格式发送给服务器。这种方式适用于较小的文件,因为 Base64 编码会增加数据大小。

    上传import { reactive, ref } from "vue";import http from "@/http";import { ElMessage } from "element-plus";//要上传图片的File对象数组,用于提交后台const files = ref([]);const getFiles = (e: any) => {  files.value = files.value.concat([...e.target.files]);};/***** 将文件转换为base64*** files 文件数组,File[]*** callback 解析完成的回调方法,该回调方法第一个参数是解析的base64数组,第二个参数是对应的文件名称数组*/const readFiles = (files: File[] = [], callback: (f: string[],n:string[]) => void) => {  const len = files.length;  let readNum = 0;  const results = [];  for (let i = 0; i  {      readNum++;      results.push(e.target.result);      if (readNum === len) {        callback(results,files.map((item: any) => item.name));      }    };    reader.onerror = () => {      readNum++;      if (readNum === len) {        callback(results,files.map((item: any) => item.name));      }    };    reader.readAsDataURL(files[i]);  }};const upload = () => { readFiles(files.value, (base64List: string[], filenameList: string[]) => {    http.post("files/upload", {      files: base64List,      filename: filenameList,    }).then((data: any) => {      ElMessage({         message: "上传成功",         type: "success",       });    }).catch((err: any) => {       ElMessage({         message: err.message,         type: "error",       });    });  });};

后端处理上传图片

1.编写路由文件

/** /routes/files.js */const path = require("path");const { uploadFiles, removeFiles } = require("../module/files")const { koaBody } = require('koa-body');const router = require('koa-router')();const { jsonable } = require('../middleware/files')const fs = require("fs");const { static_basepath, static_uploadpath } = require("../index.config");const dayjs = require('dayjs');/** 上传图片 */router.post('/upload', async (ctx, next) => {    const content_type = ctx.request.header['content-type']    if (content_type === 'application/json') {        return jsonable()(ctx, next);    } else if (content_type.includes('multipart/form-data')) {        return koaBody({            // 支持多文件格式            multipart: true,            formidable: {                // 上传目录                uploadDir: path.join(static_basepath, static_uploadpath),                // 保留文件扩展名                keepExtensions: true,                //图片上传前的事件句柄                onFileBegin(formName, file) {                    ctx.request.formName = formName;                    //上传图片的根目录                    const dirpath = path.join(static_basepath, static_uploadpath, dirname);                    const D = new Date();                    //上传图片的二级目录(以‘年月日’格式为目录名)                    const dirname = dayjs(D).format("YYYYMMDD");                    //检查上传图片的目录是否存在,若不存在,则创建                    if (!fs.existsSync(dirpath)) {                        fs.mkdirSync(dirpath);                    }                    //改写文件存储的路径                    file.filepath = path.join(static_basepath, static_uploadpath, dirname, file.newFilename)                }            }        })(ctx, next)    }}, uploadFiles);module.exports = router;

此处上传图片采用koa-router中间件router.post(path,middleware,callback)格式,middleware是中间件,该中间件代码逻辑为:

判断请求的content-type值,若为application/json,利用自定义的jsonable中间件处理上传,若为multipart/form-data,利用koa-body中间件处理上传。上传图片完成后执行uploadFiles回调函出,该函数用来处理数据库。

上传的图片如图:

2.编写自定义的jsonable中间件处理application/json的请求

新建 /middleware/files.js 文件,代码如下:

/** /middleware/files.js */const path = require("path");const fs = require("fs");const { static_basepath, static_uploadpath } = require("../index.config");const dayjs = require('dayjs');const hexoid = require('hexoid');/** 生成上传图片的目录(若没有该目录则创建) */function fileDir() {    const D = new Date();    const dirname = dayjs(D).format("YYYYMMDD");    const dirpath = path.join(static_basepath, static_uploadpath, dirname)    if (!fs.existsSync(dirpath)) {        fs.mkdirSync(dirpath);    }    return dirpath;}function jsonable() {    return async (ctx, next) => {    //请求参数        const params = ctx.request.body;        //生成上传图片的目录        const dirpath = fileDir();        //获取文件数组        const files = Array.isArray(params.files) ? params.files : [params.files];        //获取文件名称数组        const filenames = params.filename || [];        //当前被处理的文件        let file = null;        //存储图片信息的数组        const filepathArr = [];        //循环处理图片(该图片为base64数据)        while (file = files.shift()) {        //当前循环的文件名        const filename = filenames.shift() || '.png';         //生成上传图片的名称--为25为的随机名称        const uuid = hexoid(25)();        //图片base64数据主体            const base64Data = file.split('base64,')            //图片的MIMEType,即Content-Type            const mimetype = base64Data[0].replace(/^data:(.+?);/, '$1');            //图片扩展文件名            const ext = '.' + filename.split(".").reverse()[0];            //图片要存储的路径            const filepath = path.join(dirpath, uuid + ext);            //将图片上传到指定路径            fs.writeFileSync(filepath, Buffer.from(base64Data[1], 'base64'));            //获取图片信息对象            const f = fs.statSync(filepath)            filepathArr.push({ filepath: filepath, mimetype, lastModifiedDate: f.birthtime })        }        /** 改写请求参数以备后续使用(这里是模拟koa-body) Start*/        ctx.request.formName = 'file[]';        ctx.request.files = { [ctx.request.formName]: filepathArr };        /** 改写请求参数以备后续使用(这里是模拟koa-body) End*/        return next();    }}module.exports = {    jsonable,}

该中间件用于处理Content-Type为application/json的上传图片请求,图片格式须为base64,可以是多图上传。

3.编写上传图片完成后的回调方法

新建 /module/files.js 文件,其代码如下:

/** /module/files.js */const fs = require('fs')const path = require("path")const os = require("os");const { URL } = require("url");const { static_basepath } = require("../index.config");const dayjs = require('dayjs');/** 获取本机IPv4地址 */function getIPAdress() {    var interfaces = os.networkInterfaces();    for (var devName in interfaces) {        var iface = interfaces[devName];        for (var i = 0; i  {            const url = `${host}${path.relative(static_basepath, item.filepath)}`;            const myURL = new URL(url);            v.push({                username: params?.username || "",//上传图片的用户                create_time: dayjs(item.lastModifiedDate).format("YYYY-MM-DD HH:mm:ss"),//上传时间                src: myURL.pathname,//上传路径                mimetype: item.mimetype,//MimeType值                 group_by: params?.group || "",//组            })        })    } else {//单文件        const url = `${host}${path.relative(static_basepath, f.filepath)}`;        const myURL = new URL(url);        v.push({            username: params?.username || "",            create_time: dayjs(f.lastModifiedDate).format("YYYY-MM-DD HH:mm:ss"),            src: myURL.pathname,            mimetype: f.mimetype,            group_by: params?.group || "",        })    }    //整合sql语句    const keys = Object.keys(v[0]);    const columns = keys.join(",");    const values = v.map(item => `(${keys.map(key => typeof item[key] === "string" ? `'${item[key]}'` : item[key]).join(",")})`)    const sql = `INSERT INTO user_uploads (${columns}) VALUES ${values.join(",")}`;//执行sql语句并响应请求    try {        const r = await ctx.db.query(sql);        ctx.response.status = 200;        ctx.body = { message: "上传成功", code: 0, data: { path: v.map(item => item.src).join(",") } };    } catch (e) {        ctx.response.status = 500;        ctx.body = { message: e, code: 99 };    }}module.exports = {    uploadFiles};

uploadFiles回调方法主要作用就是将上传的图片写入数据库,数据库表如下:

这里对于上传图片信息写入数据库是非必须的,这里我只是为了后续做图片管理打基础。

总结下我这里后端处理上传图片的逻辑:

1.在koa-router的中间件参数中处理图片上传,在其回调函数参数中处理上传图片的数据库

2.在处理图片上传中,根据请求数据的Content-Type给以不同的处理方案

后端处理删除图片

1.编写路由文件

在 /routes/files.js 文件中添加如下代码:

/** /routes/files.js *//**  其他代码省略... */router.post('/remove', removeFiles);module.exports = router;

2.编写删除图片回调方法

在 /module/files.js 文件中添加如下代码:

/** /module/files.js *//** *** 查询该用户是否有删除该图片的权限--目前规则是只有上传者才有删除权限 *** paths 要删除的图片路径,String类型,多个时用逗号分隔*** username 用户名*/function filePermissions(ctx, next, paths, username) {    const pathArr = paths.split(',');    //将传来的paths去掉域名    const pathSql = pathArr.map(item => `'${item.replace(/^(http|https):\/\/[^\/]+/, "").replace(/^\s+|\s+$/gim, "")}'`).join(",")    //sql:筛选paths的用户名和路径集合    const sql = `SELECT src,username FROM user_uploads WHERE src in (${pathSql})`;        return new Promise(async (resolve, reject) => {        try {            const r = await ctx.db.query(sql);            if (r) {//若路径存在,则继续                const v = r.filter(item => item.username !== username);                //只要有一个权限不对,则拒绝,并将拒绝的sql行返回,否则,返回图片路径                if (v.length) {                    reject(v)                } else {                    resolve(r.map(item => item.src))                }            } else {//若路径都不存在                resolve('未查到该文件权限')            }        } catch (e) {            reject(e)        }    })}/***** 删除服务器中的图片(物理删除),并将*** fileArr 要删除图片路径,数组*/function delFile(fileArr = []) {    return new Promise((resolve, reject) => {        let t = "";        const d = [];        const errInfo = {            atleastOneSucces: false,//true 至少一个删除成功            notExit: false,//至少一个文件不存在            otherExit: false,//删除文件时至少一个文件发生其他错误        }        /** 循环删除文件 */        while (t = fileArr.pop()) {            const delPath = path.join(static_basepath, t);            try {                /**                 * @des 判断文件或文件夹是否存在                 */                if (fs.existsSync(delPath)) {                    fs.unlinkSync(delPath);                    d.push({ message: "删除成功", path: t })                    errInfo.atleastOneSucces = true;                } else {                    d.push({ errMsg: "文件不存在", path: t });                    errInfo.notExit = true;                }            } catch (error) {                d.push({ errMsg: error, path: t });                errInfo.otherExit = true;            }        }        if (errInfo.atleastOneSucces) {            if (errInfo.notExit|| errInfo.otherExit) {                return resolve({ message: "部分删除成功", code: 2, data: d });            } else {                return resolve({ message: "删除成功", code: 0, data: d });            }        } else if (!errInfo.atleastOneSucces) {            return reject({ message: "删除失败", code: 3, data: d });        }    })}/** ***在数据库删除图片信息***pathArr 要删除图片路径,数组*/function delUserUploadsTableRow(ctx, next, pathArr) {    const pathSql = pathArr.map(item => `'${item.replace(/^(http|https):\/\/[^\/]+/, "").replace(/^\s+|\s+$/gim, "")}'`).join(",");    const sql = `DELETE FROM user_uploads WHERE src in (${pathSql})`;    return new Promise(async (resolve, reject) => {        try {            await ctx.db.query(sql);            resolve(true);        } catch (e) {            reject(e)        }    })}/** 删除图片总方法 */async function removeFiles(ctx, next) {    const params = ctx.request.body;    //要删除的图片路径    const paths = params.path;    //当前操作用户    const username = params.username;    let filepathArr = [];    /** 查看当前用户是否有权限 START*/    try {        filepathArr = await filePermissions(ctx, next, paths, username)    } catch (e) {        if (Array.isArray(e)) {            ctx.response.status = 403;            ctx.body = { message: '没有权限', code: 1, data: e };        } else {            ctx.response.status = 500;            ctx.body = { message: e, code: 99 };        }        return;    }    /** 查看当前用户是否有权限 END*/    /** 物理删除图片 START*///存储删除信息    let fileDeleteInfo;    try {        fileDeleteInfo = await delFile(filepathArr);    } catch (e) {        fileDeleteInfo = e;        ctx.response.status = 500;        ctx.body = { message: e, code: 99 };        return;    }    /** 物理删除图片 END*/    /** 在数据库删除已物理删除图片的信息 START*/    try {        const successDelArr = fileDeleteInfo.data.filter(item => item.message).map(item => item.path);        await delUserUploadsTableRow(ctx, next, successDelArr);    } catch (e) {        fileDeleteInfo = { message: e, code: 4 };    }    /** 在数据库删除已物理删除图片的信息 END*/    ctx.body = fileDeleteInfo;}

番外

formidable.js

上面后端代码处理formData数据使用的是koa-body中间件,而koa-body是基于formidable.js,那么就可以使用formidable.js替代koa-body:

安装formidable.js

npm install formidable@v3

代码如下:

/** /routes/files.js */const path = require("path");const { uploadFiles, removeFiles } = require("../module/files")const router = require('koa-router')();const { IncomingForm } = require('formidable');//引入formidableconst { jsonable } = require('../middleware/files')const fs = require("fs");const { static_basepath, static_uploadpath } = require("../index.config");const dayjs = require('dayjs');router.post('/upload', async (ctx, next) => {  const content_type = ctx.request.header['content-type']   if (content_type === 'application/json') {       return jsonable()(ctx, next);   } else if (content_type.includes('multipart/form-data')) {   /** formidable插件使用 START */   //配置项       const p = {        //支持多文件上传        multipart: true,        // 上传目录        uploadDir: path.join(__dirname, '../public/uploads'),        // 保留文件扩展名        keepExtensions: true,       }       const form = new IncomingForm(p);       //上传前的事件       form.on('fileBegin', (formName, file) => {           ctx.request.formName = formName;           const dirname = dayjs().format("YYYYMMDD");           const dirpath = path.join(static_basepath, static_uploadpath, dirname)           if (!fs.existsSync(dirpath)) {               fs.mkdirSync(dirpath);           }           file.filepath = path.join(static_basepath, static_uploadpath, dirname, file.newFilename)       })       await new Promise((resolve, reject) => {       //解析Node.js Request对象           form.parse(ctx.req, (err, fields, files) => {               if (err) {                   reject(err);                   return;               }               ctx.request.files = files;               ctx.request.body = {};               for (let i in fields) {                   ctx.request.body[i] = fields[i][0]               }               resolve({ fields, files });           });       });       return next();       /** formidable插件使用 START */   }}, uploadFiles);router.post('/remove', removeFiles)module.exports = router;

本文实现的后端方法不仅能上传图片,也可以上传其他文件,且可以多文件上传。

结语

新纪元的到来让世界勃勃生机,而角角落落依然遍布着草履虫们,因为它们仍旧适应这个世界。

参考资料

Axios 中怎么上传文件(Upload File)?上传方法有哪几种?

思否:在Koa.js中实现文件上传的接口

简书:NodeJs koa2实现文件上传

CSDN:koa-body koa2 使用 koa-body 代替 koa-bodyparser 和 koa-multer

CSDN:koa-body4接收formData数据

CSDN:koa-body 文件上传自定义文件夹及文件名称

稀土掘金:原生nodejs 处理文件上传

简书:node原生处理前端传送的数据

稀土掘金:上传文件时获取上传进度

W3cways:koa + formidable实现文件上传

npm:koa-body

npm:hexoid

npm:node-mime-types

CitCode–GitHub镜像

上一篇:MySQL二进制日志(bin

下一篇:宝塔面板网页怎么用?