引言
在现代软件开发领域,微服务架构已经成为构建大规模、高可用性应用的重要方式。Node.js凭借其非阻塞I/O特性和丰富的生态系统,在微服务开发中占据重要地位。本文将深入探讨如何使用Express框架和TypeScript构建企业级的Node.js微服务,并通过Docker实现容器化部署。
微服务架构概述
什么是微服务架构
微服务架构是一种将单一应用程序拆分为多个小型、独立服务的软件设计方法。每个服务都围绕特定的业务功能构建,可以独立开发、部署和扩展。这些服务通过轻量级通信机制(通常是HTTP API)进行交互。
微服务的核心优势
- 可扩展性:可以根据需求单独扩展特定服务
- 技术多样性:不同服务可以使用不同的技术栈
- 容错性:单个服务故障不会影响整个系统
- 团队协作:小团队可以独立负责特定服务
- 部署灵活性:支持持续集成和持续部署
技术选型分析
Express.js 框架
Express.js是Node.js最流行的Web应用框架,提供了简洁而灵活的API设计。它具有以下特点:
- 轻量级且无强制性约定
- 中间件机制强大
- 丰富的生态系统
- 良好的性能表现
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.json({ message: 'Hello World!' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
TypeScript 的优势
TypeScript作为JavaScript的超集,为Node.js开发带来了类型安全和更好的开发体验:
- 编译时类型检查
- 更好的IDE支持
- 代码重构更安全
- 提高代码可维护性
项目初始化与配置
项目结构设计
microservice-project/
├── src/
│ ├── controllers/
│ ├── models/
│ ├── routes/
│ ├── services/
│ ├── middleware/
│ ├── utils/
│ └── app.ts
├── tests/
├── docker-compose.yml
├── Dockerfile
├── package.json
└── tsconfig.json
初始化项目
# 创建项目目录
mkdir microservice-project
cd microservice-project
# 初始化npm项目
npm init -y
# 安装依赖
npm install express cors helmet morgan dotenv
npm install -D typescript @types/node @types/express @types/cors @types/helmet @types/morgan ts-node nodemon
TypeScript 配置文件
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}
核心服务实现
应用入口文件
// src/app.ts
import express, { Application, Request, Response, NextFunction } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import dotenv from 'dotenv';
// 导入路由
import userRoutes from './routes/user.routes';
import productRoutes from './routes/product.routes';
// 加载环境变量
dotenv.config();
const app: Application = express();
const PORT: number = parseInt(process.env.PORT || '3000', 10);
// 中间件配置
app.use(helmet()); // 安全头部
app.use(cors()); // 跨域支持
app.use(morgan('combined')); // 日志记录
app.use(express.json()); // JSON解析
app.use(express.urlencoded({ extended: true })); // URL编码解析
// 健康检查端点
app.get('/health', (req: Request, res: Response) => {
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString(),
service: 'microservice-project'
});
});
// 路由注册
app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);
// 全局错误处理中间件
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({
error: 'Something went wrong!',
message: err.message
});
});
// 404处理
app.use('*', (req: Request, res: Response) => {
res.status(404).json({
error: 'Not Found',
message: 'The requested resource was not found'
});
});
export default app;
用户服务实现
// src/models/user.model.ts
export interface User {
id: string;
name: string;
email: string;
createdAt: Date;
updatedAt: Date;
}
export class UserModel {
private users: User[] = [
{
id: '1',
name: 'John Doe',
email: 'john@example.com',
createdAt: new Date(),
updatedAt: new Date()
}
];
async findById(id: string): Promise<User | null> {
return this.users.find(user => user.id === id) || null;
}
async findAll(): Promise<User[]> {
return this.users;
}
async create(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
const newUser: User = {
id: Math.random().toString(36).substr(2, 9),
...userData,
createdAt: new Date(),
updatedAt: new Date()
};
this.users.push(newUser);
return newUser;
}
async update(id: string, userData: Partial<User>): Promise<User | null> {
const index = this.users.findIndex(user => user.id === id);
if (index === -1) return null;
this.users[index] = {
...this.users[index],
...userData,
updatedAt: new Date()
};
return this.users[index];
}
async delete(id: string): Promise<boolean> {
const initialLength = this.users.length;
this.users = this.users.filter(user => user.id !== id);
return this.users.length < initialLength;
}
}
// src/controllers/user.controller.ts
import { Request, Response } from 'express';
import { UserModel, User } from '../models/user.model';
class UserController {
private userService: UserModel;
constructor() {
this.userService = new UserModel();
}
async getAllUsers(req: Request, res: Response): Promise<void> {
try {
const users = await this.userService.findAll();
res.status(200).json({
success: true,
data: users,
count: users.length
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch users',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
async getUserById(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params;
const user = await this.userService.findById(id);
if (!user) {
res.status(404).json({
success: false,
error: 'User not found'
});
return;
}
res.status(200).json({
success: true,
data: user
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch user',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
async createUser(req: Request, res: Response): Promise<void> {
try {
const { name, email } = req.body;
// 验证输入
if (!name || !email) {
res.status(400).json({
success: false,
error: 'Name and email are required'
});
return;
}
const user = await this.userService.create({ name, email });
res.status(201).json({
success: true,
data: user
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to create user',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
async updateUser(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params;
const { name, email } = req.body;
const updatedUser = await this.userService.update(id, { name, email });
if (!updatedUser) {
res.status(404).json({
success: false,
error: 'User not found'
});
return;
}
res.status(200).json({
success: true,
data: updatedUser
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to update user',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
async deleteUser(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params;
const deleted = await this.userService.delete(id);
if (!deleted) {
res.status(404).json({
success: false,
error: 'User not found'
});
return;
}
res.status(200).json({
success: true,
message: 'User deleted successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to delete user',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
}
export default new UserController();
// src/routes/user.routes.ts
import express, { Router } from 'express';
import userController from '../controllers/user.controller';
const router: Router = express.Router();
router.get('/', userController.getAllUsers);
router.get('/:id', userController.getUserById);
router.post('/', userController.createUser);
router.put('/:id', userController.updateUser);
router.delete('/:id', userController.deleteUser);
export default router;
产品服务实现
// src/models/product.model.ts
export interface Product {
id: string;
name: string;
description: string;
price: number;
category: string;
stock: number;
createdAt: Date;
updatedAt: Date;
}
export class ProductModel {
private products: Product[] = [
{
id: '1',
name: 'Laptop',
description: 'High-performance laptop',
price: 999.99,
category: 'Electronics',
stock: 50,
createdAt: new Date(),
updatedAt: new Date()
}
];
async findById(id: string): Promise<Product | null> {
return this.products.find(product => product.id === id) || null;
}
async findAll(): Promise<Product[]> {
return this.products;
}
async findByCategory(category: string): Promise<Product[]> {
return this.products.filter(product => product.category === category);
}
async create(productData: Omit<Product, 'id' | 'createdAt' | 'updatedAt'>): Promise<Product> {
const newProduct: Product = {
id: Math.random().toString(36).substr(2, 9),
...productData,
createdAt: new Date(),
updatedAt: new Date()
};
this.products.push(newProduct);
return newProduct;
}
async update(id: string, productData: Partial<Product>): Promise<Product | null> {
const index = this.products.findIndex(product => product.id === id);
if (index === -1) return null;
this.products[index] = {
...this.products[index],
...productData,
updatedAt: new Date()
};
return this.products[index];
}
async delete(id: string): Promise<boolean> {
const initialLength = this.products.length;
this.products = this.products.filter(product => product.id !== id);
return this.products.length < initialLength;
}
}
// src/controllers/product.controller.ts
import { Request, Response } from 'express';
import { ProductModel, Product } from '../models/product.model';
class ProductController {
private productService: ProductModel;
constructor() {
this.productService = new ProductModel();
}
async getAllProducts(req: Request, res: Response): Promise<void> {
try {
const products = await this.productService.findAll();
res.status(200).json({
success: true,
data: products,
count: products.length
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch products',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
async getProductsByCategory(req: Request, res: Response): Promise<void> {
try {
const { category } = req.params;
const products = await this.productService.findByCategory(category);
res.status(200).json({
success: true,
data: products,
count: products.length
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch products by category',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
async getProductById(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params;
const product = await this.productService.findById(id);
if (!product) {
res.status(404).json({
success: false,
error: 'Product not found'
});
return;
}
res.status(200).json({
success: true,
data: product
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch product',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
async createProduct(req: Request, res: Response): Promise<void> {
try {
const { name, description, price, category, stock } = req.body;
// 验证输入
if (!name || !price || !category) {
res.status(400).json({
success: false,
error: 'Name, price and category are required'
});
return;
}
const product = await this.productService.create({
name,
description,
price,
category,
stock
});
res.status(201).json({
success: true,
data: product
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to create product',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
async updateProduct(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params;
const { name, description, price, category, stock } = req.body;
const updatedProduct = await this.productService.update(id, {
name,
description,
price,
category,
stock
});
if (!updatedProduct) {
res.status(404).json({
success: false,
error: 'Product not found'
});
return;
}
res.status(200).json({
success: true,
data: updatedProduct
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to update product',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
async deleteProduct(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params;
const deleted = await this.productService.delete(id);
if (!deleted) {
res.status(404).json({
success: false,
error: 'Product not found'
});
return;
}
res.status(200).json({
success: true,
message: 'Product deleted successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to delete product',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
}
export default new ProductController();
// src/routes/product.routes.ts
import express, { Router } from 'express';
import productController from '../controllers/product.controller';
const router: Router = express.Router();
router.get('/', productController.getAllProducts);
router.get('/category/:category', productController.getProductsByCategory);
router.get('/:id', productController.getProductById);
router.post('/', productController.createProduct);
router.put('/:id', productController.updateProduct);
router.delete('/:id', productController.deleteProduct);
export default router;
中间件与工具类
验证中间件
// src/middleware/validation.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { validationResult } from 'express-validator';
export const validate = (req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: 'Validation failed',
details: errors.array()
});
}
next();
};
export const validateUser = [
(req: Request, res: Response, next: NextFunction) => {
if (!req.body.name || !req.body.email) {
return res.status(400).json({
success: false,
error: 'Name and email are required'
});
}
next();
}
];
export const validateProduct = [
(req: Request, res: Response, next: NextFunction) => {
if (!req.body.name || !req.body.price || !req.body.category) {
return res.status(400).json({
success: false,
error: 'Name, price and category are required'
});
}
next();
}
];
日志中间件
// src/middleware/logger.middleware.ts
import { Request, Response, NextFunction } from 'express';
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
export const requestLogger = (req: Request, res: Response, next: NextFunction) => {
logger.info(`${req.method} ${req.url}`, {
method: req.method,
url: req.url,
headers: req.headers,
body: req.body,
ip: req.ip
});
next();
};
export const errorLogger = (error: Error, req: Request, res: Response, next: NextFunction) => {
logger.error(error.message, {
stack: error.stack,
method: req.method,
url: req.url,
ip: req.ip
});
next(error);
};
Docker容器化部署
Dockerfile 配置
# Dockerfile
FROM node:18-alpine
# 设置工作目录
WORKDIR /app
# 复制package文件
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production
# 复制源代码
COPY . .
# 构建项目
RUN npm run build
# 暴露端口
EXPOSE 3000
# 创建非root用户
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
USER nextjs
# 启动应用
CMD ["npm", "start"]
Docker Compose 配置
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
volumes:
- ./logs:/app/logs
networks:
- microservice-network
# 可选:添加数据库服务
database:
image: postgres:15-alpine
environment:
POSTGRES_DB: microservice_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- microservice-network
volumes:
postgres_data:
networks:
microservice-network:
driver: bridge
环境配置文件
# .env
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/microservice_db
JWT_SECRET=my_jwt_secret_key
LOG_LEVEL=info
服务间通信
HTTP客户端实现
// src/utils/http.client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
export class HttpClient {
private client: AxiosInstance;
constructor(baseURL: string, timeout: number = 5000) {
this.client = axios.create({
baseURL,
timeout,
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器
this.client.interceptors.request.use(
(config) => {
// 添加认证头等
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
this.client.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
return Promise.reject(error);
}
);
}
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.get<T>(url, config);
return response;
}
async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.post<T>(url, data, config);
return response;
}
async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.put<T>(url, data, config);
return response;
}
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.delete<T>(url, config);
return response;
}
}
export const httpClient = new HttpClient('http://localhost:3000');
微服务间调用示例
// src/services/external.service.ts
import { HttpClient } from '../utils/http.client';
export class ExternalService {
private client: HttpClient;
constructor() {
this.client = new HttpClient('http://user-service:3000');
}
async getUserById(id: string) {
try {
const user = await this.client.get<any>(`/api/users/${id}`);
return user;
} catch (error) {
console.error('Failed to fetch user:', error);
throw new Error('User service unavailable');
}
}
}
测试策略
单元测试配置
// tests/user.controller.test.ts
import request from 'supertest';
import app from '../src/app';
describe('User Controller', () => {
describe('GET /api/users', () => {
it('should return all users', async () => {
const response = await request(app)
.get('/api/users')
.expect(200)
.expect('Content-Type', /json/);
expect(response.body).toHaveProperty('success', true);
expect(response.body).toHaveProperty('data');
});
});
describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
name: 'Jane Doe',
email: 'jane@example.com'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201)
.expect('Content-Type', /json/);
expect(response.body).toHaveProperty('success', true);
expect(response.body.data).toHaveProperty('name', userData.name);
expect(response.body.data).toHaveProperty('email', userData.email);
});
});
});
集成测试
// tests/integration.test.ts
import { describe, it, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import app from '../src/app';
describe('Integration Tests', () => {
let server: any;
beforeAll(() => {
server = app.listen(0);
});
afterAll((done) => {
server.close(done);
});
it('should handle health check endpoint', async () => {
const response = await request(server)
.get('/health')
.expect(200);
expect(response.body).toHaveProperty('status', 'OK');
});
});
性能优化与监控
缓存实现
// src/utils/cache.util.ts
import NodeCache from 'node-cache';
export class CacheUtil {
private static instance: NodeCache;
private static readonly DEFAULT_TTL = 3600; // 1小时
static getInstance(): NodeCache {
if (!this.instance) {
this.instance = new NodeCache({
stdTTL: this.DEFAULT_TTL,
checkperiod: 120
});
}
return this.instance
评论 (0)