文件上传的消息格式
-
文件上传的本质仍然是一个数据提交,无非就是数据量大一些而已;
-
在实践中,人们逐渐的形成了一种共识,自行规定文件上传默认使用下面的请求格式:
- 除非接口文档特别说明,文件上传一般使用 POST 请求;
- 接口文档中会规定上传地址,一般一个站点会有一个统一的上传地址;
- 除非接口文档特别说明,
content-type: multipart/form-data
,浏览器会自动分配一个定界符boundary
- 请求体的格式是一个被定界符
boundary
分割的消息,每个分割区域本质就是一个键值对; - 除了键值对外,
multipart/form-data
允许添加其他额外信息,比如文件数据区域,一般会把文件在本地的名称和文件 MIME 类型告诉服务器;
-
表单请求头示例:
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
文件上传的实现
文件上传逻辑
前端代码
<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);
};
服务端代码
// 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;
ES6+ Proxy、ES5 Object.defineProperty
上一篇