强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Node.js 开发指南 / 第 12 章 · Express 框架

第 12 章 · Express 框架

12.1 Express 简介

Express 是 Node.js 最流行的 Web 框架,提供了简洁的 API 来构建 Web 应用和 API。

# 安装
npm init -y
npm install express

# 推荐同时安装
npm install cors helmet morgan compression

Hello Express

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

app.get('/', (req, res) => {
  res.json({ message: 'Hello, Express!' });
});

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

Express vs 原生 http

特性原生 httpExpress
路由手动解析 URL声明式路由
中间件手动实现丰富的中间件生态
请求体解析手动读取流express.json() 内置
错误处理手动 try/catch错误中间件
模板引擎不支持多种引擎支持
静态文件手动实现express.static()

12.2 路由

基本路由

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

// GET 请求
app.get('/', (req, res) => {
  res.send('首页');
});

// POST 请求
app.post('/api/users', (req, res) => {
  res.status(201).json({ created: true });
});

// PUT 请求
app.put('/api/users/:id', (req, res) => {
  res.json({ updated: req.params.id });
});

// DELETE 请求
app.delete('/api/users/:id', (req, res) => {
  res.json({ deleted: req.params.id });
});

// 所有 HTTP 方法
app.all('/api/health', (req, res) => {
  res.json({ status: 'ok', method: req.method });
});

路由参数与查询参数

// 路由参数
app.get('/api/users/:id', (req, res) => {
  const { id } = req.params;
  res.json({ id });
});

// 可选参数
app.get('/api/posts/:category/:id?', (req, res) => {
  const { category, id } = req.params;
  res.json({ category, id: id || 'all' });
});

// 正则参数
app.get(/\/api\/files\/(.+)/, (req, res) => {
  const filePath = req.params[0];
  res.json({ path: filePath });
});

// 查询参数
app.get('/api/search', (req, res) => {
  const { q, page = 1, limit = 10 } = req.query;
  res.json({ query: q, page: Number(page), limit: Number(limit) });
});

Router 模块化

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

router.get('/', (req, res) => {
  res.json({ users: [] });
});

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

router.post('/', (req, res) => {
  res.status(201).json({ created: true });
});

module.exports = router;

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

app.use('/api/users', userRoutes);
// GET  /api/users      → 用户列表
// GET  /api/users/:id  → 单个用户
// POST /api/users      → 创建用户

12.3 中间件

中间件执行流程

请求 → 中间件1 → 中间件2 → ... → 路由处理 → 响应
         │          │                    │
         ↓          ↓                    ↓
       next()    next()              发送响应

内置中间件

// 解析 JSON 请求体
app.use(express.json({ limit: '10mb' }));

// 解析 URL 编码的请求体
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// 静态文件服务
app.use(express.static('public'));
app.use('/uploads', express.static('uploads'));
app.use(express.static('public', {
  maxAge: '1d',        // 缓存时间
  etag: true,           // ETag 支持
  lastModified: true,   // Last-Modified 支持
}));

自定义中间件

// 日志中间件
function logger(req, res, next) {
  const start = Date.now();
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.url} ${res.statusCode} ${duration}ms`);
  });
  next();
}

app.use(logger);

// 认证中间件
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: '未提供认证令牌' });
  }
  try {
    const payload = verifyToken(token);
    req.user = payload;
    next();
  } catch (err) {
    res.status(401).json({ error: '令牌无效' });
  }
}

// 路由级别中间件
app.get('/api/profile', authenticate, (req, res) => {
  res.json({ user: req.user });
});

// 路由组中间件
app.use('/api/admin', authenticate, adminRouter);

第三方中间件

const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');

// CORS — 跨域资源共享
app.use(cors({
  origin: ['https://example.com', 'http://localhost:3001'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true,
}));

// Helmet — 安全头部
app.use(helmet());

// Morgan — HTTP 请求日志
app.use(morgan('combined'));
// 或自定义格式
app.use(morgan(':method :url :status :response-time ms'));

// Compression — 响应压缩
app.use(compression());

12.4 请求与响应增强

// 请求处理
app.post('/api/users', express.json(), (req, res) => {
  console.log('Body:', req.body);
  console.log('Content-Type:', req.get('Content-Type'));
  console.log('IP:', req.ip);
  console.log('是否 XHR:', req.xhr);
  console.log('协议:', req.protocol);
  console.log('是否安全:', req.secure);
});

// 响应方法
app.get('/api/data', (req, res) => {
  // JSON 响应
  res.json({ data: 'hello' });
  
  // 或
  // res.send('文本响应');
  // res.send(Buffer.from('二进制'));
  // res.sendFile('/path/to/file.html');
  // res.download('/path/to/file.pdf');
  // res.redirect(301, '/new-url');
  // res.status(404).json({ error: 'Not found' });
});

// 链式调用
app.get('/api/chain', (req, res) => {
  res
    .status(200)
    .set({
      'X-Custom': 'value',
      'Cache-Control': 'max-age=3600',
    })
    .json({ ok: true });
});

12.5 错误处理

// 错误处理中间件(4 个参数)
app.use((err, req, res, next) => {
  console.error(err.stack);
  
  const status = err.status || 500;
  const message = process.env.NODE_ENV === 'production'
    ? 'Internal Server Error'
    : err.message;

  res.status(status).json({
    error: message,
    ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
  });
});

// 404 处理(放在所有路由之后)
app.use((req, res) => {
  res.status(404).json({ error: `Cannot ${req.method} ${req.url}` });
});

// 异步错误处理
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/api/data', asyncHandler(async (req, res) => {
  const data = await fetchData(); // 如果抛错,会被错误中间件捕获
  res.json(data);
}));

// 自定义错误类
class AppError extends Error {
  constructor(message, status) {
    super(message);
    this.status = status;
    this.name = 'AppError';
  }
}

app.get('/api/users/:id', asyncHandler(async (req, res) => {
  const user = await findUser(req.params.id);
  if (!user) {
    throw new AppError('用户不存在', 404);
  }
  res.json(user);
}));

12.6 模板引擎

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

// 设置模板引擎
app.set('view engine', 'ejs');
app.set('views', './views');

// EJS 模板示例
app.get('/', (req, res) => {
  res.render('index', {
    title: '首页',
    users: ['Alice', 'Bob', 'Charlie'],
    currentTime: new Date().toLocaleString('zh-CN'),
  });
});
<!-- views/index.ejs -->
<!DOCTYPE html>
<html>
<head>
  <title><%= title %></title>
</head>
<body>
  <h1><%= title %></h1>
  <p>当前时间: <%= currentTime %></p>
  
  <h2>用户列表</h2>
  <ul>
    <% users.forEach(user => { %>
      <li><%= user %></li>
    <% }); %>
  </ul>
  
  <% if (users.length === 0) { %>
    <p>暂无用户</p>
  <% } %>
</body>
</html>

12.7 实战项目结构

project/
├── package.json
├── .env
├── src/
│   ├── app.js              # Express 应用配置
│   ├── server.js            # 服务器启动
│   ├── config/
│   │   └── index.js         # 配置管理
│   ├── middleware/
│   │   ├── auth.js          # 认证中间件
│   │   ├── logger.js        # 日志中间件
│   │   └── errorHandler.js  # 错误处理
│   ├── routes/
│   │   ├── index.js         # 路由汇总
│   │   ├── users.js         # 用户路由
│   │   └── posts.js         # 文章路由
│   ├── controllers/
│   │   ├── userController.js
│   │   └── postController.js
│   ├── services/
│   │   ├── userService.js
│   │   └── postService.js
│   ├── models/
│   │   ├── User.js
│   │   └── Post.js
│   └── utils/
│       └── errors.js
├── views/                   # 模板文件
├── public/                  # 静态资源
└── tests/                   # 测试文件
// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const routes = require('./routes');
const errorHandler = require('./middleware/errorHandler');

const app = express();

// 中间件
app.use(helmet());
app.use(cors());
app.use(morgan('dev'));
app.use(express.json());
app.use(express.static('public'));

// 路由
app.use('/api', routes);

// 错误处理
app.use(errorHandler);

module.exports = app;

// src/server.js
const app = require('./app');
const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(`服务器运行在端口 ${PORT}`);
});

注意事项

⚠️ 中间件顺序很重要:Express 中间件按注册顺序执行,日志和安全中间件应放在路由之前。

⚠️ 异步错误不会自动捕获:Express 4.x 不会自动捕获 async 函数的错误,需要包装 asyncHandler 或使用 Express 5。

⚠️ 不要在生产环境暴露错误栈err.stack 包含敏感信息,生产环境应只返回通用错误消息。

⚠️ 设置请求体大小限制express.json({ limit: '10mb' }) 防止大请求体攻击。

业务场景

  1. RESTful API 服务:使用 Router 模块化组织 API 路由
  2. BFF 层:为前端应用聚合多个后端服务的数据
  3. 管理后台:配合模板引擎构建服务端渲染的管理界面
  4. Webhook 接收端:接收第三方平台的回调通知

扩展阅读


上一章第 11 章 · HTTP 服务与客户端 下一章第 13 章 · REST API 设计 — REST 设计原则、CRUD、版本控制和分页。