Skip to content

路由和导航

概述

路由是决定应用程序如何响应特定端点客户端请求的机制。本章介绍 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 应用程序中的状态管理,包括会话管理、缓存策略和数据持久化。

实践练习

  1. 使用 Express 路由器创建具有完整 CRUD 操作的 RESTful API
  2. 实现具有向后兼容性的 API 版本控制
  3. 构建一个基于角色的访问控制的路由守卫系统
  4. 创建一个支持热重载的动态路由加载系统

关键要点

  • 路由决定应用程序如何响应客户端请求
  • Express.js 提供强大的路由功能,支持中间件
  • 模块化路由改善代码组织和可维护性
  • 路由守卫支持身份验证和授权
  • API 版本控制允许向后兼容
  • 动态路由加载支持灵活的应用程序架构
  • 路由中的适当错误处理改善用户体验

本站内容仅供学习和研究使用。