Node.js微服务架构设计:基于Express与TypeScript的实战教程

Sam90
Sam90 2026-02-28T23:03:09+08:00
0 0 0

引言

在现代软件开发中,微服务架构已成为构建大型分布式系统的重要模式。Node.js凭借其非阻塞I/O特性和丰富的生态系统,成为实现微服务架构的理想选择。本文将深入探讨如何使用Express框架和TypeScript构建可扩展、可维护的微服务应用,涵盖从基础概念到实际实现的完整流程。

微服务架构概述

什么是微服务架构

微服务架构是一种将单一应用程序拆分为多个小型、独立服务的软件架构模式。每个服务:

  • 运行在自己的进程中
  • 通过轻量级通信机制(通常是HTTP API)进行通信
  • 专注于特定的业务功能
  • 可以独立部署、扩展和维护

微服务的优势与挑战

优势:

  • 技术栈灵活性:不同服务可以使用不同的技术栈
  • 独立部署:服务可以独立开发、测试和部署
  • 可扩展性:可以根据需求单独扩展特定服务
  • 团队自治:不同团队可以负责不同服务

挑战:

  • 分布式复杂性:需要处理网络通信、容错等复杂问题
  • 数据一致性:跨服务的数据同步和一致性保证
  • 运维复杂度:需要更多的监控、日志和调试工具
  • 网络延迟:服务间通信可能带来性能开销

Node.js微服务架构技术选型

Express框架选择

Express.js是Node.js最流行的Web应用框架,具有以下优势:

  • 简洁的API设计
  • 中间件支持丰富
  • 社区活跃,文档完善
  • 与TypeScript兼容性好

TypeScript的引入价值

TypeScript为JavaScript添加了静态类型检查,为微服务开发带来:

  • 编译时错误检查
  • 更好的代码提示和重构支持
  • 提高代码可维护性
  • 便于团队协作开发

项目初始化与基础结构搭建

项目结构设计

microservice-project/
├── package.json
├── tsconfig.json
├── README.md
├── services/
│   ├── user-service/
│   │   ├── src/
│   │   │   ├── controllers/
│   │   │   ├── models/
│   │   │   ├── routes/
│   │   │   ├── middleware/
│   │   │   ├── services/
│   │   │   └── app.ts
│   │   └── package.json
│   └── order-service/
│       ├── src/
│       │   ├── controllers/
│       │   ├── models/
│       │   ├── routes/
│       │   ├── middleware/
│       │   ├── services/
│       │   └── app.ts
│       └── package.json
├── shared/
│   ├── types/
│   ├── utils/
│   └── config/
└── docker-compose.yml

TypeScript配置文件

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "types": ["node", "express"],
    "typeRoots": ["./node_modules/@types"],
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "resolveJsonModule": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "removeComments": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noPropertyAccessFromIndexSignature": true,
    "strictBindCallApply": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

服务基础配置

// shared/config/index.ts
export interface ServiceConfig {
  port: number;
  host: string;
  name: string;
  database: {
    host: string;
    port: number;
    name: string;
    username: string;
    password: string;
  };
  redis: {
    host: string;
    port: number;
    password?: string;
  };
  jwt: {
    secret: string;
    expiresIn: string;
  };
}

export const config: ServiceConfig = {
  port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
  host: process.env.HOST || 'localhost',
  name: process.env.SERVICE_NAME || 'default-service',
  database: {
    host: process.env.DB_HOST || 'localhost',
    port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 5432,
    name: process.env.DB_NAME || 'myapp',
    username: process.env.DB_USER || 'postgres',
    password: process.env.DB_PASSWORD || 'password'
  },
  redis: {
    host: process.env.REDIS_HOST || 'localhost',
    port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : 6379,
    password: process.env.REDIS_PASSWORD
  },
  jwt: {
    secret: process.env.JWT_SECRET || 'your-secret-key',
    expiresIn: process.env.JWT_EXPIRES_IN || '24h'
  }
};

用户服务实现

数据模型定义

// services/user-service/src/models/user.model.ts
import { Document, Schema, model } from 'mongoose';

export interface User extends Document {
  username: string;
  email: string;
  password: string;
  firstName: string;
  lastName: string;
  createdAt: Date;
  updatedAt: Date;
}

export const UserSchema = new Schema<User>({
  username: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    minlength: 3,
    maxlength: 30
  },
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true,
    match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Please enter a valid email']
  },
  password: {
    type: String,
    required: true,
    minlength: 6
  },
  firstName: {
    type: String,
    required: true,
    trim: true,
    maxlength: 50
  },
  lastName: {
    type: String,
    required: true,
    trim: true,
    maxlength: 50
  }
}, {
  timestamps: true
});

export const UserModel = model<User>('User', UserSchema);

控制器实现

// services/user-service/src/controllers/user.controller.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/user.service';
import { User } from '../models/user.model';

export class UserController {
  private userService: UserService;

  constructor() {
    this.userService = new UserService();
  }

  async createUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const userData: Partial<User> = req.body;
      const user = await this.userService.createUser(userData);
      
      res.status(201).json({
        success: true,
        data: user,
        message: 'User created successfully'
      });
    } catch (error) {
      next(error);
    }
  }

  async getUserById(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const { id } = req.params;
      const user = await this.userService.getUserById(id);
      
      if (!user) {
        res.status(404).json({
          success: false,
          message: 'User not found'
        });
        return;
      }
      
      res.json({
        success: true,
        data: user
      });
    } catch (error) {
      next(error);
    }
  }

  async updateUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const { id } = req.params;
      const userData: Partial<User> = req.body;
      const user = await this.userService.updateUser(id, userData);
      
      res.json({
        success: true,
        data: user,
        message: 'User updated successfully'
      });
    } catch (error) {
      next(error);
    }
  }

  async deleteUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const { id } = req.params;
      await this.userService.deleteUser(id);
      
      res.json({
        success: true,
        message: 'User deleted successfully'
      });
    } catch (error) {
      next(error);
    }
  }

  async getAllUsers(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const { page = 1, limit = 10 } = req.query;
      const users = await this.userService.getAllUsers(
        parseInt(page as string),
        parseInt(limit as string)
      );
      
      res.json({
        success: true,
        data: users,
        pagination: {
          page: parseInt(page as string),
          limit: parseInt(limit as string),
          total: users.length
        }
      });
    } catch (error) {
      next(error);
    }
  }
}

服务层实现

// services/user-service/src/services/user.service.ts
import { User, UserModel } from '../models/user.model';
import bcrypt from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';

export class UserService {
  async createUser(userData: Partial<User>): Promise<User> {
    try {
      // 检查用户是否已存在
      const existingUser = await UserModel.findOne({
        $or: [{ email: userData.email }, { username: userData.username }]
      });
      
      if (existingUser) {
        throw new Error('User already exists');
      }
      
      // 密码加密
      if (userData.password) {
        const saltRounds = 10;
        userData.password = await bcrypt.hash(userData.password, saltRounds);
      }
      
      // 创建用户
      const user = new UserModel({
        ...userData,
        _id: uuidv4()
      });
      
      return await user.save();
    } catch (error) {
      throw new Error(`Failed to create user: ${error.message}`);
    }
  }

  async getUserById(id: string): Promise<User | null> {
    try {
      return await UserModel.findById(id);
    } catch (error) {
      throw new Error(`Failed to get user: ${error.message}`);
    }
  }

  async updateUser(id: string, userData: Partial<User>): Promise<User | null> {
    try {
      // 如果更新密码,需要加密
      if (userData.password) {
        const saltRounds = 10;
        userData.password = await bcrypt.hash(userData.password, saltRounds);
      }
      
      return await UserModel.findByIdAndUpdate(
        id,
        { ...userData },
        { new: true, runValidators: true }
      );
    } catch (error) {
      throw new Error(`Failed to update user: ${error.message}`);
    }
  }

  async deleteUser(id: string): Promise<void> {
    try {
      await UserModel.findByIdAndDelete(id);
    } catch (error) {
      throw new Error(`Failed to delete user: ${error.message}`);
    }
  }

  async getAllUsers(page: number, limit: number): Promise<User[]> {
    try {
      const skip = (page - 1) * limit;
      return await UserModel.find()
        .skip(skip)
        .limit(limit)
        .sort({ createdAt: -1 });
    } catch (error) {
      throw new Error(`Failed to get users: ${error.message}`);
    }
  }

  async findByEmail(email: string): Promise<User | null> {
    try {
      return await UserModel.findOne({ email });
    } catch (error) {
      throw new Error(`Failed to find user by email: ${error.message}`);
    }
  }
}

路由配置

// services/user-service/src/routes/user.routes.ts
import { Router } from 'express';
import { UserController } from '../controllers/user.controller';

const router = Router();
const userController = new UserController();

// 定义路由
router.post('/', userController.createUser.bind(userController));
router.get('/:id', userController.getUserById.bind(userController));
router.put('/:id', userController.updateUser.bind(userController));
router.delete('/:id', userController.deleteUser.bind(userController));
router.get('/', userController.getAllUsers.bind(userController));

export default router;

应用启动文件

// services/user-service/src/app.ts
import express, { Application, Request, Response, NextFunction } from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import { config } from '../../shared/config';
import userRoutes from './routes/user.routes';
import { errorHandler } from './middleware/error.middleware';

class App {
  public app: Application;

  constructor() {
    this.app = express();
    this.initializeMiddleware();
    this.initializeRoutes();
    this.initializeDatabase();
    this.initializeErrorHandling();
  }

  private initializeMiddleware(): void {
    this.app.use(helmet());
    this.app.use(cors());
    this.app.use(morgan('combined'));
    this.app.use(express.json());
    this.app.use(express.urlencoded({ extended: true }));
  }

  private initializeRoutes(): void {
    this.app.use('/api/users', userRoutes);
    
    // 健康检查端点
    this.app.get('/health', (req: Request, res: Response) => {
      res.json({
        status: 'OK',
        timestamp: new Date().toISOString(),
        service: config.name
      });
    });
  }

  private async initializeDatabase(): Promise<void> {
    try {
      await mongoose.connect(
        `mongodb://${config.database.username}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.name}`,
        {
          useNewUrlParser: true,
          useUnifiedTopology: true,
          useCreateIndex: true,
          useFindAndModify: false
        }
      );
      
      console.log('Connected to MongoDB');
    } catch (error) {
      console.error('MongoDB connection error:', error);
      process.exit(1);
    }
  }

  private initializeErrorHandling(): void {
    this.app.use(errorHandler);
    
    // 404处理
    this.app.use((req: Request, res: Response, next: NextFunction) => {
      res.status(404).json({
        success: false,
        message: 'Route not found'
      });
    });
  }

  public listen(): void {
    const port = config.port;
    this.app.listen(port, () => {
      console.log(`User service listening at http://localhost:${port}`);
    });
  }
}

export default App;

API网关设计

API网关核心功能

API网关作为微服务架构的入口点,承担以下职责:

  • 路由请求到相应服务
  • 身份验证和授权
  • 请求/响应转换
  • 限流和熔断
  • 日志记录和监控

基于Express的API网关实现

// api-gateway/src/app.ts
import express, { Application, Request, Response, NextFunction } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { config } from '../shared/config';

class GatewayApp {
  public app: Application;

  constructor() {
    this.app = express();
    this.initializeMiddleware();
    this.initializeRoutes();
  }

  private initializeMiddleware(): void {
    this.app.use(helmet());
    this.app.use(cors());
    this.app.use(morgan('combined'));
    this.app.use(express.json());
    this.app.use(express.urlencoded({ extended: true }));
  }

  private initializeRoutes(): void {
    // 用户服务代理
    this.app.use('/api/users', 
      createProxyMiddleware({
        target: 'http://user-service:3000',
        changeOrigin: true,
        pathRewrite: {
          '^/api/users': '/api/users'
        }
      })
    );

    // 订单服务代理
    this.app.use('/api/orders',
      createProxyMiddleware({
        target: 'http://order-service:3001',
        changeOrigin: true,
        pathRewrite: {
          '^/api/orders': '/api/orders'
        }
      })
    );

    // 健康检查端点
    this.app.get('/health', (req: Request, res: Response) => {
      res.json({
        status: 'OK',
        timestamp: new Date().toISOString(),
        service: 'api-gateway'
      });
    });
  }

  public listen(): void {
    const port = config.port;
    this.app.listen(port, () => {
      console.log(`API Gateway listening at http://localhost:${port}`);
    });
  }
}

export default GatewayApp;

服务间通信机制

HTTP通信实现

// shared/utils/http.client.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';

export class HttpClient {
  private client: AxiosInstance;

  constructor(baseURL: string, timeout: number = 10000) {
    this.client = axios.create({
      baseURL,
      timeout,
      headers: {
        'Content-Type': 'application/json'
      }
    });

    // 请求拦截器
    this.client.interceptors.request.use(
      (config: AxiosRequestConfig) => {
        // 添加认证头等
        const token = process.env.AUTH_TOKEN;
        if (token) {
          config.headers = {
            ...config.headers,
            'Authorization': `Bearer ${token}`
          };
        }
        return config;
      },
      (error) => {
        return Promise.reject(error);
      }
    );

    // 响应拦截器
    this.client.interceptors.response.use(
      (response: AxiosResponse) => {
        return response.data;
      },
      (error) => {
        return Promise.reject(error);
      }
    );
  }

  async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.client.get<T>(url, config);
  }

  async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.post<T>(url, data, config);
  }

  async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.put<T>(url, data, config);
  }

  async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.client.delete<T>(url, config);
  }
}

服务发现实现

// shared/services/discovery.service.ts
import { HttpClient } from '../utils/http.client';

export interface ServiceInstance {
  id: string;
  name: string;
  host: string;
  port: number;
  status: 'UP' | 'DOWN';
  lastHeartbeat: Date;
}

export class ServiceDiscovery {
  private httpClient: HttpClient;
  private serviceRegistry: Map<string, ServiceInstance[]> = new Map();
  private discoveryInterval: NodeJS.Timeout | null = null;

  constructor(discoveryUrl: string) {
    this.httpClient = new HttpClient(discoveryUrl);
  }

  async discoverServices(): Promise<void> {
    try {
      const services = await this.httpClient.get<ServiceInstance[]>('/services');
      services.forEach(service => {
        if (!this.serviceRegistry.has(service.name)) {
          this.serviceRegistry.set(service.name, []);
        }
        this.serviceRegistry.get(service.name)!.push(service);
      });
    } catch (error) {
      console.error('Service discovery failed:', error);
    }
  }

  getService(name: string): ServiceInstance | null {
    const instances = this.serviceRegistry.get(name);
    if (!instances || instances.length === 0) {
      return null;
    }
    
    // 简单的负载均衡策略:轮询
    const instance = instances[0];
    instances.push(instance);
    instances.shift();
    
    return instance;
  }

  startDiscovery(interval: number = 30000): void {
    this.discoveryInterval = setInterval(() => {
      this.discoverServices().catch(console.error);
    }, interval);
  }

  stopDiscovery(): void {
    if (this.discoveryInterval) {
      clearInterval(this.discoveryInterval);
    }
  }
}

中间件与安全机制

认证中间件

// services/user-service/src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { config } from '../../../shared/config';

export interface AuthenticatedRequest extends Request {
  user?: {
    id: string;
    email: string;
    role: string;
  };
}

export const authenticate = (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
  try {
    const token = req.header('Authorization')?.replace('Bearer ', '');
    
    if (!token) {
      throw new Error('Access denied. No token provided.');
    }
    
    const decoded = jwt.verify(token, config.jwt.secret) as { id: string; email: string; role: string };
    req.user = decoded;
    next();
  } catch (error) {
    res.status(401).json({
      success: false,
      message: 'Invalid token'
    });
  }
};

export const authorize = (...roles: string[]) => {
  return (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
    if (!req.user) {
      return res.status(401).json({
        success: false,
        message: 'Authentication required'
      });
    }
    
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        success: false,
        message: 'Insufficient permissions'
      });
    }
    
    next();
  };
};

请求验证中间件

// services/user-service/src/middleware/validation.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { body, validationResult, ValidationChain } from 'express-validator';

export const validate = (req: Request, res: Response, next: NextFunction): void => {
  const errors = validationResult(req);
  
  if (!errors.isEmpty()) {
    return res.status(400).json({
      success: false,
      message: 'Validation failed',
      errors: errors.array()
    });
  }
  
  next();
};

export const userValidationRules = (): ValidationChain[] => {
  return [
    body('username')
      .isLength({ min: 3, max: 30 })
      .withMessage('Username must be between 3 and 30 characters')
      .matches(/^[a-zA-Z0-9_]+$/)
      .withMessage('Username can only contain letters, numbers, and underscores'),
    
    body('email')
      .isEmail()
      .normalizeEmail()
      .withMessage('Please provide a valid email address'),
    
    body('password')
      .isLength({ min: 6 })
      .withMessage('Password must be at least 6 characters long')
      .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
      .withMessage('Password must contain at least one uppercase letter, one lowercase letter, and one number'),
    
    body('firstName')
      .isLength({ min: 1, max: 50 })
      .withMessage('First name must be between 1 and 50 characters'),
    
    body('lastName')
      .isLength({ min: 1, max: 50 })
      .withMessage('Last name must be between 1 and 50 characters')
  ];
};

监控与日志系统

日志配置

// shared/utils/logger.ts
import winston from 'winston';
import { config } from '../config';

const { combine, timestamp, printf } = winston.format;

const logFormat = printf(({ level, message, timestamp, ...meta }) => {
  return `${timestamp} [${level}] ${message} ${
    Object.keys(meta).length ? JSON.stringify(meta) : ''
  }`;
});

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: combine(
    timestamp(),
    logFormat
  ),
  defaultMeta: { service: config.name },
  transports: [
    new winston.transports.Console({
      format: combine(
        timestamp(),
        logFormat
      )
    }),
    new winston.transports.File({
      filename: 'logs/error.log',
      level: 'error',
      format: combine(
        timestamp(),
        logFormat
      )
    }),
    new winston.transports.File({
      filename: 'logs/combined.log',
      format: combine(
        timestamp(),
        logFormat
      )
    })
  ]
});

export default logger;

性能监控中间件

// services/user-service/src/middleware/monitoring.middleware.ts
import { Request, Response, NextFunction } from 'express';
import logger from '../../shared/utils/logger';

export const performanceMonitor = (req: Request, res: Response, next: NextFunction): void => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    const logData = {
      method: req.method,
      url: req.url,
      statusCode: res.statusCode,
      duration: `${duration}ms`,
      userAgent: req.get('User-Agent'),
      ip: req.ip
    };
    
    if (duration > 1000) {
      logger.warn('Slow request detected', logData);
    } else {
      logger.info('Request completed', logData);
    }
  });
  
  next();
};

export const requestCounter = (req: Request, res: Response, next: NextFunction): void => {
  // 这里可以集成Prometheus等监控系统
  // 例如:prometheusClient.inc({ method: req.method, path: req.path });
  next();
};

部署与容器化

Dockerfile配置

# services/user-service/Dockerfile
FROM node:16-alpine

# 设置工作目录
WORKDIR /app

# 复制package文件
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 复制源代码
COPY . .

# 构建应用
RUN npm run build

# 暴露端口
EXPOSE 3000

# 启动命令
CMD ["npm", "start"]

Docker Compose配置

# docker-compose.yml
version: '3.8'

services:
  mongodb:
    image: mongo:5.0
    container_name: mongodb
    ports:
      - "27017:27017"
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000