在 Web 应用中,表单是用户与服务器交互的主要方式之一。Express 提供了灵活的方式来处理不同类型的表单数据,包括简单的文本字段、文件上传等。本章将详细讲解如何解析 GET 和 POST 表单、处理文件上传,以及基本的数据验证技巧。
HTML 表单支持两种 HTTP 方法:GET 和 POST。它们将表单数据以不同方式发送到服务器。
无论使用哪种方法,Express 都能方便地提取数据。
当表单使用 method="GET" 提交时,数据会以 ?key1=value1&key2=value2 的形式附加在 URL 上。在 Express 中,可以通过 req.query 对象获取这些参数。
示例:一个搜索表单
<!-- search.html -->
<form action="/search" method="GET">
<input type="text" name="q" placeholder="输入搜索关键词">
<button type="submit">搜索</button>
</form>
Express 路由处理:
app.get('/search', (req, res) => {
const query = req.query.q;
res.send(`你搜索的关键词是:${query}`);
});
访问 /search?q=nodejs 就会显示 "你搜索的关键词是:nodejs"。
如果有多个字段,req.query 会包含所有键值对。对于重复的字段名(如多选框),Express 会将值放入数组。
大多数简单表单使用 enctype="application/x-www-form-urlencoded"(默认),数据在请求体中编码为键值对字符串。Express 需要内置中间件 express.urlencoded() 来解析这种格式。
app.use(express.urlencoded({ extended: true }));
extended: true 允许解析复杂对象和数组(使用 qs 库),false 则使用 querystring 库(仅支持简单键值对)。
示例:登录表单
<form action="/login" method="POST">
<input type="text" name="username" placeholder="用户名">
<input type="password" name="password" placeholder="密码">
<button type="submit">登录</button>
</form>
Express 路由:
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 验证用户名密码...
res.send(`登录尝试:${username}`);
});
复选框在没有选中时不会提交任何值。如果选中,其值(默认为 "on")会被提交。如果有多个同名的复选框(如多选框),需要在名称后添加 [] 以便 Express 解析为数组。
<input type="checkbox" name="hobby[]" value="reading"> 阅读
<input type="checkbox" name="hobby[]" value="coding"> 编程
在服务器端,req.body.hobby 将是一个包含选中值的数组(如果至少选中一项)。
文件上传需要使用 enctype="multipart/form-data" 的表单,并且不能使用 express.urlencoded() 解析,因为它无法处理二进制数据。常用的解决方案是使用第三方中间件 multer。
安装 multer:
npm install multer
示例:包含文件上传的表单
<form action="/profile" method="POST" enctype="multipart/form-data">
<input type="text" name="username" placeholder="用户名">
<input type="file" name="avatar">
<button type="submit">上传</button>
</form>
使用 multer 处理:
const multer = require('multer');
const upload = multer({ dest: 'uploads/' }); // 文件保存目录
app.post('/profile', upload.single('avatar'), (req, res) => {
// req.file 包含上传文件的信息
console.log(req.file);
// req.body 包含文本字段
console.log(req.body.username);
res.send('文件上传成功');
});
upload.single('avatar') 处理名为 "avatar" 的文件字段,并将文件信息挂载到 req.file 上。如果表单包含多个文件字段,可以使用 upload.fields([...]);如果是同一字段的多个文件,使用 upload.array('photos', 12)。
你可以自定义存储引擎,例如控制文件名和路径:
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + '-' + file.originalname);
}
});
const upload = multer({ storage: storage });
还可以添加文件过滤:
const fileFilter = (req, file, cb) => {
if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png') {
cb(null, true);
} else {
cb(new Error('仅允许 JPEG 和 PNG 格式'), false);
}
};
const upload = multer({ storage: storage, fileFilter: fileFilter });
永远不要信任客户端传来的数据。服务器端必须对表单数据进行验证和清理。以下是一些常用方法:
app.post('/register', (req, res) => {
const { username, email, age } = req.body;
const errors = [];
if (!username || username.length < 3) {
errors.push('用户名至少 3 个字符');
}
if (!email || !email.includes('@')) {
errors.push('无效的邮箱');
}
if (age && isNaN(parseInt(age))) {
errors.push('年龄必须是数字');
}
if (errors.length > 0) {
res.status(400).json({ errors });
} else {
// 继续处理...
res.send('注册成功');
}
});
express-validator 是一组流行的验证中间件,基于 validator.js。
安装:npm install express-validator
const { body, validationResult } = require('express-validator');
app.post('/register',
body('username').isLength({ min: 3 }).withMessage('用户名至少 3 个字符'),
body('email').isEmail().withMessage('无效的邮箱'),
body('age').optional().isInt({ min: 0 }).withMessage('年龄必须是正整数'),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 验证通过,处理业务
res.send('注册成功');
}
);
还可以对数据进行清理(如去除空格、规范化):
body('username').trim().escape()
下面结合表单、文件上传和验证,创建一个完整的用户注册接口。
HTML 表单:
<form action="/register" method="POST" enctype="multipart/form-data">
<div>
<label>用户名:</label>
<input type="text" name="username" required>
</div>
<div>
<label>邮箱:</label>
<input type="email" name="email" required>
</div>
<div>
<label>年龄:</label>
<input type="number" name="age">
</div>
<div>
<label>头像:</label>
<input type="file" name="avatar" accept="image/*">
</div>
<button type="submit">注册</button>
</form>
Express 应用:
const express = require('express');
const multer = require('multer');
const { body, validationResult } = require('express-validator');
const path = require('path');
const app = express();
// 配置文件上传
const storage = multer.diskStorage({
destination: 'uploads/avatars',
filename: (req, file, cb) => {
const unique = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, file.fieldname + '-' + unique + ext);
}
});
const upload = multer({
storage: storage,
limits: { fileSize: 2 * 1024 * 1024 }, // 2MB
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('仅允许上传图片'), false);
}
}
});
// 解析其他表单字段(注意:multipart 由 multer 处理,这里不需要 express.urlencoded)
app.use(express.urlencoded({ extended: true }));
app.post('/register',
upload.single('avatar'),
body('username').isLength({ min: 3 }).withMessage('用户名至少 3 个字符').trim().escape(),
body('email').isEmail().withMessage('无效的邮箱').normalizeEmail(),
body('age').optional().isInt({ min: 1 }).withMessage('年龄必须是正整数'),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
// 如果有错误,可以删除已上传的文件(可选)
if (req.file) {
fs.unlink(req.file.path, (err) => console.log('删除文件失败', err));
}
return res.status(400).json({ errors: errors.array() });
}
// 所有数据有效,保存到数据库等操作
console.log('用户数据:', req.body);
console.log('头像信息:', req.file);
res.json({
message: '注册成功',
user: req.body,
avatar: req.file ? req.file.filename : null
});
}
);
// 错误处理中间件(捕获 multer 错误)
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
return res.status(400).json({ error: err.message });
} else if (err) {
return res.status(400).json({ error: err.message });
}
next();
});
express.urlencoded() 和 express.json() 时要放在需要解析的路由之前。