全栈架构设计:从前端到后端的完整方案
在现代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)
}
}
}
总结
构建一个成功的全栈应用需要考虑:
- 可扩展性:微服务架构支持独立扩展
- 性能:缓存策略和数据库优化
- 安全性:认证授权和输入验证
- 可维护性:清晰的代码结构和文档
- 可观测性:监控、日志和错误追踪
记住,架构设计是一个迭代过程,需要根据业务需求和技术发展不断调整和优化。
下一步
在后续文章中,我们将深入探讨微服务间的通信模式、事件驱动架构和分布式系统的一致性保证。敬请期待!