Skip to content

ExpressJS - 速查表

在这份速查表中,我们将涵盖 Express.js 的核心功能和概念,以在 2024 年构建健壮且可扩展的应用程序。我们将探讨路由、中间件、现代异步模式和数据处理。你将学习如何使用现代工具链设置项目、处理各种 HTTP 方法、管理静态资产以及使用最佳实践与数据库集成。本指南是您使用 Express.js 构建高效 API 和 Web 应用程序的快速参考。

  • 简介
  • 现代项目设置
  • 核心概念:路由
  • 核心概念:中间件
  • 内容服务:视图与静态文件
  • 请求和响应对象
  • 使用 express.Router 进行模块化路由
  • 应用程序方法
  • 现代错误处理
  • 调试和日志记录
  • 使用 Async/Await 进行数据库集成
  • 使用 JWT 进行安全身份验证
  • 基本安全实践
  • 使用现代工具进行测试
  • 部署到云端

Express.js 是一个极简且灵活的 Node.js Web 应用程序框架,它为 Web 和移动应用程序提供了强大的功能集。它充当基本 Web 应用程序功能的薄层,而不会掩盖你熟悉和喜爱的 Node.js 功能。

为你的项目创建一个新目录,进入该目录,并初始化一个 package.json 文件。此文件将跟踪你项目的元数据和依赖项。

$ mkdir my-modern-app
$ cd my-modern-app
$ npm init -y

安装 Express 并将其添加到你的项目依赖项中。

$ npm install express

此代码使用现代 ES 模块语法设置了一个基本的 Express 服务器。它监听由环境变量定义的端口(最佳实践),并响应一个 JSON 消息。要使用 ES 模块语法 (import/export),请在 package.json 中添加 "type": "module"。

import express from 'express';
// 最佳实践:使用环境变量进行配置
const PORT = process.env.PORT || 3000;
const app = express();
app.get('/', (req, res) => {
res.json({ message: 'Hello Modern World!' });
});
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});

在开发过程中,强烈建议使用像 nodemon 这样的工具,它会在文件更改时自动重启服务器。

# 将 nodemon 作为开发依赖项安装
$ npm install --save-dev nodemon
# 在你的 package.json 中添加一个脚本
# "scripts": {
# "dev": "nodemon index.js"
# }
# 运行开发服务器
$ npm run dev

路由定义了应用程序如何响应客户端对特定端点的请求,该端点是 URI(或路径)和特定的 HTTP 请求方法(GET、POST 等)。

每个路由可以有一个或多个处理函数,这些函数在路由匹配时执行。

// GET 方法路由
app.get('/', (req, res) => res.send('GET request to the homepage'));
// POST 方法路由
app.post('/users', (req, res) => res.send('POST request to create a user'));
// 带有参数的动态路由
app.get('/users/:userId', (req, res) => {
res.send(`Details for user ${req.params.userId}`);
});

你可以提供多个回调函数,它们像中间件一样处理请求。唯一的例外是这些回调可以调用 next('route') 来绕过剩余的路由回调。

const logTimestamp = (req, res, next) => {
console.log('Time: ', Date.now());
next(); // 将控制权传递给下一个处理程序
};
app.get('/with-handler', [logTimestamp], (req, res) => {
res.send('Check the console for a timestamp!');
});

中间件函数是那些可以访问请求对象 (req)、响应对象 (res) 以及应用程序请求-响应周期中的 next 函数的函数。它们可以执行任何代码,对请求和响应对象进行更改,结束请求-响应周期,并调用堆栈中的下一个中间件。

Express 包含用于常见任务的内置中间件。自 Express 4.16+ 起,流行的 body-parser 已被包含在内。

// 用于解析 JSON 请求体的中间件
app.use(express.json());
// 用于解析 URL 编码请求体的中间件
app.use(express.urlencoded({ extended: true }));
// 用于从 'public' 目录提供静态文件的中间件
app.use(express.static('public'));

编写你自己的中间件很简单。这是一个记录请求方法和路径的日志器。

const requestLogger = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
next(); // 将控制权传递给下一个处理程序
};
app.use(requestLogger);

虽然许多现代 Express 应用程序充当前端客户端(如 React 或 Vue)的 JSON API,但它也可以使用模板引擎在服务器上渲染动态 HTML。

Pug、EJS 和 Handlebars 是流行的选择。以下是设置 EJS 的方法。

$ npm install ejs
// 在你的 app.js 中
app.set('view engine', 'ejs');
// 可选:指定模板所在的目录
// app.set('views', path.join(__dirname, 'views'));

2. 创建模板(例如 views/profile.ejs)

Section titled “2. 创建模板(例如 views/profile.ejs)”

模板可以嵌入从路由处理程序传递的动态数据。

<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1>Hello, <%= user.name %>!</h1>
</body>
</html>

使用 res.render() 渲染视图并将渲染后的 HTML 字符串发送到客户端。

app.get('/profile', (req, res) => {
const userData = { name: 'Alex' };
res.render('profile', {
title: 'User Profile',
user: userData
});
});

路由处理程序和中间件可以访问 req 和 res 对象,它们分别代表 HTTP 请求和响应。

属性说明
req.params路由参数(例如,/users/:id)
req.query查询字符串参数(例如,?sort=desc)
req.body解析后的请求体(需要 express.json() 或 express.urlencoded())
req.headers传入的请求头
req.method请求的 HTTP 方法(GET、POST 等)
req.originalUrl请求的完整原始 URL
方法说明
res.send()发送各种类型的响应(字符串、Buffer、对象)
res.json()发送 JSON 响应(自动设置 Content-Type)
res.status()设置响应的 HTTP 状态码
res.render()渲染视图模板
res.redirect()重定向到指定的 URL
res.cookie()设置 Cookie
app.get('/api/health', (req, res) => {
res.status(200).json({ status: 'UP' });
});

使用 express.Router 进行模块化路由

Section titled “使用 express.Router 进行模块化路由”

对于大型应用程序,最佳实践是使用 express.Router 将路由处理程序分组到单独的文件中,从而保持主应用程序文件的整洁。

1. 创建路由文件(例如 routes/userRoutes.js)

Section titled “1. 创建路由文件(例如 routes/userRoutes.js)”
import { Router } from 'express';
const router = Router();
// 此路由现在实际上是 /api/users/
router.get('/', (req, res) => {
res.send('List of all users');
});
// 此路由现在实际上是 /api/users/profile
router.get('/profile', (req, res) => {
res.send('User profile page');
});
export default router;

2. 在你的主应用程序文件中挂载路由

Section titled “2. 在你的主应用程序文件中挂载路由”
import express from 'express';
import userRoutes from './routes/userRoutes.js';
const app = express();
// 将路由挂载到 /api/users 路径
app.use('/api/users', userRoutes);
// ... 其他应用程序设置 ...
app.listen(3000);
方法说明
app.use([path], callback)在指定路径挂载中间件函数
app.get(path, callback)处理 GET 请求
app.post(path, callback)处理 POST 请求
app.put(path, callback)处理 PUT 请求
app.delete(path, callback)处理 DELETE 请求
app.listen(port, [callback])将应用程序绑定到端口并启动服务器
app.set(name, value)将设置名称 name 赋值为 value
app.engine(ext, callback)将给定的模板引擎回调注册为 ext

集中式错误处理中间件是一种最佳实践。它应该在所有其他 app.use() 和路由调用之后定义。这个特殊的中间件有四个参数(err、req、res、next)。

// 未匹配路由的 404 处理程序
app.use((req, res, next) => {
res.status(404).send('Sorry, that route does not exist.');
});
// 集中式错误处理程序
app.use((err, req, res, next) => {
console.error(err.stack);
// 在生产环境中避免泄露堆栈跟踪
const statusCode = err.statusCode || 500;
const message = process.env.NODE_ENV === 'production' ? 'Something broke!' : err.message;
res.status(statusCode).json({ error: message });
});

有效的调试和日志记录对于开发和生产至关重要。

使用 --inspect 标志运行你的应用程序,以启用 Chrome DevTools 或 VS Code 调试器等工具进行调试。这允许你设置断点并实时检查变量。

$ node --inspect index.js

对于生产环境,简单的 console.log 是不够的。使用 pino 或 winston 等结构化日志库来获取可过滤、机器可读的日志。

$ npm install pino
// 示例用法
import pino from 'pino';
const logger = pino({ level: 'info' });
logger.info('Server started successfully.');
logger.error({ err }, 'An error occurred during request processing.');

现代 Node.js 大力推崇使用 async/await 来处理数据库查询等异步操作。这使得代码比回调或 .then() 链更清晰、更容易理解。

pg 库是连接 PostgreSQL 的标准。它完全支持 Promise,非常适合 async/await。

$ npm install pg
// db.js
import pg from 'pg';
const { Pool } = pg;
const pool = new Pool({
connectionString: process.env.DATABASE_URL, // 例如:postgresql://user:password@host:port/database
});
export default pool;
// 在路由中使用
import pool from './db.js';
app.get('/users', async (req, res, next) => {
try {
const { rows } = await pool.query('SELECT * FROM users');
res.json(rows);
} catch (err) {
next(err); // 将错误传递给中心处理程序
}
});

原生的 mongodb 驱动程序是完全异步优先的。

$ npm install mongodb
// mongo.js
import { MongoClient } from 'mongodb';
const uri = process.env.MONGO_URI; // 例如:mongodb://localhost:27017
const client = new MongoClient(uri);
let db;
async function connectToDatabase() {
try {
await client.connect();
db = client.db('my-app-db');
console.log('Connected to MongoDB');
} catch (err) {
console.error('Failed to connect to MongoDB', err);
process.exit(1);
}
}
export { connectToDatabase, db };
// 在路由中使用
import { db } from './mongo.js';
app.get('/products', async (req, res, next) => {
try {
const products = await db.collection('products').find({}).toArray();
res.json(products);
} catch (err) {
next(err);
}
});

JSON Web Tokens (JWT) 是一种在 API 中处理无状态身份验证的标准方式。

# JWT 用于令牌创建,bcrypt 用于密码哈希
$ npm install jsonwebtoken bcryptjs

2. 用户注册和登录(控制器逻辑)

Section titled “2. 用户注册和登录(控制器逻辑)”

此逻辑使用 async/await 实现清晰可读的代码,并使用 try/catch 进行错误处理。至关重要的是,像 JWT_SECRET 这样的密钥必须存储在环境变量中,而不是硬编码。

import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
// 假设用户模型来自 Mongoose 或 Prisma 等数据库 ORM
import User from '../models/User.js';
export const register = async (req, res, next) => {
try {
const { email, password, name } = req.body;
const hashedPassword = await bcrypt.hash(password, 12);
const newUser = new User({ email, name, password: hashedPassword });
await newUser.save();
res.status(201).json({ message: 'User created!' });
} catch (err) {
next(err);
}
};
export const login = async (req, res, next) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ message: 'Authentication failed' });
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ message: 'Authentication failed' });
}
const token = jwt.sign(
{ id: user._id, email: user.email },
process.env.JWT_SECRET, // 使用环境变量!
{ expiresIn: '1h' }
);
res.status(200).json({ token });
} catch (err) {
next(err);
}
};

此中间件通过验证 Authorization 头部中的 JWT 来保护路由。

import jwt from 'jsonwebtoken';
export const protect = (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'No token, authorization denied' });
}
const token = authHeader.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // 将解码后的用户 payload 添加到请求中
next();
} catch (err) {
res.status(401).json({ message: 'Token is not valid' });
}
};
// 用法:
// app.get('/api/protected-route', protect, (req, res) => { ... });

安全不是事后才考虑的事情。从这些基本的中间件和实践开始。

helmet 设置各种 HTTP 头,以帮助保护你的应用程序免受已知 Web 漏洞的侵害。

$ npm install helmet
// 在 app.js 中
import helmet from 'helmet';
app.use(helmet());

使用 express-rate-limit 来防止暴力破解攻击和拒绝服务攻击。

$ npm install express-rate-limit
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 将每个 IP 的请求限制为每个时间窗口 100 次
standardHeaders: true, // 在 `RateLimit-*` 头中返回速率限制信息
legacyHeaders: false, // 禁用 `X-RateLimit-*` 头
});
// 将速率限制中间件应用于所有请求
app.use(limiter);

基于会话的应用程序的 CSRF 保护

Section titled “基于会话的应用程序的 CSRF 保护”

跨站请求伪造 (CSRF) 是使用 Cookie/会话的传统 Web 应用程序关注的问题。对于在头部使用 JWT 的无状态 API,CSRF 的风险较小。如果你正在构建一个使用会话的服务器渲染应用程序,则需要像 csurf 的继任者这样的库或双重提交 Cookie 模式。对于现代 SPA,请确保你正在使用 SameSite Cookie 和正确的 CORS 策略。

测试运行器(如 Jest 或 Vitest)和 HTTP 断言库(Supertest)的组合是测试 Express 应用程序的强大设置。

我们将使用 Vitest 的速度和现代特性,以及 Supertest 来向我们的应用程序发出请求。

$ npm install --save-dev vitest supertest

2. 为测试构建你的应用程序结构

Section titled “2. 为测试构建你的应用程序结构”

将应用程序创建与服务器监听部分分开。这允许你在不启动服务器的情况下将 app 对象导入到测试中。

app.js
import express from 'express';
const app = express();
app.get('/', (req, res) => res.status(200).send('OK'));
export default app;
// index.js (或 server.js)
import app from './app.js';
const PORT = 3000;
app.listen(PORT, () => console.log(`Listening on port ${PORT}`));

此测试使用 supertest 对内存中的应用程序发出实际 HTTP 请求,并使用 vitest 定义测试套件和断言。

import { describe, it, expect } from 'vitest';
import request from 'supertest';
import app from '../app.js'; // 导入 Express 应用程序
describe('GET /', () => {
it('should respond with a 200 status code and OK text', async () => {
const response = await request(app).get('/');
expect(response.statusCode).toBe(200);
expect(response.text).toBe('OK');
});
});
describe('GET /non-existent-route', () => {
it('should respond with a 404 status code', async () => {
const response = await request(app).get('/not-a-real-route');
expect(response.statusCode).toBe(404);
});
});

像 Vercel、Render 和 Heroku 这样的平台使得部署 Node.js 应用程序变得简单。以下是 Vercel 的快速指南,它在无服务器部署方面表现出色。

Vercel 可以自动将你的 Express 应用程序转换为无服务器函数。将你的主 Express 应用程序文件放在 /api 目录中。

my-modern-app/
├── api/
│ └── index.js <-- 你的 Express 应用程序代码在这里
├── public/ <-- 静态文件(可选)
└── package.json

2. 更新你的 Express 应用程序 (api/index.js)

Section titled “2. 更新你的 Express 应用程序 (api/index.js)”

确保你的应用程序导出了 app 对象。Vercel 的构建过程将处理服务器创建。

import express from 'express';
const app = express();
app.get('/api/hello', (req, res) => {
res.send('Hello from Express on Vercel!');
});
// 导出应用程序
export default app;

此配置文件告诉 Vercel 将所有传入请求定向到你的无服务器 Express 应用程序。

{
"version": 2,
"rewrites": [
{ "source": "/api/(.*)", "destination": "/api" }
]
}

安装 Vercel CLI,链接你的项目,然后进行部署。

# 全局安装 Vercel CLI
$ npm install -g vercel
# 从项目根目录部署
$ vercel
# 部署到生产环境
$ vercel --prod