Express 路由与请求处理

核心知识点

路由参数

Express 支持多种类型的路由参数:

  • 路径参数:如 /users/:id 中的 id
  • 查询参数:如 ?name=value 中的键值对
  • 正则表达式路由:使用正则表达式匹配路径

请求方法

Express 支持所有 HTTP 请求方法:

  • GET:获取资源
  • POST:创建资源
  • PUT:更新资源
  • DELETE:删除资源
  • PATCH:部分更新资源
  • OPTIONS:获取资源的可用方法
  • HEAD:获取资源的头部信息

请求体解析

对于包含请求体的请求(如 POST、PUT),需要使用中间件解析请求体:

  • express.json():解析 JSON 格式的请求体
  • express.urlencoded():解析 URL 编码的表单数据
  • multer:处理文件上传

路由模块化

使用 express.Router() 创建模块化的路由处理器,便于组织大型应用的代码结构。

路由中间件

为特定路由添加中间件,实现路由级别的功能增强。

实用案例

案例一:高级路由参数

const express = require('express');
const app = express();
const port = 3000;

// 基本路径参数
app.get('/users/:id', (req, res) => {
  res.json({
    userId: req.params.id,
    message: '用户信息'
  });
});

// 多个路径参数
app.get('/users/:id/posts/:postId', (req, res) => {
  res.json({
    userId: req.params.id,
    postId: req.params.postId,
    message: '用户文章'
  });
});

// 正则表达式路由 - 匹配数字 ID
app.get('/products/:id(\d+)', (req, res) => {
  res.json({
    productId: req.params.id,
    message: '产品信息(数字 ID)'
  });
});

// 正则表达式路由 - 匹配字母开头的用户名
app.get('/profiles/:username([a-zA-Z][a-zA-Z0-9_]+)', (req, res) => {
  res.json({
    username: req.params.username,
    message: '用户资料'
  });
});

// 可选参数
app.get('/blog/:year?/:month?/:day?', (req, res) => {
  const { year, month, day } = req.params;
  res.json({
    year: year || '全部',
    month: month || '全部',
    day: day || '全部',
    message: '博客归档'
  });
});

app.listen(port, () => {
  console.log(`服务器运行在 http://localhost:${port}`);
});

案例二:请求体解析

const express = require('express');
const app = express();
const port = 3000;

// 解析 JSON 请求体
app.use(express.json());

// 解析 URL 编码的表单数据
app.use(express.urlencoded({ extended: true }));

// 处理 GET 请求 - 查询参数
app.get('/search', (req, res) => {
  res.json({
    query: req.query,
    message: '搜索结果'
  });
});

// 处理 POST 请求 - JSON 数据
app.post('/api/users', (req, res) => {
  res.json({
    user: req.body,
    message: '用户创建成功'
  });
});

// 处理 PUT 请求 - 更新资源
app.put('/api/users/:id', (req, res) => {
  res.json({
    userId: req.params.id,
    updates: req.body,
    message: '用户更新成功'
  });
});

// 处理 PATCH 请求 - 部分更新
app.patch('/api/users/:id', (req, res) => {
  res.json({
    userId: req.params.id,
    partialUpdates: req.body,
    message: '用户部分更新成功'
  });
});

// 处理 DELETE 请求
app.delete('/api/users/:id', (req, res) => {
  res.json({
    userId: req.params.id,
    message: '用户删除成功'
  });
});

app.listen(port, () => {
  console.log(`服务器运行在 http://localhost:${port}`);
});

案例三:模块化路由

// routes/users.js
const express = require('express');
const router = express.Router();

// 用户相关路由
router.get('/', (req, res) => {
  res.json({
    users: [
      { id: 1, name: '张三' },
      { id: 2, name: '李四' }
    ],
    message: '用户列表'
  });
});

router.get('/:id', (req, res) => {
  res.json({
    id: req.params.id,
    name: '用户名称',
    email: 'user@example.com'
  });
});

router.post('/', (req, res) => {
  res.json({
    user: req.body,
    message: '用户创建成功'
  });
});

module.exports = router;

// routes/products.js
const express = require('express');
const router = express.Router();

// 产品相关路由
router.get('/', (req, res) => {
  res.json({
    products: [
      { id: 1, name: '产品1', price: 100 },
      { id: 2, name: '产品2', price: 200 }
    ],
    message: '产品列表'
  });
});

router.get('/:id', (req, res) => {
  res.json({
    id: req.params.id,
    name: '产品名称',
    price: 99.99,
description: '产品描述'
  });
});

module.exports = router;

// app.js
const express = require('express');
const app = express();
const port = 3000;
const userRoutes = require('./routes/users');
const productRoutes = require('./routes/products');

// 中间件
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 注册路由
app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);

// 根路径
app.get('/', (req, res) => {
  res.send('Express 模块化路由示例');
});

app.listen(port, () => {
  console.log(`服务器运行在 http://localhost:${port}`);
});

案例四:表单处理

const express = require('express');
const app = express();
const port = 3000;

// 解析表单数据
app.use(express.urlencoded({ extended: true }));

// 提供表单页面
app.get('/form', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <title>表单示例</title>
    </head>
    <body>
      <h1>用户注册</h1>
      <form action="/submit-form" method="POST">
        <div>
          <label>姓名:</label>
          <input type="text" name="name" required>
        </div>
        <div>
          <label>邮箱:</label>
          <input type="email" name="email" required>
        </div>
        <div>
          <label>密码:</label>
          <input type="password" name="password" required>
        </div>
        <div>
          <label>性别:</label>
          <input type="radio" name="gender" value="male" checked> 男
          <input type="radio" name="gender" value="female"> 女
        </div>
        <div>
          <label>爱好:</label>
          <input type="checkbox" name="hobbies" value="reading"> 阅读
          <input type="checkbox" name="hobbies" value="sports"> 运动
          <input type="checkbox" name="hobbies" value="coding"> 编程
        </div>
        <div>
          <label>城市:</label>
          <select name="city">
            <option value="beijing">北京</option>
            <option value="shanghai">上海</option>
            <option value="guangzhou">广州</option>
          </select>
        </div>
        <button type="submit">提交</button>
      </form>
    </body>
    </html>
  `);
});

// 处理表单提交
app.post('/submit-form', (req, res) => {
  // 获取表单数据
  const formData = req.body;
  
  // 处理复选框数据(如果有多个值)
  if (typeof formData.hobbies === 'string') {
    formData.hobbies = [formData.hobbies];
  }
  
  // 发送响应
  res.json({
    message: '表单提交成功',
    data: formData
  });
});

app.listen(port, () => {
  console.log(`服务器运行在 http://localhost:${port}`);
});

案例五:文件上传处理

const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
const port = 3000;

// 配置 multer 存储
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, './uploads');
  },
  filename: function (req, file, cb) {
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
  }
});

// 创建 multer 实例
const upload = multer({ storage: storage });

// 提供文件上传页面
app.get('/upload', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <title>文件上传</title>
    </head>
    <body>
      <h1>文件上传示例</h1>
      <form action="/upload-file" method="POST" enctype="multipart/form-data">
        <div>
          <label>用户名:</label>
          <input type="text" name="username" required>
        </div>
        <div>
          <label>上传文件:</label>
          <input type="file" name="avatar" required>
        </div>
        <div>
          <label>上传多个文件:</label>
          <input type="file" name="photos" multiple>
        </div>
        <button type="submit">上传</button>
      </form>
    </body>
    </html>
  `);
});

// 处理单个文件上传
app.post('/upload-file', upload.fields([
  { name: 'avatar', maxCount: 1 },
  { name: 'photos', maxCount: 5 }
]), (req, res) => {
  const files = req.files;
  const formData = req.body;
  
  res.json({
    message: '文件上传成功',
    user: formData.username,
    avatar: files.avatar ? files.avatar[0].filename : null,
    photos: files.photos ? files.photos.map(file => file.filename) : []
  });
});

app.listen(port, () => {
  console.log(`服务器运行在 http://localhost:${port}`);
});

学习目标

  1. 掌握路由参数:能够使用路径参数、查询参数和正则表达式路由
  2. 处理多种请求方法:学会使用不同 HTTP 请求方法处理各种操作
  3. 解析请求体:能够解析 JSON 和表单格式的请求体数据
  4. 模块化路由:学会使用 express.Router() 创建模块化路由
  5. 处理表单数据:能够处理各种类型的表单输入,包括复选框、单选按钮等
  6. 文件上传:学会使用 multer 处理文件上传
  7. 路由中间件:能够为特定路由添加中间件

代码优化建议

1. 路由参数验证

不好的做法

app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  // 直接使用参数,没有验证
  getUserById(userId, (err, user) => {
    if (err) {
      res.status(500).send('错误');
    } else {
      res.json(user);
    }
  });
});

好的做法

app.get('/users/:id', (req, res, next) => {
  const userId = req.params.id;
  // 验证参数
  if (!/^\d+$/.test(userId)) {
    return res.status(400).json({ error: '无效的用户 ID' });
  }
  next();
}, (req, res) => {
  const userId = parseInt(req.params.id);
  getUserById(userId, (err, user) => {
    if (err) {
      res.status(500).send('错误');
    } else {
      res.json(user);
    }
  });
});

2. 错误处理

不好的做法

app.post('/api/data', (req, res) => {
  const data = req.body;
  // 没有错误处理
  saveData(data);
  res.json({ message: '成功' });
});

好的做法

app.post('/api/data', async (req, res, next) => {
  try {
    const data = req.body;
    // 验证数据
    if (!data.name) {
      return res.status(400).json({ error: '缺少必要字段' });
    }
    // 异步保存数据
    await saveData(data);
    res.json({ message: '成功' });
  } catch (error) {
    next(error); // 传递给错误处理中间件
  }
});

// 全局错误处理中间件
app.use((err, req, res, next) => {
  console.error('错误:', err);
  res.status(500).json({ error: '服务器内部错误' });
});

3. 路由组织

不好的做法

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

// 所有路由都在一个文件中
app.get('/api/users', (req, res) => {...});
app.get('/api/users/:id', (req, res) => {...});
app.post('/api/users', (req, res) => {...});
app.put('/api/users/:id', (req, res) => {...});
app.delete('/api/users/:id', (req, res) => {...});

app.get('/api/products', (req, res) => {...});
app.get('/api/products/:id', (req, res) => {...});
app.post('/api/products', (req, res) => {...});
// 更多路由...

好的做法

// routes/users.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {...});
router.get('/:id', (req, res) => {...});
router.post('/', (req, res) => {...});
router.put('/:id', (req, res) => {...});
router.delete('/:id', (req, res) => {...});

module.exports = router;

// routes/products.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {...});
router.get('/:id', (req, res) => {...});
router.post('/', (req, res) => {...});

module.exports = router;

// app.js
const express = require('express');
const app = express();
const userRoutes = require('./routes/users');
const productRoutes = require('./routes/products');

app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);

常见问题与解决方案

问题1:请求体解析失败

原因

  • 没有使用正确的中间件解析请求体
  • 请求体格式与中间件不匹配
  • 请求体过大

解决方案

  • 使用 express.json() 解析 JSON 数据
  • 使用 express.urlencoded() 解析表单数据
  • 使用 multer 处理文件上传
  • 调整中间件配置,如增加请求体大小限制

问题2:路由参数获取错误

原因

  • 路由定义顺序错误
  • 路径参数与查询参数混淆
  • 正则表达式路由语法错误

解决方案

  • 将具体路由放在通配符路由之前
  • 使用 req.params 获取路径参数,req.query 获取查询参数
  • 检查正则表达式语法

问题3:文件上传失败

原因

  • 没有使用 multer 中间件
  • 表单没有设置 enctype=&quot;multipart/form-data&quot;
  • 上传目录权限问题
  • 文件大小超过限制

解决方案

  • 安装并正确配置 multer
  • 确保表单设置了正确的 enctype
  • 确保上传目录存在且有写入权限
  • 调整 multer 配置,增加文件大小限制

问题4:路由模块化后无法访问

原因

  • 路由文件路径错误
  • 路由注册路径错误
  • 路由导出或导入错误

解决方案

  • 检查文件路径是否正确
  • 检查 app.use() 的路径参数
  • 确保使用 module.exports 导出路由,使用 require() 导入路由

总结

通过本教程的学习,你应该能够:

  1. 掌握 Express 路由系统的高级特性,包括路径参数、查询参数和正则表达式路由
  2. 处理各种 HTTP 请求方法,实现完整的 RESTful API
  3. 解析不同格式的请求体数据,包括 JSON、表单和文件上传
  4. 使用 express.Router() 创建模块化的路由处理器,组织大型应用的代码结构
  5. 处理各种类型的表单输入,包括文本、复选框、单选按钮等
  6. 为特定路由添加中间件,实现路由级别的功能增强
  7. 编写清晰、可维护的 Express 路由代码

这些技能将帮助你构建功能完整、结构清晰的 Express 应用,为后续学习数据库集成、认证授权等高级功能打下坚实的基础。

« 上一篇 Node.js Express 框架入门 下一篇 » Express 中间件机制