文件上传的消息格式

  1. 文件上传的本质仍然是一个数据提交,无非就是数据量大一些而已;

  2. 在实践中,人们逐渐的形成了一种共识,自行规定文件上传默认使用下面的请求格式:

    • 除非接口文档特别说明,文件上传一般使用 POST 请求;
    • 接口文档中会规定上传地址,一般一个站点会有一个统一的上传地址;
    • 除非接口文档特别说明,content-type: multipart/form-data,浏览器会自动分配一个定界符 boundary
    • 请求体的格式是一个被定界符boundary分割的消息,每个分割区域本质就是一个键值对;
    • 除了键值对外,multipart/form-data 允许添加其他额外信息,比如文件数据区域,一般会把文件在本地的名称和文件 MIME 类型告诉服务器;
  3. 表单请求头示例:

    POST 上传地址 HTTP/1.1
    其他请求头
    Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
    
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="avatar"; filename="小仙女.jpg"
    Content-Type: image/jpeg
    
    (文件二进制数据)
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="username"
    
    admin
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="password"
    
    123123
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    

文件上传的实现

文件上传逻辑

前端代码

HTML
CSS
JavaScript
<div class="form-container">
  <div class="form-item">
    <div class="title">账号</div>
    <input type="text" id="username" />
  </div>
  <div class="form-item">
    <div class="title">密码</div>
    <input type="password" id="password" />
  </div>
  <div class="form-item">
    <div class="title">
      头像
      <button id="btnupload">上传文件</button>
      <input type="file" id="fileUploader" style="display: none" />
    </div>
    <img src="" id="avatar" alt="" />
  </div>
  <div class="form-item">
    <button class="submit">提交注册</button>
  </div>
</div>
* {
    box-sizing: border-box;
}

.form-container {
    width: 400px;
    margin: 0 auto;
    background: #eee;
    border-radius: 5px;
    border: 1px solid #ccc;
    padding: 30px;
}

.form-item {
    margin: 1.5em 0;
}

.title {
    height: 30px;
    line-height: 30px;
}

input {
    width: 100%;
    height: 30px;
    font-size: inherit;
}

img {
    max-width: 200px;
    max-height: 250px;
    margin-top: 1em;
    border-radius: 5px;
}

.submit {
    width: 100%;
    background: #0057d8;
    color: #fff;
    border: 1px solid #0141a0;
    border-radius: 5px;
    height: 40px;
    font-size: inherit;
    transition: 0.2s;
}

.submit:hover {
    background: #0061f3;
    border: 1px solid #0057d8;
}
const doms = {
    username: document.querySelector('#username'),
    password: document.querySelector('#password'),
    btnUpload: document.querySelector('#btnupload'),
    fileUploader: document.querySelector('#fileUploader'),
    submit: document.querySelector('.submit'),
    avatar: document.querySelector('#avatar'),
};

doms.btnUpload.onclick = function () {
    doms.fileUploader.click();
};
doms.fileUploader.onchange = async function () {
    // 一般先会在这里对文件进行验证
    // console.log(doms.fileUploader.files);

    // 上传文件
    const formData = new FormData();
    formData.append('file', doms.fileUploader.files[0]); // 添加一个键值对

    const resp = await fetch('http://localhost:8000/api/upload', {
        method: 'POST',
        body: formData,
    }).then((resp) => resp.json());
    doms.avatar.src = resp.data;
};

doms.submit.onclick = async function () {
    const resp = await fetch('http://localhost:8000/api/user/reg', {
        method: 'POST',
        headers: {
            'content-type': 'application/json',
        },
        body: JSON.stringify({
            username: doms.username.value,
            password: doms.password.value,
            avatar: doms.avatar.src,
        }),
    }).then((resp) => resp.json());
    console.log(resp);
};

服务端代码

JavaScript
JavaScript
JavaScript
JavaScript
// index.js
const express = require('express');
const path = require('path');
const app = express();
const cors = require('cors');
const port = require('./config').port;

app.use(cors());
app.use(express.static(path.join(__dirname, './public')));
app.use(express.urlencoded({
  extended: true
}));
app.use(express.json());
app.use('/api/upload', require('./upload-handler'));
app.use('/api/user', require('./user'));

app.listen(port, () => {
  console.log(`server listen on ${port}`);
});
// config.js
module.exports = {
  port: 8000,
};
// upload-handler.js
const express = require('express');
const router = express.Router();
const path = require('path');

const config = {
  fieldName: 'file',
  sizeLimit: 1 * 1024 * 1024,
  extends: ['.jpg', '.jpeg', '.gif', '.png', '.bmp', '.webp'],
  saveDir: path.resolve(__dirname, './public/upload'),
  createFilename(ext) {
    if (!ext.startsWith('.')) {
      ext = '.' + ext;
    }
    const rad = Math.random().toString(36).substr(2);
    const time = new Date().getTime().toString(36);
    return rad + time + ext;
  },
};

const multer = require('multer');
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, config.saveDir);
  },
  filename: function (req, file, cb) {
    const ext = path.extname(file.originalname);
    cb(null, config.createFilename(ext));
  },
});

class ExtendNameError extends Error {}

const upload = multer({
  storage,
  fileFilter(req, file, cb) {
    const ext = path.extname(file.originalname);
    if (config.extends.includes(ext)) {
      cb(null, true);
    } else {
      cb(new ExtendNameError('无效的文件类型'));
    }
  },
  limits: {
    fileSize: config.sizeLimit,
  },
}).single(config.fieldName);

router.post('/', (req, res) => {
  upload(req, res, function (err) {
    if (err) {
      let msg;
      switch (err.message) {
        case 'File too large':
          msg = '文件大小超过了限制';
          break;
        case 'Unexpected field':
          msg = `无法找到${fieldName}字段`;
          break;
      }
      if (err instanceof ExtendNameError) {
        msg = err.message;
      }
      res.send({
        code: 403,
        msg,
        data: null
      });
    } else {
      res.send({
        code: 0,
        msg: '',
        data: `${req.protocol}://${req.hostname}:${require('./config').port}/upload/${req.file.filename}`,
      });
    }
  });
});

module.exports = router;
// user.js
const express = require('express');
const router = express.Router();

const users = [];
router.post('/reg', (req, res) => {
  users.push(req.body);
  res.send({
    code: 0,
    msg: '',
    data: {
      username: req.body.username,
      avatar: req.body.avatar,
    },
  });
});

module.exports = router;
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 文件上传的消息格式
  2. 2. 文件上传的实现
    1. 2.1. 文件上传逻辑
    2. 2.2. 前端代码
    3. 2.3. 服务端代码