处理表单数据 #

一、表单数据类型 #

1.1 常见表单类型 #

类型 Content-Type 说明
URL编码 application/x-www-form-urlencoded 默认表单类型
JSON application/json API常用
多部分表单 multipart/form-data 文件上传

1.2 HTML表单示例 #

html
<form action="/submit" method="POST">
    <input type="text" name="username">
    <input type="email" name="email">
    <input type="password" name="password">
    <button type="submit">提交</button>
</form>

二、URL编码表单 #

2.1 基本处理 #

javascript
const express = require('express');
const app = express();

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

app.post('/submit', (req, res) => {
    const { username, email, password } = req.body;
    res.json({ username, email, password: '***' });
});

2.2 extended选项 #

javascript
app.use(express.urlencoded({ extended: true }));
app.use(express.urlencoded({ extended: false }));
extended 解析库 说明
true qs 支持嵌套对象
false querystring 不支持嵌套对象

2.3 嵌套对象 #

使用 extended: true

javascript
app.use(express.urlencoded({ extended: true }));

app.post('/user', (req, res) => {
    console.log(req.body);
    res.json(req.body);
});

表单数据 user[name]=张三&user[email]=test@example.com

json
{
    "user": {
        "name": "张三",
        "email": "test@example.com"
    }
}

2.4 数组数据 #

表单数据 tags[]=node&tags[]=express

json
{
    "tags": ["node", "express"]
}

三、JSON数据 #

3.1 基本处理 #

javascript
app.use(express.json());

app.post('/api/users', (req, res) => {
    const { name, email, age } = req.body;
    res.status(201).json({ name, email, age });
});

3.2 配置选项 #

javascript
app.use(express.json({
    limit: '10kb',
    strict: true,
    type: 'application/json'
}));

3.3 处理不同内容类型 #

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

app.post('/data', (req, res) => {
    const contentType = req.headers['content-type'];
    
    res.json({
        contentType,
        body: req.body
    });
});

四、表单验证 #

4.1 手动验证 #

javascript
app.post('/register', (req, res) => {
    const { username, email, password, confirmPassword } = req.body;
    const errors = [];
    
    if (!username || username.length < 3) {
        errors.push('用户名至少3个字符');
    }
    
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!email || !emailRegex.test(email)) {
        errors.push('请输入有效的邮箱地址');
    }
    
    if (!password || password.length < 6) {
        errors.push('密码至少6个字符');
    }
    
    if (password !== confirmPassword) {
        errors.push('两次密码不一致');
    }
    
    if (errors.length > 0) {
        return res.status(400).json({ errors });
    }
    
    res.status(201).json({ username, email });
});

4.2 使用express-validator #

bash
npm install express-validator
javascript
const { body, validationResult } = require('express-validator');

const validateRegister = [
    body('username')
        .notEmpty().withMessage('用户名不能为空')
        .isLength({ min: 3, max: 20 }).withMessage('用户名3-20个字符')
        .trim().escape(),
    body('email')
        .isEmail().withMessage('请输入有效的邮箱地址')
        .normalizeEmail(),
    body('password')
        .isLength({ min: 6 }).withMessage('密码至少6个字符')
        .matches(/\d/).withMessage('密码必须包含数字')
        .matches(/[a-z]/).withMessage('密码必须包含小写字母')
        .matches(/[A-Z]/).withMessage('密码必须包含大写字母'),
    body('confirmPassword')
        .custom((value, { req }) => {
            if (value !== req.body.password) {
                throw new Error('两次密码不一致');
            }
            return true;
        }),
    (req, res, next) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            return res.status(400).json({ errors: errors.array() });
        }
        next();
    }
];

app.post('/register', validateRegister, (req, res) => {
    res.status(201).json({ 
        message: '注册成功',
        user: { username: req.body.username, email: req.body.email }
    });
});

4.3 自定义验证器 #

javascript
const { body } = require('express-validator');

const isEmailExist = async (email) => {
    const user = await User.findOne({ email });
    if (user) {
        throw new Error('邮箱已被注册');
    }
};

app.post('/register', [
    body('email')
        .isEmail()
        .custom(isEmailExist),
    body('username')
        .custom(async (username) => {
            const user = await User.findOne({ username });
            if (user) {
                throw new Error('用户名已被使用');
            }
        })
], (req, res) => {
    res.status(201).json({ message: '注册成功' });
});

五、动态表单 #

5.1 动态字段 #

javascript
app.post('/dynamic', (req, res) => {
    const formData = req.body;
    
    Object.keys(formData).forEach(key => {
        console.log(`${key}: ${formData[key]}`);
    });
    
    res.json({ received: Object.keys(formData).length, data: formData });
});

5.2 数组字段 #

javascript
app.post('/items', (req, res) => {
    const { items } = req.body;
    
    if (!Array.isArray(items)) {
        return res.status(400).json({ error: 'items必须是数组' });
    }
    
    items.forEach((item, index) => {
        console.log(`Item ${index}:`, item);
    });
    
    res.json({ count: items.length, items });
});

5.3 嵌套对象 #

javascript
app.post('/order', (req, res) => {
    const { customer, items, shipping } = req.body;
    
    res.json({
        customer: {
            name: customer.name,
            email: customer.email
        },
        itemCount: items.length,
        shipping: {
            address: shipping.address,
            city: shipping.city
        }
    });
});

六、文件上传表单 #

6.1 单文件上传 #

javascript
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });

app.post('/upload', upload.single('avatar'), (req, res) => {
    res.json({
        file: req.file,
        body: req.body
    });
});

6.2 多文件上传 #

javascript
app.post('/photos', upload.array('photos', 5), (req, res) => {
    res.json({
        count: req.files.length,
        files: req.files
    });
});

6.3 混合表单 #

javascript
app.post('/profile', upload.single('avatar'), (req, res) => {
    const { name, email, bio } = req.body;
    const avatar = req.file;
    
    res.json({
        name,
        email,
        bio,
        avatar: avatar ? {
            originalname: avatar.originalname,
            size: avatar.size,
            mimetype: avatar.mimetype
        } : null
    });
});

七、CSRF保护 #

7.1 安装csurf #

bash
npm install csurf

7.2 基本配置 #

javascript
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

app.get('/form', csrfProtection, (req, res) => {
    res.render('form', { csrfToken: req.csrfToken() });
});

app.post('/submit', csrfProtection, (req, res) => {
    res.json({ message: '表单提交成功' });
});

7.3 前端表单 #

html
<form action="/submit" method="POST">
    <input type="hidden" name="_csrf" value="<%= csrfToken %>">
    <input type="text" name="username">
    <button type="submit">提交</button>
</form>

7.4 AJAX请求 #

javascript
app.use(csrf({ cookie: true }));

app.get('/csrf-token', (req, res) => {
    res.json({ csrfToken: req.csrfToken() });
});

app.post('/api/submit', (req, res) => {
    res.json({ message: '成功' });
});

前端:

javascript
fetch('/api/submit', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken
    },
    body: JSON.stringify(data)
});

八、完整示例 #

8.1 注册表单 #

javascript
const express = require('express');
const { body, validationResult } = require('express-validator');
const app = express();

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

const validate = (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
    }
    next();
};

app.post('/register', [
    body('username')
        .notEmpty().withMessage('用户名不能为空')
        .isLength({ min: 3, max: 20 }).withMessage('用户名3-20个字符')
        .matches(/^[a-zA-Z0-9_]+$/).withMessage('用户名只能包含字母、数字和下划线')
        .trim(),
    body('email')
        .isEmail().withMessage('请输入有效的邮箱地址')
        .normalizeEmail(),
    body('password')
        .isLength({ min: 8 }).withMessage('密码至少8个字符')
        .matches(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/).withMessage('密码必须包含大小写字母和数字'),
    body('confirmPassword')
        .custom((value, { req }) => {
            if (value !== req.body.password) {
                throw new Error('两次密码不一致');
            }
            return true;
        }),
    body('phone')
        .optional()
        .matches(/^1[3-9]\d{9}$/).withMessage('请输入有效的手机号'),
    body('age')
        .optional()
        .isInt({ min: 1, max: 150 }).withMessage('年龄必须在1-150之间'),
    validate
], async (req, res) => {
    try {
        const { username, email, password, phone, age } = req.body;
        
        const user = {
            id: Date.now(),
            username,
            email,
            phone,
            age
        };
        
        res.status(201).json({
            success: true,
            message: '注册成功',
            user
        });
    } catch (error) {
        res.status(500).json({ error: '服务器错误' });
    }
});

app.listen(3000);

8.2 联系表单 #

javascript
app.post('/contact', [
    body('name')
        .notEmpty().withMessage('姓名不能为空')
        .trim().escape(),
    body('email')
        .isEmail().withMessage('请输入有效的邮箱地址')
        .normalizeEmail(),
    body('subject')
        .notEmpty().withMessage('主题不能为空')
        .isLength({ max: 100 }).withMessage('主题最多100个字符'),
    body('message')
        .notEmpty().withMessage('消息不能为空')
        .isLength({ min: 10, max: 1000 }).withMessage('消息10-1000个字符'),
    validate
], async (req, res) => {
    try {
        const { name, email, subject, message } = req.body;
        
        console.log('联系表单:', { name, email, subject, message });
        
        res.json({
            success: true,
            message: '消息已发送,我们会尽快回复'
        });
    } catch (error) {
        res.status(500).json({ error: '发送失败' });
    }
});

九、总结 #

表单处理要点:

类型 Content-Type 处理方法
URL编码 application/x-www-form-urlencoded express.urlencoded()
JSON application/json express.json()
文件上传 multipart/form-data multer

下一步,让我们学习文件上传!

最后更新:2026-03-28