Featured Article

全栈架构设计:从前端到后端的完整方案

探讨现代全栈应用的架构设计,包括微服务、数据库选型、API设计和部署策略。

April 22, 2024 (1y ago)
18 min read
3,240 views
267 likes
Category
Advanced
#fullstack#architecture#microservices#database#deployment

全栈架构设计:从前端到后端的完整方案

在现代Web开发中,构建一个可扩展、可维护的全栈应用需要深思熟虑的架构设计。本文将分享我在设计全栈架构时的经验和最佳实践。

架构概览

整体架构图

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Frontend      │    │   API Gateway   │    │   Microservices │
│   (Next.js)     │◄──►│   (Kong/Nginx)  │◄──►│   (Node.js)     │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                                │                        │
                       ┌─────────────────┐    ┌─────────────────┐
                       │   Load Balancer │    │    Database     │
                       │   (Cloudflare)  │    │ (PostgreSQL +   │
                       └─────────────────┘    │     Redis)      │
                                              └─────────────────┘

前端架构设计

Next.js 15 + App Router

// app/layout.tsx - 根布局
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="zh">
      <body className={inter.className}>
        <Providers>
          <Header />
          <main className="min-h-screen">
            {children}
          </main>
          <Footer />
        </Providers>
      </body>
    </html>
  )
}
 
// app/providers.tsx - 全局状态管理
'use client'
 
export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      <AuthProvider>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </AuthProvider>
    </QueryClientProvider>
  )
}

状态管理策略

// 使用Zustand进行客户端状态管理
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
 
interface UserStore {
  user: User | null
  setUser: (user: User) => void
  logout: () => void
}
 
export const useUserStore = create<UserStore>()(
  persist(
    (set) => ({
      user: null,
      setUser: (user) => set({ user }),
      logout: () => set({ user: null }),
    }),
    {
      name: 'user-storage',
    }
  )
)
 
// 服务端状态使用TanStack Query
export function useUserProfile(userId: string) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUserProfile(userId),
    staleTime: 5 * 60 * 1000, // 5分钟
  })
}

后端架构设计

微服务架构

// 用户服务 (user-service)
import express from 'express'
import { z } from 'zod'
 
const app = express()
 
const CreateUserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(2),
})
 
app.post('/users', async (req, res) => {
  try {
    const userData = CreateUserSchema.parse(req.body)
    const user = await userService.createUser(userData)
    res.status(201).json({ user })
  } catch (error) {
    if (error instanceof z.ZodError) {
      return res.status(400).json({ errors: error.errors })
    }
    res.status(500).json({ error: 'Internal server error' })
  }
})
 
// 产品服务 (product-service)
app.get('/products', async (req, res) => {
  const { page = 1, limit = 20, category } = req.query
  
  const products = await productService.getProducts({
    page: Number(page),
    limit: Number(limit),
    category: category as string,
  })
  
  res.json(products)
})

API网关设计

// api-gateway/routes.ts
import { createProxyMiddleware } from 'http-proxy-middleware'
 
export const routes = {
  '/api/users': {
    target: process.env.USER_SERVICE_URL,
    changeOrigin: true,
    pathRewrite: { '^/api/users': '/users' },
  },
  '/api/products': {
    target: process.env.PRODUCT_SERVICE_URL,
    changeOrigin: true,
    pathRewrite: { '^/api/products': '/products' },
  },
  '/api/orders': {
    target: process.env.ORDER_SERVICE_URL,
    changeOrigin: true,
    pathRewrite: { '^/api/orders': '/orders' },
  },
}
 
// 中间件配置
app.use('/api/users', createProxyMiddleware(routes['/api/users']))
app.use('/api/products', createProxyMiddleware(routes['/api/products']))
app.use('/api/orders', createProxyMiddleware(routes['/api/orders']))

数据库设计

PostgreSQL 主数据库

-- 用户表
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) UNIQUE NOT NULL,
  password_hash VARCHAR(255) NOT NULL,
  name VARCHAR(100) NOT NULL,
  avatar_url TEXT,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);
 
-- 产品表
CREATE TABLE products (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name VARCHAR(255) NOT NULL,
  description TEXT,
  price DECIMAL(10,2) NOT NULL,
  category_id UUID REFERENCES categories(id),
  stock_quantity INTEGER DEFAULT 0,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);
 
-- 索引优化
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_price ON products(price);
CREATE INDEX idx_users_email ON users(email);

Redis 缓存策略

// cache/redis-client.ts
import Redis from 'ioredis'
 
const redis = new Redis({
  host: process.env.REDIS_HOST,
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD,
})
 
export class CacheService {
  // 用户会话缓存
  async setUserSession(userId: string, sessionData: any, ttl = 3600) {
    await redis.setex(`session:${userId}`, ttl, JSON.stringify(sessionData))
  }
 
  async getUserSession(userId: string) {
    const data = await redis.get(`session:${userId}`)
    return data ? JSON.parse(data) : null
  }
 
  // 产品缓存
  async cacheProducts(key: string, products: any[], ttl = 600) {
    await redis.setex(`products:${key}`, ttl, JSON.stringify(products))
  }
 
  async getCachedProducts(key: string) {
    const data = await redis.get(`products:${key}`)
    return data ? JSON.parse(data) : null
  }
 
  // 分布式锁
  async acquireLock(key: string, ttl = 10) {
    const result = await redis.set(`lock:${key}`, '1', 'EX', ttl, 'NX')
    return result === 'OK'
  }
 
  async releaseLock(key: string) {
    await redis.del(`lock:${key}`)
  }
}

认证与授权

JWT + Refresh Token

// auth/jwt-service.ts
import jwt from 'jsonwebtoken'
 
export class JWTService {
  generateTokens(userId: string) {
    const accessToken = jwt.sign(
      { userId, type: 'access' },
      process.env.JWT_SECRET!,
      { expiresIn: '15m' }
    )
 
    const refreshToken = jwt.sign(
      { userId, type: 'refresh' },
      process.env.JWT_REFRESH_SECRET!,
      { expiresIn: '7d' }
    )
 
    return { accessToken, refreshToken }
  }
 
  verifyAccessToken(token: string) {
    try {
      return jwt.verify(token, process.env.JWT_SECRET!) as any
    } catch (error) {
      throw new Error('Invalid access token')
    }
  }
 
  verifyRefreshToken(token: string) {
    try {
      return jwt.verify(token, process.env.JWT_REFRESH_SECRET!) as any
    } catch (error) {
      throw new Error('Invalid refresh token')
    }
  }
}
 
// 中间件
export function authenticateToken(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers['authorization']
  const token = authHeader && authHeader.split(' ')[1]
 
  if (!token) {
    return res.sendStatus(401)
  }
 
  try {
    const decoded = jwtService.verifyAccessToken(token)
    req.user = decoded
    next()
  } catch (error) {
    return res.sendStatus(403)
  }
}

部署与DevOps

Docker容器化

# Dockerfile.frontend
FROM node:18-alpine AS builder
 
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
 
COPY . .
RUN npm run build
 
FROM node:18-alpine AS runner
WORKDIR /app
 
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
 
EXPOSE 3000
CMD ["node", "server.js"]
# Dockerfile.backend
FROM node:18-alpine
 
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
 
COPY . .
RUN npm run build
 
EXPOSE 8000
CMD ["npm", "start"]

Kubernetes部署

# k8s/frontend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: myapp/frontend:latest
        ports:
        - containerPort: 3000
        env:
        - name: API_URL
          value: "http://api-gateway:8080"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: frontend-service
spec:
  selector:
    app: frontend
  ports:
  - port: 80
    targetPort: 3000
  type: LoadBalancer

CI/CD Pipeline

# .github/workflows/deploy.yml
name: Deploy to Production
 
on:
  push:
    branches: [main]
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npm run test
      - run: npm run lint
 
  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build Docker images
        run: |
          docker build -f Dockerfile.frontend -t myapp/frontend:${{ github.sha }} .
          docker build -f Dockerfile.backend -t myapp/backend:${{ github.sha }} .
      
      - name: Push to registry
        run: |
          echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
          docker push myapp/frontend:${{ github.sha }}
          docker push myapp/backend:${{ github.sha }}
      
      - name: Deploy to Kubernetes
        run: |
          kubectl set image deployment/frontend frontend=myapp/frontend:${{ github.sha }}
          kubectl set image deployment/backend backend=myapp/backend:${{ github.sha }}

监控与日志

应用监控

// monitoring/metrics.ts
import prometheus from 'prom-client'
 
// 创建指标
const httpRequestDuration = new prometheus.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status'],
  buckets: [0.1, 0.5, 1, 2, 5]
})
 
const httpRequestTotal = new prometheus.Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status']
})
 
// 中间件
export function metricsMiddleware(req: Request, res: Response, next: NextFunction) {
  const start = Date.now()
  
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000
    const labels = {
      method: req.method,
      route: req.route?.path || req.path,
      status: res.statusCode.toString()
    }
    
    httpRequestDuration.observe(labels, duration)
    httpRequestTotal.inc(labels)
  })
  
  next()
}

结构化日志

// logging/logger.ts
import winston from 'winston'
 
export const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'api-gateway' },
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
    new winston.transports.Console({
      format: winston.format.simple()
    })
  ]
})
 
// 使用示例
app.use((req, res, next) => {
  logger.info('HTTP Request', {
    method: req.method,
    url: req.url,
    userAgent: req.get('User-Agent'),
    ip: req.ip
  })
  next()
})

性能优化

数据库优化

// 连接池配置
import { Pool } from 'pg'
 
const pool = new Pool({
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT || '5432'),
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  max: 20, // 最大连接数
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
})
 
// 查询优化
export class ProductRepository {
  async getProductsWithPagination(page: number, limit: number, category?: string) {
    const offset = (page - 1) * limit
    
    let query = `
      SELECT p.*, c.name as category_name 
      FROM products p 
      LEFT JOIN categories c ON p.category_id = c.id
    `
    const params: any[] = []
    
    if (category) {
      query += ' WHERE c.slug = $1'
      params.push(category)
    }
    
    query += ' ORDER BY p.created_at DESC LIMIT $' + (params.length + 1) + ' OFFSET $' + (params.length + 2)
    params.push(limit, offset)
    
    const result = await pool.query(query, params)
    return result.rows
  }
}

安全最佳实践

输入验证与清理

// validation/schemas.ts
import { z } from 'zod'
import DOMPurify from 'dompurify'
 
export const CreateProductSchema = z.object({
  name: z.string().min(1).max(255).transform(val => DOMPurify.sanitize(val)),
  description: z.string().max(2000).transform(val => DOMPurify.sanitize(val)),
  price: z.number().positive().max(999999.99),
  categoryId: z.string().uuid(),
})
 
// 中间件
export function validateRequest(schema: z.ZodSchema) {
  return (req: Request, res: Response, next: NextFunction) => {
    try {
      req.body = schema.parse(req.body)
      next()
    } catch (error) {
      if (error instanceof z.ZodError) {
        return res.status(400).json({ errors: error.errors })
      }
      next(error)
    }
  }
}

总结

构建一个成功的全栈应用需要考虑:

  1. 可扩展性:微服务架构支持独立扩展
  2. 性能:缓存策略和数据库优化
  3. 安全性:认证授权和输入验证
  4. 可维护性:清晰的代码结构和文档
  5. 可观测性:监控、日志和错误追踪

记住,架构设计是一个迭代过程,需要根据业务需求和技术发展不断调整和优化。

下一步

在后续文章中,我们将深入探讨微服务间的通信模式、事件驱动架构和分布式系统的一致性保证。敬请期待!

CleanLove

Written by CleanLove

Full-stack developer passionate about modern web technologies

Initializing application
Loading page content, please wait...