05月02, 2023

文件上传1-基本原理

文件上传,无论用什么应用程序,浏览器,小程序或者说APP,其实还是逃不出HTTP协议的格式,

文件上传,也就是HTTP请求,对于HTTP请求来说无非也就是请求行请求头请求正文

请求行这部分没什么变化

请求方法 请求地址 协议版本组成

请求头中,有一个很重要的内容Content-Type

Content-Type:multipart/form-data; boundary=----分隔符

其中,content-typemultipart/form-data,简单来说,这种格式,我们就可以上传文件。

在HTML表单中的form标签,通过设置:encType来指定;比如:encType="multipart/form-data",在其他客户端采用Content-Type来指定;比如:Content-Type="multipart/form-data"

content-type:multipart/form-data要求提交方式必须为POST

boundary后面的,是长度为16的随机base64字符,它是浏览器自动生成的分隔符。

请求正文,生成的数据,都由这段分隔符包裹,它标志着一段数据(当有多个数据内容的时候)的开始和结束

下面是上传的一段请求正文

可以看到上面传递的每一段数据,都由boundary生成的分隔符进行分割

无论怎么样,文件上传前端提供的信息,也都是这个样子,后端接收到这些数据之后,进行处理。

大多数情况下,前端人员根本不需要纠结后端,不过要实现上传效果,那么肯定需要后端处理。

后端处理文件

为了达到试验效果,后端这里是express框架,使用multer第三方库,来处理上传文件

npm install --save multer

地址:https://github.com/expressjs/multer/blob/master/doc/README-zh-cn.md

不过由于是上传,肯定是post提交,也可能会出现跨域问题,所以对于express框架的相关配置大家需要提前准备

cors

//安装cors
npm install cors

//使用cors
let express = require('express')
let cors = require('cors')
let app = express()

app.use(cors())

post提交

app.use(express.json());
app.use(express.urlencoded({ extended: false }));

config.js: 上传相关配置文件

const path = require('path');

//文件上传大小限制,单个文件最大2M
let sizeLimit = 2 * 1024 * 1024;
//文件后缀限制
let exts = ['.jpg', '.jpeg', '.bmp', '.webp', '.gif', '.png'];
//文件上传个数
let countLimit = 10;

//文件地址配置
exports.configPath = {
  //上传地址
  upload: path.resolve(__dirname, '../public/upload'),
};

//单文件上传配置
exports.single = {
  sizeLimit,
  exts,
  fieldName: 'avatar',
};
//多文件上传配置
exports.multi = {
  fieldName: 'images',
  sizeLimit,
  countLimit,
  exts,
};

UploadErrorTypes.js 上传文件错误提示

class UploadErrorTypes extends Error {
  constructor(code, msg) {
    super(msg);
    this.code = code;
    this.msg = msg;
  }
}

class ExtError extends UploadErrorTypes {
  constructor() {
    super(1, '后缀名不符合要求');
  }
}

class SizeLimitError extends UploadErrorTypes {
  constructor() {
    super(2, '文件过大');
  }
}

class CountLimitError extends UploadErrorTypes {
  constructor() {
    super(3, '文件数量超过要求');
  }
}

class UnexpectedRequest extends UploadErrorTypes {
  constructor() {
    super(4, '未知请求错误');
  }
}

exports.UploadErrorTypes = UploadErrorTypes;
exports.ExtError = ExtError;
exports.SizeLimitError = SizeLimitError;
exports.CountLimitError = CountLimitError;
exports.UnexpectedRequest = UnexpectedRequest;

fileUtil.js 文件上传工具函数

const path = require('path');

//上传文件重命名函数
exports.uploadSuffixName = (originalname,filename) => { 
  let ext = path.extname(filename);
  if (ext) { //如果新生成的文件名有后缀名,直接返回新文件名
    return filename;
  }
  ext = path.extname(originalname);
  if (!ext) { //如果源文件名本身就没有后缀名,直接返回新文件名
    return filename;
  }

  return filename + ext;
}

//生成后端访问路径函数
exports.generateUrl = function (req, filename, basePath = '/upload') {
  return `${req.protocol}://${req.header('host')}${basePath}/${filename}`;
};

单文件上传路由

const express = require('express');
const router = express.Router();
//引入配置文件
const config = require('../utils/config');
//引入multer模块
const multer = require('multer');
//配置multer
const upload = multer({
  dest: config.configPath.upload, //上传文件保存路径
  limits: { 
    fileSize: config.single.sizeLimit,//上传文件大小限制
  },
  fileFilter(req, file, cb) { //文件后缀过滤
    const path = require('path');
    const ext = path.extname(file.originalname);
    if (config.single.exts.includes(ext)) {
      cb(null, true);
    } else {
      const { ExtError } = require('../utils/UploadErrorTypes.js');
      cb(new ExtError());
    }
  },
});

//单文件上传函数
const single = upload.single(config.single.fieldName);

/**
 * 单文件上传
 * @group upload - 上传操作 
 * @route POST /upload/single 
 * @consumes multipart/form-data
 * @param {file} avatar.body.required - 上传的文件
 * @returns {object} 0 - 返回成功信息{code:0,msg:'上传成功',data:url}
 * @returns {json} 1 - 后缀名不符合要求
 * @returns {json} 2 - 文件过大
 * @returns {json} 3 - 文件数量超过要求
 * @returns {json} 4 - 未知请求错误
 */
router.post('/single', async (req, res, next) => {

  single(req, res, async (err) => {
    if (err instanceof multer.MulterError) {
      const { SizeLimitError, UnexpectedRequest } = require('../utils/UploadErrorTypes.js');
      if (err.message === 'File too large') {
        err = new SizeLimitError();
      } else {
        err = new UnexpectedRequest();
      }
    }
    if (err) {
      next(err);
      return;
    }

    console.log(req.file);

    const { uploadSuffixName,generateUrl } = require('../utils/fileUtils');

    const filename = uploadSuffixName(req.file.originalname, req.file.filename);
    const fs = require('fs');
    //为保存文件重命名
    await fs.promises.rename(req.file.path, `${req.file.destination}/${filename}`);
    //生成后端访问路径
    const url = generateUrl(req, filename);
    res.send({
      code:0,
      data: url,
      msg:'上传成功'
    });
  });
});

module.exports = router;

本文链接:http://www.yanhongzhi.com/post/upload1.html

-- EOF --

Comments