Node.js 实战
概述
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,凭借其事件驱动、非阻塞 I/O 模型,成为构建高性能 Web 应用的首选平台。本文将从 Express、Koa、Fastify 三大主流框架入手,深入讲解中间件模式、RESTful API 设计以及生产部署最佳实践。
Express 框架
Express 是 Node.js 生态中最成熟、使用最广泛的 Web 框架,提供了简洁而强大的 API 用于构建 Web 应用和 RESTful 接口。
路由系统
Express 的路由系统支持 GET、POST、PUT、DELETE 等 HTTP 方法,并允许通过路径参数和正则表达式匹配 URL:
const express = require('express');
const app = express();
// 基础路由
app.get('/users', (req, res) => {
res.json({ users: [] });
});
// 路径参数
app.get('/users/:id', (req, res) => {
res.json({ id: req.params.id });
});
// 链式路由
app.route('/articles')
.get((req, res) => { /* 获取列表 */ })
.post((req, res) => { /* 创建文章 */ });中间件机制
Express 的中间件是一个函数数组,请求会按照注册顺序依次经过每个中间件:
// 应用级中间件
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 路由级中间件
const authMiddleware = (req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: '未授权' });
}
next();
};
app.use('/api', authMiddleware);错误处理
Express 通过四个参数的中间件处理错误:
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message || '服务器内部错误'
});
});模板引擎
Express 支持多种模板引擎,如 EJS、Pug、Handlebars:
app.set('view engine', 'ejs');
app.set('views', './views');
app.get('/profile', (req, res) => {
res.render('profile', { name: '张三', age: 28 });
});Koa 框架
Koa 由 Express 原班人马打造,利用 async/await 彻底解决了回调地狱问题,提供了更现代的中间件模型。
async/await 中间件
Koa 的中间件通过 async 函数实现,使用 await next() 控制流程,形成经典的"洋葱模型":
const Koa = require('koa');
const app = new Koa();
// 记录请求耗时
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
// 响应处理
app.use(async (ctx) => {
ctx.body = { message: 'Hello Koa' };
});上下文对象
Koa 将请求和响应对象封装到统一的 ctx 上下文中,简化了 API:
app.use(async (ctx) => {
// 请求信息
console.log(ctx.method, ctx.url, ctx.headers);
console.log(ctx.query, ctx.params);
// 请求体解析需要额外中间件
// ctx.request.body
// 响应
ctx.status = 200;
ctx.body = {
data: '响应内容',
timestamp: Date.now()
};
});常用中间件生态
Koa 本身非常轻量,功能通过中间件扩展:
const Koa = require('koa');
const Router = require('@koa/router');
const bodyParser = require('koa-bodyparser');
const cors = require('@koa/cors');
const app = new Koa();
const router = new Router();
app.use(cors());
app.use(bodyParser());
router.get('/api/todos', async (ctx) => {
ctx.body = { todos: [] };
});
app.use(router.routes()).use(router.allowedMethods());Koa 的洋葱模型在执行顺序上比 Express 更可控,特别适合需要前置和后置处理的场景,如日志记录、事务管理、请求耗时统计等。
Fastify 框架
Fastify 以性能著称,号称最快的 Node.js Web 框架之一,同时内置了模式验证和强大的插件系统。
性能优势
Fastify 的性能优化体现在多个方面:
- JSON 序列化优化:使用
fast-json-stringify库,比原生JSON.stringify快 2-3 倍 - schema-based 序列化:通过 JSON Schema 定义响应结构,序列化时跳过类型检查
- 低开销路由:基于 Radix Tree 的路由匹配算法
const fastify = require('fastify')({ logger: true });
fastify.get('/users/:id', async (request, reply) => {
return { id: request.params.id, name: '测试用户' };
});模式验证
Fastify 内置基于 JSON Schema 的请求验证和响应序列化:
fastify.post('/users', {
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 2, maxLength: 50 },
email: { type: 'string', format: 'email' },
age: { type: 'number', minimum: 0, maximum: 150 }
}
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' }
}
}
}
}
}, async (request, reply) => {
const user = await createUser(request.body);
reply.code(201).send(user);
});插件系统
Fastify 的插件系统基于封装(encapsulation)概念,每个插件都有自己的作用域:
// 插件定义
async function todoPlugin(fastify, opts) {
fastify.decorate('todoDb', new Map());
fastify.get('/todos', async (request, reply) => {
return Array.from(fastify.todoDb.values());
});
}
// 注册插件
fastify.register(todoPlugin, { prefix: '/api' });三种框架对比
| 特性 | Express | Koa | Fastify |
|---|---|---|---|
| 性能 | 中等 | 较高 | 最高 |
| 中间件模型 | 线性(Callback) | 洋葱模型(Async/Await) | 线性 + 封装 |
| 路由 | 内置 | 需第三方(@koa/router) | 内置(Radix Tree) |
| 请求验证 | 无(需第三方) | 无(需第三方) | 内置(JSON Schema) |
| 插件系统 | 松散(app.use) | 松散(app.use) | 封装式(register) |
| TypeScript 支持 | 一般 | 一般 | 原生支持 |
| 错误处理 | 4 参数中间件 | try/catch + ctx.throw | 内置错误插件 |
| 序列化性能 | 标准 | 标准 | 优化(fast-json-stringify) |
| 学习曲线 | 低 | 中 | 中高 |
| 生态丰富度 | 极高 | 较高 | 中等 |
| 适用场景 | 传统 Web 应用、快速原型 | API 服务、需要精细控制的场景 | 高性能 API、微服务 |
中间件模式详解
中间件是 Node.js Web 框架的核心设计模式。不同框架的实现虽有差异,但核心理念一致:请求经过一系列处理单元后产生响应。
应用级中间件
应用级中间件对所有路由生效,常用于全局配置:
// Express
app.use(express.json());
app.use(cors());
app.use(compression());路由级中间件
路由级中间件只对特定路由或路由组生效:
const requireAuth = (req, res, next) => {
if (!req.session.user) {
return res.redirect('/login');
}
next();
};
// 仅保护特定路由
app.get('/dashboard', requireAuth, (req, res) => {
res.render('dashboard');
});
// 保护一组路由
app.use('/admin', requireAuth, adminRouter);错误处理中间件
错误处理是生产环境的关键环节,应当:
- 全局捕获未处理异常
- 统一错误响应格式
- 支持自定义错误类型
// 自定义错误类
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
// 全局错误处理
app.use((err, req, res, next) => {
const status = err.statusCode || 500;
const message = err.isOperational ? err.message : '服务器内部错误';
res.status(status).json({
success: false,
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});第三方中间件
常用的生产级中间件:
| 中间件 | 用途 |
|---|---|
| cors | 跨域资源共享 |
| helmet | HTTP 安全头 |
| compression | 响应压缩 |
| morgan | HTTP 请求日志 |
| rate-limiter-flexible | 速率限制 |
| express-session | 会话管理 |
| passport | 认证策略 |
| multer | 文件上传 |
RESTful API 设计规范
URL 设计
- 使用名词复数形式:
/users、/articles、/orders - 使用子资源表达关联:
/users/:id/orders - 使用查询参数进行过滤:
/users?role=admin&page=1&limit=20 - 不要使用动词:使用
POST /orders而非POST /createOrder
HTTP 方法语义
| 方法 | 操作 | 状态码 | 幂等 |
|---|---|---|---|
| GET | 获取资源 | 200 | 是 |
| POST | 创建资源 | 201 | 否 |
| PUT | 全量更新资源 | 200 | 是 |
| PATCH | 部分更新资源 | 200 | 是 |
| DELETE | 删除资源 | 204 | 是 |
状态码使用
200 OK:GET、PUT、PATCH 请求成功201 Created:POST 创建成功204 No Content:DELETE 删除成功400 Bad Request:请求参数错误401 Unauthorized:未认证403 Forbidden:无权限404 Not Found:资源不存在409 Conflict:资源冲突(如重复创建)422 Unprocessable Entity:验证失败429 Too Many Requests:请求频率过高500 Internal Server Error:服务器内部错误
响应格式
统一的 JSON 响应结构:
// 成功响应
{
"success": true,
"data": { ... },
"meta": {
"page": 1,
"limit": 20,
"total": 100
}
}
// 错误响应
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "邮箱格式不正确",
"details": [
{ "field": "email", "message": "请输入有效的邮箱地址" }
]
}
}实战:完整 CRUD API 示例
以下使用 Express 实现一个完整的待办事项 RESTful API,包含数据验证、错误处理和完整的 CRUD 操作。
const express = require('express');
const app = express();
app.use(express.json());
// 内存数据存储
let todos = [];
let nextId = 1;
// 验证中间件
const validateTodo = (req, res, next) => {
const { title } = req.body;
if (!title || typeof title !== 'string' || title.trim().length === 0) {
return res.status(422).json({
success: false,
error: {
code: 'VALIDATION_ERROR',
message: '标题不能为空'
}
});
}
req.body.title = title.trim();
next();
};
// GET /api/todos - 获取所有待办事项
app.get('/api/todos', (req, res) => {
const { completed, keyword } = req.query;
let result = [...todos];
if (completed !== undefined) {
const isCompleted = completed === 'true';
result = result.filter(t => t.completed === isCompleted);
}
if (keyword) {
result = result.filter(t =>
t.title.toLowerCase().includes(keyword.toLowerCase())
);
}
res.json({
success: true,
data: result,
meta: { total: result.length }
});
});
// GET /api/todos/:id - 获取单个待办事项
app.get('/api/todos/:id', (req, res) => {
const id = parseInt(req.params.id, 10);
const todo = todos.find(t => t.id === id);
if (!todo) {
return res.status(404).json({
success: false,
error: { code: 'NOT_FOUND', message: '待办事项不存在' }
});
}
res.json({ success: true, data: todo });
});
// POST /api/todos - 创建待办事项
app.post('/api/todos', validateTodo, (req, res) => {
const todo = {
id: nextId++,
title: req.body.title,
completed: false,
createdAt: new Date().toISOString()
};
todos.push(todo);
res.status(201).json({ success: true, data: todo });
});
// PUT /api/todos/:id - 更新待办事项
app.put('/api/todos/:id', (req, res) => {
const id = parseInt(req.params.id, 10);
const todo = todos.find(t => t.id === id);
if (!todo) {
return res.status(404).json({
success: false,
error: { code: 'NOT_FOUND', message: '待办事项不存在' }
});
}
const { title, completed } = req.body;
if (title !== undefined) {
if (typeof title !== 'string' || title.trim().length === 0) {
return res.status(422).json({
success: false,
error: { code: 'VALIDATION_ERROR', message: '标题不能为空' }
});
}
todo.title = title.trim();
}
if (completed !== undefined) {
todo.completed = Boolean(completed);
}
todo.updatedAt = new Date().toISOString();
res.json({ success: true, data: todo });
});
// DELETE /api/todos/:id - 删除待办事项
app.delete('/api/todos/:id', (req, res) => {
const id = parseInt(req.params.id, 10);
const index = todos.findIndex(t => t.id === id);
if (index === -1) {
return res.status(404).json({
success: false,
error: { code: 'NOT_FOUND', message: '待办事项不存在' }
});
}
todos.splice(index, 1);
res.status(204).send();
});
// 全局错误处理
app.use((err, req, res, next) => {
console.error('未捕获错误:', err);
res.status(500).json({
success: false,
error: { code: 'INTERNAL_ERROR', message: '服务器内部错误' }
});
});
app.listen(3000, () => {
console.log('Todo API 服务已启动: http://localhost:3000');
});Koa 版本对比
使用 Koa 实现同样的功能:
const Koa = require('koa');
const Router = require('@koa/router');
const bodyParser = require('koa-bodyparser');
const app = new Koa();
const router = new Router({ prefix: '/api' });
app.use(bodyParser());
let todos = [];
let nextId = 1;
// 错误处理中间件
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
success: false,
error: { code: 'ERROR', message: err.message }
};
}
});
router.get('/todos', (ctx) => {
ctx.body = { success: true, data: todos, meta: { total: todos.length } };
});
router.get('/todos/:id', (ctx) => {
const todo = todos.find(t => t.id === parseInt(ctx.params.id));
if (!todo) {
ctx.status = 404;
ctx.body = { success: false, error: { code: 'NOT_FOUND', message: '待办事项不存在' } };
return;
}
ctx.body = { success: true, data: todo };
});
router.post('/todos', (ctx) => {
const { title } = ctx.request.body;
if (!title) {
ctx.status = 422;
ctx.body = { success: false, error: { code: 'VALIDATION_ERROR', message: '标题不能为空' } };
return;
}
const todo = { id: nextId++, title, completed: false, createdAt: new Date().toISOString() };
todos.push(todo);
ctx.status = 201;
ctx.body = { success: true, data: todo };
});
router.put('/todos/:id', (ctx) => {
const todo = todos.find(t => t.id === parseInt(ctx.params.id));
if (!todo) {
ctx.status = 404;
ctx.body = { success: false, error: { code: 'NOT_FOUND', message: '待办事项不存在' } };
return;
}
const { title, completed } = ctx.request.body;
if (title) todo.title = title;
if (completed !== undefined) todo.completed = completed;
todo.updatedAt = new Date().toISOString();
ctx.body = { success: true, data: todo };
});
router.delete('/todos/:id', (ctx) => {
const index = todos.findIndex(t => t.id === parseInt(ctx.params.id));
if (index === -1) {
ctx.status = 404;
ctx.body = { success: false, error: { code: 'NOT_FOUND', message: '待办事项不存在' } };
return;
}
todos.splice(index, 1);
ctx.status = 204;
});
app.use(router.routes()).use(router.allowedMethods());Fastify 版本对比
使用 Fastify 实现同样的功能:
const fastify = require('fastify')({ logger: true });
let todos = [];
let nextId = 1;
const TodoSchema = {
type: 'object',
properties: {
id: { type: 'number' },
title: { type: 'string' },
completed: { type: 'boolean' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' }
}
};
fastify.get('/api/todos', {
schema: {
response: { 200: { type: 'array', items: TodoSchema } }
}
}, async () => {
return todos;
});
fastify.get('/api/todos/:id', {
schema: {
params: { type: 'object', properties: { id: { type: 'number' } } },
response: { 200: TodoSchema }
}
}, async (request, reply) => {
const todo = todos.find(t => t.id === request.params.id);
if (!todo) {
reply.code(404);
throw new Error('待办事项不存在');
}
return todo;
});
fastify.post('/api/todos', {
schema: {
body: {
type: 'object', required: ['title'],
properties: { title: { type: 'string', minLength: 1 } }
},
response: { 201: TodoSchema }
}
}, async (request, reply) => {
const todo = {
id: nextId++,
title: request.body.title,
completed: false,
createdAt: new Date().toISOString()
};
todos.push(todo);
reply.code(201);
return todo;
});
fastify.put('/api/todos/:id', async (request, reply) => {
const todo = todos.find(t => t.id === request.params.id);
if (!todo) {
reply.code(404);
throw new Error('待办事项不存在');
}
if (request.body.title) todo.title = request.body.title;
if (request.body.completed !== undefined) todo.completed = request.body.completed;
todo.updatedAt = new Date().toISOString();
return todo;
});
fastify.delete('/api/todos/:id', async (request, reply) => {
const index = todos.findIndex(t => t.id === request.params.id);
if (index === -1) {
reply.code(404);
throw new Error('待办事项不存在');
}
todos.splice(index, 1);
reply.code(204);
});生产部署最佳实践
环境管理
项目根目录/
├── .env.production # 生产环境变量
├── .env.staging # 预发布环境变量
├── .env.development # 开发环境变量
└── .env.example # 环境变量模板使用 dotenv 管理环境配置:
const dotenv = require('dotenv');
const path = require('path');
const envFile = process.env.NODE_ENV || 'development';
dotenv.config({ path: path.resolve(process.cwd(), `.env.${envFile}`) });进程管理
推荐使用 PM2 管理 Node.js 进程:
# 启动应用,开启集群模式
pm2 start app.js -i max --name my-api
# 保存进程列表
pm2 save
# 设置开机自启
pm2 startup
# 查看状态
pm2 status
pm2 monitPM2 配置示例(ecosystem.config.js):
module.exports = {
apps: [{
name: 'todo-api',
script: 'app.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
max_memory_restart: '500M',
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss'
}]
};日志管理
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
// 生产环境下不使用 console.log
if (process.env.NODE_ENV === 'production') {
console.log = (...args) => logger.info(...args);
console.error = (...args) => logger.error(...args);
}安全最佳实践
# 安装安全相关中间件
npm install helmet cors express-rate-limit hppconst helmet = require('helmet');
const rateLimit = require('express-rate-limit');
// HTTP 安全头
app.use(helmet());
// 请求频率限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 最多 100 次请求
standardHeaders: true,
legacyHeaders: false,
message: { success: false, error: { code: 'RATE_LIMIT', message: '请求频率过高,请稍后再试' } }
});
app.use('/api', limiter);
// 防止参数污染
app.use(hpp());Docker 部署
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine
WORKDIR /app
RUN addgroup --system app && adduser --system app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER app
EXPOSE 3000
CMD ["node", "app.js"]性能监控与告警
- 应用性能监控:使用 Elastic APM、Datadog 或 OpenTelemetry
- 健康检查端点:暴露
/health路由,返回服务状态 - 内存泄漏监控:定期使用
heapdump分析堆内存 - 错误追踪:集成 Sentry 或 Elastic APM 进行实时错误追踪
// 健康检查端点
app.get('/health', (req, res) => {
res.json({
status: 'UP',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage()
});
});容器化部署完整流程
# docker-compose.yml
version: '3.8'
services:
api:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=postgres
depends_on:
- postgres
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health')"]
interval: 30s
timeout: 5s
retries: 3
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- api
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: todos
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
volumes:
pgdata:在线演示
以下为待办事项 REST API 的在线调试演示,可直接在浏览器中发送 GET、POST、PUT、DELETE 请求,查看请求与响应的完整详情。