路由和导航
概述
路由是决定应用程序如何响应特定端点客户端请求的机制。本章介绍 Node.js 中的 HTTP 路由,从使用内置 http 模块的基础路由到使用 Express.js 的高级路由。
基础 HTTP 路由
使用 HTTP 模块的手动路由
javascript
// basic-routing.js
const http = require('http');
const url = require('url');
const querystring = require('querystring');
class BasicRouter {
constructor() {
this.routes = new Map();
}
addRoute(method, path, handler) {
const key = `${method.toUpperCase()}:${path}`;
this.routes.set(key, handler);
}
get(path, handler) {
this.addRoute('GET', path, handler);
}
post(path, handler) {
this.addRoute('POST', path, handler);
}
put(path, handler) {
this.addRoute('PUT', path, handler);
}
delete(path, handler) {
this.addRoute('DELETE', path, handler);
}
async handleRequest(req, res) {
const parsedUrl = url.parse(req.url, true);
const path = parsedUrl.pathname;
const method = req.method;
const query = parsedUrl.query;
// Add query parameters to request
req.query = query;
// Parse body for POST/PUT requests
if (method === 'POST' || method === 'PUT') {
req.body = await this.parseBody(req);
}
// Find matching route
const routeKey = `${method}:${path}`;
const handler = this.routes.get(routeKey);
if (handler) {
try {
await handler(req, res);
} catch (error) {
this.handleError(res, error);
}
} else {
this.handle404(res);
}
}
async parseBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const contentType = req.headers['content-type'];
if (contentType && contentType.includes('application/json')) {
resolve(JSON.parse(body));
} else if (contentType && contentType.includes('application/x-www-form-urlencoded')) {
resolve(querystring.parse(body));
} else {
resolve(body);
}
} catch (error) {
reject(error);
}
});
req.on('error', reject);
});
}
handleError(res, error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Internal Server Error',
message: error.message
}));
}
handle404(res) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Not Found',
message: 'Route not found'
}));
}
}
// Usage example
const router = new BasicRouter();
// Define routes
router.get('/', (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Welcome to the API' }));
});
router.get('/users', (req, res) => {
const { page = 1, limit = 10 } = req.query;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
users: [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
],
pagination: { page: parseInt(page), limit: parseInt(limit) }
}));
});
router.post('/users', (req, res) => {
const userData = req.body;
// Simulate user creation
const newUser = {
id: Date.now(),
...userData,
createdAt: new Date().toISOString()
};
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(newUser));
});
// Create server
const server = http.createServer((req, res) => {
router.handleRequest(req, res);
});
server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});Express.js 路由
基础 Express 路由
javascript
// express-basic-routing.js
const express = require('express');
const app = express();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Basic routes
app.get('/', (req, res) => {
res.json({ message: 'Welcome to Express API' });
});
// Route with parameters
app.get('/users/:id', (req, res) => {
const { id } = req.params;
res.json({
user: {
id: parseInt(id),
name: `User ${id}`,
email: `user${id}@example.com`
}
});
});
// Route with query parameters
app.get('/search', (req, res) => {
const { q, category, page = 1, limit = 10 } = req.query;
res.json({
query: q,
category,
results: [],
pagination: {
page: parseInt(page),
limit: parseInt(limit)
}
});
});
// Multiple route parameters
app.get('/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params;
res.json({
post: {
id: parseInt(postId),
userId: parseInt(userId),
title: `Post ${postId} by User ${userId}`
}
});
});
// Route with optional parameters
app.get('/products/:category/:subcategory?', (req, res) => {
const { category, subcategory } = req.params;
res.json({
category,
subcategory: subcategory || 'all',
products: []
});
});
// Wildcard routes
app.get('/files/*', (req, res) => {
const filePath = req.params[0];
res.json({
message: `Accessing file: ${filePath}`,
fullPath: `/files/${filePath}`
});
});
app.listen(3000, () => {
console.log('Express server running on http://localhost:3000');
});高级 Express 路由
javascript
// express-advanced-routing.js
const express = require('express');
const app = express();
app.use(express.json());
// Route-specific middleware
const authenticateUser = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
// Simulate token validation
req.user = { id: 1, name: 'John Doe' };
next();
};
const validateUserData = (req, res, next) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
next();
};
// Routes with middleware
app.get('/protected', authenticateUser, (req, res) => {
res.json({
message: 'This is a protected route',
user: req.user
});
});
app.post('/users', validateUserData, (req, res) => {
const userData = req.body;
res.status(201).json({
message: 'User created successfully',
user: {
id: Date.now(),
...userData,
createdAt: new Date().toISOString()
}
});
});
// Route handlers with multiple functions
app.get('/multi-handler',
(req, res, next) => {
console.log('First handler');
req.customData = 'Hello from first handler';
next();
},
(req, res, next) => {
console.log('Second handler');
req.customData += ' and second handler';
next();
},
(req, res) => {
res.json({ message: req.customData });
}
);
// Route parameter validation
app.param('userId', (req, res, next, userId) => {
const id = parseInt(userId);
if (isNaN(id) || id <= 0) {
return res.status(400).json({ error: 'Invalid user ID' });
}
req.userId = id;
next();
});
app.get('/users/:userId', (req, res) => {
res.json({
user: {
id: req.userId,
name: `User ${req.userId}`
}
});
});
// Error handling for specific routes
app.get('/error-demo', (req, res, next) => {
const error = new Error('Something went wrong');
error.status = 500;
next(error);
});
// Route-level error handler
app.use('/api', (error, req, res, next) => {
res.status(error.status || 500).json({
error: error.message,
path: req.path
});
});
app.listen(3000);路由模块
创建模块化路由
javascript
// routes/users.js
const express = require('express');
const router = express.Router();
// Middleware specific to this router
router.use((req, res, next) => {
console.log('Users router middleware');
req.timestamp = new Date().toISOString();
next();
});
// Mock data
let users = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
// GET /users
router.get('/', (req, res) => {
const { page = 1, limit = 10, search } = req.query;
let filteredUsers = users;
if (search) {
filteredUsers = users.filter(user =>
user.name.toLowerCase().includes(search.toLowerCase()) ||
user.email.toLowerCase().includes(search.toLowerCase())
);
}
const startIndex = (page - 1) * limit;
const endIndex = startIndex + parseInt(limit);
const paginatedUsers = filteredUsers.slice(startIndex, endIndex);
res.json({
users: paginatedUsers,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: filteredUsers.length,
pages: Math.ceil(filteredUsers.length / limit)
},
timestamp: req.timestamp
});
});
// GET /users/:id
router.get('/:id', (req, res) => {
const id = parseInt(req.params.id);
const user = users.find(u => u.id === id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ user, timestamp: req.timestamp });
});
// POST /users
router.post('/', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
const newUser = {
id: Math.max(...users.map(u => u.id)) + 1,
name,
email,
createdAt: req.timestamp
};
users.push(newUser);
res.status(201).json({
message: 'User created successfully',
user: newUser
});
});
// PUT /users/:id
router.put('/:id', (req, res) => {
const id = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === id);
if (userIndex === -1) {
return res.status(404).json({ error: 'User not found' });
}
const { name, email } = req.body;
users[userIndex] = {
...users[userIndex],
name: name || users[userIndex].name,
email: email || users[userIndex].email,
updatedAt: req.timestamp
};
res.json({
message: 'User updated successfully',
user: users[userIndex]
});
});
// DELETE /users/:id
router.delete('/:id', (req, res) => {
const id = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === id);
if (userIndex === -1) {
return res.status(404).json({ error: 'User not found' });
}
const deletedUser = users.splice(userIndex, 1)[0];
res.json({
message: 'User deleted successfully',
user: deletedUser
});
});
module.exports = router;javascript
// routes/posts.js
const express = require('express');
const router = express.Router();
let posts = [
{ id: 1, title: 'First Post', content: 'This is the first post', userId: 1 },
{ id: 2, title: 'Second Post', content: 'This is the second post', userId: 2 }
];
// GET /posts
router.get('/', (req, res) => {
const { userId, page = 1, limit = 10 } = req.query;
let filteredPosts = posts;
if (userId) {
filteredPosts = posts.filter(post => post.userId === parseInt(userId));
}
res.json({
posts: filteredPosts,
total: filteredPosts.length
});
});
// GET /posts/:id
router.get('/:id', (req, res) => {
const id = parseInt(req.params.id);
const post = posts.find(p => p.id === id);
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
res.json({ post });
});
// POST /posts
router.post('/', (req, res) => {
const { title, content, userId } = req.body;
if (!title || !content || !userId) {
return res.status(400).json({ error: 'Title, content, and userId are required' });
}
const newPost = {
id: Math.max(...posts.map(p => p.id)) + 1,
title,
content,
userId: parseInt(userId),
createdAt: new Date().toISOString()
};
posts.push(newPost);
res.status(201).json({
message: 'Post created successfully',
post: newPost
});
});
module.exports = router;javascript
// app.js - Using modular routes
const express = require('express');
const usersRouter = require('./routes/users');
const postsRouter = require('./routes/posts');
const app = express();
// Global middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Logging middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.path} - ${new Date().toISOString()}`);
next();
});
// Mount routers
app.use('/api/users', usersRouter);
app.use('/api/posts', postsRouter);
// Root route
app.get('/', (req, res) => {
res.json({
message: 'API Server',
endpoints: {
users: '/api/users',
posts: '/api/posts'
}
});
});
// 404 handler
app.use('*', (req, res) => {
res.status(404).json({
error: 'Route not found',
path: req.originalUrl
});
});
// Error handler
app.use((error, req, res, next) => {
console.error('Error:', error);
res.status(error.status || 500).json({
error: error.message || 'Internal Server Error',
path: req.path
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});高级路由模式
路由版本控制
javascript
// routes/v1/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({
version: 'v1',
users: [
{ id: 1, name: 'John Doe' }
]
});
});
module.exports = router;javascript
// routes/v2/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({
version: 'v2',
data: {
users: [
{
id: 1,
firstName: 'John',
lastName: 'Doe',
profile: {
email: 'john@example.com'
}
}
],
meta: {
total: 1,
page: 1
}
}
});
});
module.exports = router;javascript
// app-versioned.js
const express = require('express');
const app = express();
app.use(express.json());
// Version 1 routes
app.use('/api/v1/users', require('./routes/v1/users'));
// Version 2 routes
app.use('/api/v2/users', require('./routes/v2/users'));
// Default to latest version
app.use('/api/users', require('./routes/v2/users'));
// Version detection middleware
app.use('/api', (req, res, next) => {
const version = req.headers['api-version'] || 'v2';
req.apiVersion = version;
next();
});
app.listen(3000);动态路由加载
javascript
// utils/route-loader.js
const fs = require('fs');
const path = require('path');
class RouteLoader {
constructor(app) {
this.app = app;
this.routes = new Map();
}
loadRoutes(routesDir) {
const routeFiles = this.getRouteFiles(routesDir);
for (const file of routeFiles) {
this.loadRoute(file);
}
}
getRouteFiles(dir) {
const files = [];
const readDir = (currentDir) => {
const items = fs.readdirSync(currentDir);
for (const item of items) {
const fullPath = path.join(currentDir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
readDir(fullPath);
} else if (item.endsWith('.js') && !item.startsWith('_')) {
files.push(fullPath);
}
}
};
readDir(dir);
return files;
}
loadRoute(filePath) {
try {
const routeModule = require(filePath);
const routePath = this.getRoutePathFromFile(filePath);
if (typeof routeModule === 'function') {
// Route module exports a function that returns a router
const router = routeModule();
this.app.use(routePath, router);
} else if (routeModule.router) {
// Route module exports an object with router property
this.app.use(routePath, routeModule.router);
} else {
// Route module exports a router directly
this.app.use(routePath, routeModule);
}
this.routes.set(routePath, filePath);
console.log(`Loaded route: ${routePath} from ${filePath}`);
} catch (error) {
console.error(`Failed to load route from ${filePath}:`, error.message);
}
}
getRoutePathFromFile(filePath) {
const relativePath = path.relative(path.join(__dirname, '../routes'), filePath);
const routePath = '/' + relativePath
.replace(/\\/g, '/') // Convert Windows paths
.replace(/\.js$/, '') // Remove .js extension
.replace(/\/index$/, '') // Remove /index
.replace(/\[([^\]]+)\]/g, ':$1'); // Convert [param] to :param
return routePath === '/' ? '' : routePath;
}
reloadRoute(routePath) {
const filePath = this.routes.get(routePath);
if (filePath) {
// Clear require cache
delete require.cache[require.resolve(filePath)];
// Reload the route
this.loadRoute(filePath);
}
}
getLoadedRoutes() {
return Array.from(this.routes.keys());
}
}
module.exports = RouteLoader;路由守卫和中间件
javascript
// middleware/auth-guards.js
const jwt = require('jsonwebtoken');
const config = require('../config');
// Authentication guard
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const decoded = jwt.verify(token, config.jwt.secret);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
// Authorization guard
const authorize = (roles = []) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (roles.length && !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
// Rate limiting guard
const rateLimit = (maxRequests = 100, windowMs = 15 * 60 * 1000) => {
const requests = new Map();
return (req, res, next) => {
const key = req.ip;
const now = Date.now();
const windowStart = now - windowMs;
// Clean old entries
const userRequests = requests.get(key) || [];
const validRequests = userRequests.filter(time => time > windowStart);
if (validRequests.length >= maxRequests) {
return res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil((validRequests[0] + windowMs - now) / 1000)
});
}
validRequests.push(now);
requests.set(key, validRequests);
next();
};
};
// Validation guard
const validate = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details.map(detail => detail.message)
});
}
next();
};
};
module.exports = {
authenticate,
authorize,
rateLimit,
validate
};javascript
// routes/protected.js - Using guards
const express = require('express');
const { authenticate, authorize, rateLimit } = require('../middleware/auth-guards');
const router = express.Router();
// Apply rate limiting to all routes in this router
router.use(rateLimit(50, 15 * 60 * 1000)); // 50 requests per 15 minutes
// Public route
router.get('/public', (req, res) => {
res.json({ message: 'This is a public route' });
});
// Authenticated route
router.get('/private', authenticate, (req, res) => {
res.json({
message: 'This is a private route',
user: req.user
});
});
// Admin only route
router.get('/admin', authenticate, authorize(['admin']), (req, res) => {
res.json({
message: 'This is an admin-only route',
user: req.user
});
});
// Multiple roles
router.get('/moderator', authenticate, authorize(['admin', 'moderator']), (req, res) => {
res.json({
message: 'This route is for admins and moderators',
user: req.user
});
});
module.exports = router;下一步
在下一章中,我们将探索 Node.js 应用程序中的状态管理,包括会话管理、缓存策略和数据持久化。
实践练习
- 使用 Express 路由器创建具有完整 CRUD 操作的 RESTful API
- 实现具有向后兼容性的 API 版本控制
- 构建一个基于角色的访问控制的路由守卫系统
- 创建一个支持热重载的动态路由加载系统
关键要点
- 路由决定应用程序如何响应客户端请求
- Express.js 提供强大的路由功能,支持中间件
- 模块化路由改善代码组织和可维护性
- 路由守卫支持身份验证和授权
- API 版本控制允许向后兼容
- 动态路由加载支持灵活的应用程序架构
- 路由中的适当错误处理改善用户体验