表单数据处理

在 Web 应用中,表单是用户与服务器交互的主要方式之一。Express 提供了灵活的方式来处理不同类型的表单数据,包括简单的文本字段、文件上传等。本章将详细讲解如何解析 GET 和 POST 表单、处理文件上传,以及基本的数据验证技巧。

1. 表单的两种提交方式

HTML 表单支持两种 HTTP 方法:GETPOST。它们将表单数据以不同方式发送到服务器。

  • GET:表单数据附加在 URL 的查询字符串中,适合非敏感、幂等的请求(如搜索)。
  • POST:表单数据放在请求体(body)中,适合包含敏感信息或文件上传的场景。

无论使用哪种方法,Express 都能方便地提取数据。

2. 处理 GET 表单(查询参数)

当表单使用 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 会将值放入数组。

3. 处理 POST 表单(URL 编码)

大多数简单表单使用 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}`);
});

3.1 复选框与多选

复选框在没有选中时不会提交任何值。如果选中,其值(默认为 "on")会被提交。如果有多个同名的复选框(如多选框),需要在名称后添加 [] 以便 Express 解析为数组。

<input type="checkbox" name="hobby[]" value="reading"> 阅读
<input type="checkbox" name="hobby[]" value="coding"> 编程

在服务器端,req.body.hobby 将是一个包含选中值的数组(如果至少选中一项)。

4. 文件上传(multipart/form-data)

文件上传需要使用 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)

4.1 multer 的高级配置

你可以自定义存储引擎,例如控制文件名和路径:

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 });

5. 数据验证与清理

永远不要信任客户端传来的数据。服务器端必须对表单数据进行验证和清理。以下是一些常用方法:

5.1 手动验证

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('注册成功');
  }
});

5.2 使用验证库(如 express-validator)

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()

6. 综合示例:用户注册与头像上传

下面结合表单、文件上传和验证,创建一个完整的用户注册接口。

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();
});

7. 注意事项

  • 对于生产环境,文件上传必须进行严格的限制(大小、类型),并确保存储路径安全。
  • 使用 express.urlencoded()express.json() 时要放在需要解析的路由之前。
  • multer 会将文件存储在磁盘上,记得定期清理无用文件。
  • 永远在服务器端验证数据,客户端验证仅用于提升用户体验。
总结: 本章介绍了 Express 处理表单数据的核心方法,包括 GET 查询参数、POST 的 URL 编码数据、multipart 文件上传以及基本的验证技巧。掌握这些知识,你就能构建功能完整的用户交互界面。