全栈经典场景:一文搞定前后端文件上传

大家好,我是杨成功。

前段时间我开源了 "仿稀土掘金" 博客系统,实现了掘金的文章和沸点功能,地址在 GitHub。考虑到服务器资源有限,我没有加上传图片的功能。

这几天有小伙伴说想要上传图片,还说上传前可以压缩一下,服务端不会占用很多资源。

我想想也是,所以安排上。

功能分前端和后端两部分实现:前端选择和压缩图片,后端接收和存储图片。

后端实现上传接口

后端使用 Express 框架,如果上传文件,需要用到 multer 模块。

首先安装:

sh 复制代码
$ yarn add multer

使用 multer 最简单的方式,就是指定一个存储路径,并在 Express 路由中添加一个接收字段:

js 复制代码
// 导入模块
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });

// 路由
app.post('/upload', upload.single('avatar'), function (req, res, next) {
  // req.file 就是接受到的文件
  let { path, filename } = req.file;
  res.send('ok');
});

上方代码中的 uploads/ 是文件存储路径,avatar 是上传文件的字段名。

如果 req.file 的值为空,说明没有接收到文件,做一个判断处理:

js 复制代码
if (!req.file) {
  res.status(400).send({ message: '没有接收到文件' });
}

如果上传文件指定的字段不是 avatar,那么请求不会走到路由中,而是直接抛出异常。

如果定义了错误处理中间件,该异常会走到错误处理中间件中,你可以输出 JSON 格式的错误。

上传成功后,文件被存储在 uoloads 文件夹下,是这样的:

可以看到,文件被重命名为 32 位的随机字符串,并且没有后缀!

重命名文件并添加后缀

不知道为啥,上面的方式上传后不生成文件后缀,可能是我姿势不对,有了解的同学麻烦告诉我。

没办法,只能手动处理文件名,需要用 diskStorage 实现:

js 复制代码
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, '/uploads');
  },
  filename: (req, file, cb) => {
    cb(null, file.fieldname + '.后缀名');
  },
});
const upload = multer({ storage });

上方代码中,destination 方法用于生成文件目录,filename 方法用于生成文件名。

为了防止同名文件被覆盖,使用 crypto 模块生成指定长度的文件名,使用 path 模块获取文件后缀,最后拼接起来就是完整文件名:

js 复制代码
const path = require('path')
const crypto = require('crypto')

filename(req, file, cb) {
  let length = 12 // 文件名长度
  let random_name = crypto
    .randomBytes(Math.ceil(length / 2))
    .toString('hex')
    .slice(0, length) // 随机字符
  let { fieldname, originalname } = file
  let after = path.extname(originalname) // 文件后缀
  cb(null, random_name + after)
}

生成目录时,如果是多层级目录,无法自动创建。因此要先创建目录,再保存。

我想把每日上传的文件存储在单独的目录中,就要通过日期动态创建目录。方法如下:

js 复制代码
const fs = require('fs')
const dayjs = require('dayjs')

destination(req, file, cb) {
  let dirpath = 'uploads/' + dayjs().format('YYYY-MM-DD')
  // 目录不存在,则递归创建
  if (!fs.existsSync(dirpath)) {
    fs.mkdirSync(dirpath, { recursive: true })
  }
  cb(null, dirpath)
},

最终的效果:

多文件上传

上面的方法是单文件上传,下面看怎么实现多文件上传。

目录和文件的处理逻辑一样,也就是 diskStorage 的部分呢不用动,只要区分单文件和多文件怎么接收。

  • upload.single('image') :接收单文件,通过 req.file 获取文件。
  • upload.array('images') :接收多文件,通过 req.files 获取文件

定义一个接收多文件的路由:

js 复制代码
const upload = multer({ storage });

router.post('/uploads', upload.array('images', 4), (req, res) => {
  let files = req.files
  if (!files || files.length == 0) {
    return res.status(400).send({ message: '文件不能为空' })
  }
  res.send({
    data: files.map(file => ({
      path: file.path
      filename: file.filename,
    })),
  })
})

上方代码中,upload.array('images', 4) 表示上传字段是 images,最多一次上传 4 个文件。

也可以限制单个文件的上传大小,比如不超过 1M:

js 复制代码
const upload = multer({
  storage,
  limits: {
    fileSize: 1024 * 1024, // 限制文件大小为1MB
  },
});

前端选择并上传文件

前端实现相对简单一些,主要就是选择文件,并调用上传接口。

宣传一下我的新书《前端开发实战派》,这本书囊括了从前端基础到全栈开发的实战重点知识,包含两套源码。既可以为前端初学者提供清晰的学习路径,又可以为3~5年经验的提供实战进阶方向。

🤡 新书京东5折,详情页有完整目录。源码已经开源,加我好友免费领取。

我们看 3 种常用的上传文件方法。

原生 HTML + JavaScript 上传

定义一个上传文件的输入框:

js 复制代码
<imput type="file" onchange="toupload(this)" />

获取到文件后,使用 FormData 添加文件,并通过 fetch 方法实现单文件上传:

js 复制代码
const toupload = async (e) => {
  let file = e.files[0];
  let form_data = new FormData();
  form_data.append('image', file, file.name);
  try {
    const response = await fetch('http://xxx/upload', {
      method: 'POST',
      body: form_data,
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    });
    let data = await response.json();
    console.log('上传成功:', data);
  } catch (error) {
    console.error('上传失败:', error);
  }
};

如果是多文件上传,只需要修改选择文件的部分:

js 复制代码
let files = e.files;
let form_data = new FormData();
for (let i = 0; i < files.length; i++) {
  form_data.append('images', files[i], files[i].name);
}

上传前压缩文件

如果前端选择了非常大的文件上传,不光会占用服务器带宽,还会大量占用服务器存储空间。

解决这个问题,可以在选择文件后获取文件大小,大于某个值拒绝上传。

实际情况是,用户不会先把文件压缩了,再来上传,除非迫不得已。所以应该先判断文件大小,大于某个值自动压缩文件。

文件压缩使用 image-conversion 这个包,写一个压缩函数:

js 复制代码
import { compressAccurately } from 'image-conversion';

const compressImg = (file: File) => {
  // 压缩后的最大值
  let maxsize = 300;
  // 判断文件类型
  let typeList = ['image/jpeg', 'image/png', 'image/gif'];
  let isValid = typeList.includes(file.type);
  let need_press = file.size / 1024 > maxsize;
  if (!isValid) {
    alert('图片格式只能是 JPG/PNG/GIF!');
  }
  return new Promise((resolve, reject) => {
    if (!isValid) {
      return reject();
    }
    if (!need_press) {
      // 不需要压缩,直接返回文件
      return resolve(file);
    }
    compressAccurately(file, maxsize).then((res) => {
      resolve(res);
    });
  });
};

选择文件后,使用这个方法压缩文件,返回新文件,如下:

js 复制代码
var final_file = compressImg(file);

Vue3 + Element Plus 文件上传

Element Plus 有一个文件上传组件,有两种上传方式:自动上传和手动上传。

自动上传需要指定上传接口地址,如下:

js 复制代码
<el-upload
  action="http://xxxx/upload"
  name="image"
  :headers="headers"
  :show-file-list="false"
  :multiple="false"
  :on-success="uploadSuccess"
  :before-upload="compressImg"
>
  <el-button class="actmo" plain>上传</el-button>
</el-upload>

一般上传文件接口可能有 JWT 验证,所以请求时要带请求头,通过 headers 属性传递:

js 复制代码
const headers = {
  Authorization: `Bearer ${localStorage.token}`,
};

组件中的 before-upload 在文件选择后、上传前触发,这里用来执行文件压缩。当上传成功后,on-success 指定的函数触发,参数是上传接口的返回值。

单文件上传,使用上面的自动上传即可。多文件上传,如果要展示文件列表,这里要用手动上传。

js 复制代码
<el-upload
  v-show="fileList.length > 0"
  v-model:file-list="fileList"
  action="#" // 不指定上传地址
  list-type="picture-card"
  :auto-upload="false" // 手动上传
  :on-success="uploadSuccess"
></el-upload>

上方代码绑定的 fileList 属性存储的是文件列表,当点击上传时,将文件列表的文件手动上传,方法和前面的 toupload() 方法一致,根据需要看是否压缩。

总结

前面我们从前端到后端完整实现了文件上传的功能,还包括文件压缩,文件目录、命名等规则设置,再严格一点还有上传接口验证,文件大小、格式验证,日常功能足够使用。

详细代码已经上传 GitHub,更多系列文章,请查看我的公众号 程序员成功。

相关推荐
卸任28 分钟前
Vibe coding工具使用心得
前端·ai编程
brien91 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance1 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M2 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写2 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李2 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易2 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding2 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马2 小时前
前沿视界
前端
kyriewen2 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员