最佳实践
本文档提供了使用 Vafast 框架开发应用的最佳实践和设计模式。遵循这些建议将帮助您构建更高质量、更易维护的应用程序。
项目结构
推荐的目录结构
src/
├── routes/ # 路由定义
│ ├── index.ts # 主路由文件
│ ├── user.ts # 用户相关路由
│ ├── admin.ts # 管理相关路由
│ └── api.ts # API 路由
├── middleware/ # 中间件
│ ├── auth.ts # 身份验证中间件
│ ├── cors.ts # CORS 中间件
│ ├── validation.ts # 验证中间件
│ └── index.ts # 中间件导出
├── services/ # 业务逻辑服务
│ ├── userService.ts
│ ├── authService.ts
│ └── emailService.ts
├── models/ # 数据模型
│ ├── User.ts
│ ├── Post.ts
│ └── Comment.ts
├── utils/ # 工具函数
│ ├── validation.ts
│ ├── encryption.ts
│ └── helpers.ts
├── config/ # 配置文件
│ ├── database.ts
│ ├── redis.ts
│ └── app.ts
└── index.ts # 应用入口路由组织
typescript
// routes/index.ts
import { userRoutes } from './user'
import { adminRoutes } from './admin'
import { apiRoutes } from './api'
export const routes: any[] = [
{
path: '/',
component: () => import('../components/pages/Home.vue')
},
...userRoutes,
...adminRoutes,
...apiRoutes
]typescript
// routes/user.ts
export const userRoutes: any[] = [
{
path: '/user',
middleware: [authMiddleware],
children: [
{
method: 'GET',
path: '/profile',
handler: userController.getProfile
},
{
method: 'PUT',
path: '/profile',
handler: userController.updateProfile
}
]
}
]代码组织
控制器模式
将路由处理逻辑分离到控制器中:
typescript
// controllers/userController.ts
export class UserController {
async getProfile(req: Request, params?: Record<string, string>) {
try {
const userId = (req as any).user.id
const user = await userService.findById(userId)
return new Response(JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' }
})
} catch (error) {
return new Response('Failed to get profile', { status: 500 })
}
}
async updateProfile(req: Request, params?: Record<string, string>) {
try {
const userId = (req as any).user.id
const body = await req.json()
// 验证数据
const validatedData = await validateUserUpdate(body)
const updatedUser = await userService.update(userId, validatedData)
return new Response(JSON.stringify(updatedUser), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
} catch (error) {
if (error instanceof ValidationError) {
return new Response(JSON.stringify({ error: error.message }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
})
}
return new Response('Failed to update profile', { status: 500 })
}
}
}
export const userController = new UserController()服务层模式
将业务逻辑封装在服务中:
typescript
// services/userService.ts
export class UserService {
async findById(id: string) {
// 数据库查询逻辑
const user = await db.user.findUnique({ where: { id } })
if (!user) {
throw new Error('User not found')
}
return user
}
async update(id: string, data: Partial<User>) {
// 数据更新逻辑
const updatedUser = await db.user.update({
where: { id },
data
})
return updatedUser
}
async delete(id: string) {
// 数据删除逻辑
await db.user.delete({ where: { id } })
}
}
export const userService = new UserService()中间件组织
将中间件按功能分组:
typescript
// middleware/index.ts
export { authMiddleware } from './auth'
export { corsMiddleware } from './cors'
export { validationMiddleware } from './validation'
export { rateLimitMiddleware } from './rateLimit'
export { logMiddleware } from './log'
// 组合中间件
export const commonMiddleware = [
logMiddleware,
corsMiddleware,
rateLimitMiddleware
]
export const protectedMiddleware = [
...commonMiddleware,
authMiddleware
]错误处理
统一错误处理
typescript
// utils/errors.ts
export class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public code?: string
) {
super(message)
this.name = 'AppError'
}
}
export class ValidationError extends AppError {
constructor(message: string) {
super(message, 400, 'VALIDATION_ERROR')
}
}
export class AuthenticationError extends AppError {
constructor(message: string = 'Authentication required') {
super(message, 401, 'AUTHENTICATION_ERROR')
}
}
export class AuthorizationError extends AppError {
constructor(message: string = 'Insufficient permissions') {
super(message, 403, 'AUTHORIZATION_ERROR')
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, 404, 'NOT_FOUND')
}
}错误处理中间件
typescript
// middleware/errorHandler.ts
import { AppError } from '../utils/errors'
export const errorHandler = async (req: Request, next: () => Promise<Response>) => {
try {
return await next()
} catch (error) {
console.error('Error:', error)
if (error instanceof AppError) {
return new Response(JSON.stringify({
error: error.message,
code: error.code,
statusCode: error.statusCode
}), {
status: error.statusCode,
headers: { 'Content-Type': 'application/json' }
})
}
// 未知错误
return new Response(JSON.stringify({
error: 'Internal server error',
code: 'INTERNAL_ERROR',
statusCode: 500
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
})
}
}数据验证
使用 Zod 进行验证
typescript
// utils/validation.ts
import { z } from 'zod'
export const userCreateSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
age: z.number().min(18).max(120).optional(),
password: z.string().min(8)
})
export const userUpdateSchema = userCreateSchema.partial().omit({ password: true })
export type UserCreate = z.infer<typeof userCreateSchema>
export type UserUpdate = z.infer<typeof userUpdateSchema>验证中间件
typescript
// middleware/validation.ts
import { z } from 'zod'
export const validateBody = (schema: z.ZodSchema) => {
return async (req: Request, next: () => Promise<Response>) => {
try {
const body = await req.json()
const validatedData = schema.parse(body)
// 将验证后的数据添加到请求中
;(req as any).validatedBody = validatedData
return next()
} catch (error) {
if (error instanceof z.ZodError) {
return new Response(JSON.stringify({
error: 'Validation failed',
details: error.errors
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
})
}
return new Response('Invalid JSON', { status: 400 })
}
}
}
export const validateQuery = (schema: z.ZodSchema) => {
return async (req: Request, next: () => Promise<Response>) => {
try {
const url = new URL(req.url)
const queryParams = Object.fromEntries(url.searchParams.entries())
const validatedData = schema.parse(queryParams)
;(req as any).validatedQuery = validatedData
return next()
} catch (error) {
if (error instanceof z.ZodError) {
return new Response(JSON.stringify({
error: 'Query validation failed',
details: error.errors
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
})
}
return next()
}
}
}安全性最佳实践
身份验证
typescript
// middleware/auth.ts
import jwt from 'jsonwebtoken'
export const authMiddleware = async (req: Request, next: () => Promise<Response>) => {
try {
const authHeader = req.headers.get('authorization')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new AuthenticationError('Invalid authorization header')
}
const token = authHeader.substring(7)
const decoded = jwt.verify(token, process.env.JWT_SECRET!)
;(req as any).user = decoded
return next()
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
throw new AuthenticationError('Invalid token')
}
if (error instanceof jwt.TokenExpiredError) {
throw new AuthenticationError('Token expired')
}
throw error
}
}速率限制
typescript
// middleware/rateLimit.ts
const rateLimitMap = new Map<string, { count: number; resetTime: number }>()
export const rateLimitMiddleware = (maxRequests: number = 100, windowMs: number = 15 * 60 * 1000) => {
return async (req: Request, next: () => Promise<Response>) => {
const ip = req.headers.get('x-forwarded-for') || 'unknown'
const now = Date.now()
const key = `${ip}:${Math.floor(now / windowMs)}`
const current = rateLimitMap.get(key)
if (current && current.resetTime > now) {
if (current.count >= maxRequests) {
return new Response(JSON.stringify({
error: 'Too many requests',
retryAfter: Math.ceil((current.resetTime - now) / 1000)
}), {
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': Math.ceil((current.resetTime - now) / 1000).toString()
}
})
}
current.count++
} else {
rateLimitMap.set(key, { count: 1, resetTime: now + windowMs })
}
return next()
}
}输入清理
typescript
// utils/sanitization.ts
import DOMPurify from 'isomorphic-dompurify'
export const sanitizeHtml = (html: string): string => {
return DOMPurify.sanitize(html)
}
export const sanitizeObject = <T extends Record<string, any>>(obj: T): T => {
const sanitized: any = {}
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
sanitized[key] = sanitizeHtml(value)
} else if (typeof value === 'object' && value !== null) {
sanitized[key] = sanitizeObject(value)
} else {
sanitized[key] = value
}
}
return sanitized
}性能优化
路由缓存
typescript
// utils/cache.ts
const routeCache = new Map<string, { data: any; timestamp: number; ttl: number }>()
export const cacheMiddleware = (ttl: number = 300) => {
return async (req: Request, next: () => Promise<Response>) => {
const cacheKey = `${req.method}:${req.url}`
const cached = routeCache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < cached.ttl * 1000) {
return new Response(cached.data, {
headers: {
'Content-Type': 'application/json',
'X-Cache': 'HIT',
'Cache-Control': `public, max-age=${ttl}`
}
})
}
const response = await next()
if (response.status === 200) {
const data = await response.clone().text()
routeCache.set(cacheKey, {
data,
timestamp: Date.now(),
ttl
})
}
return response
}
}数据库查询优化
typescript
// services/userService.ts
export class UserService {
async findByIdWithPosts(id: string) {
// 使用 include 避免 N+1 查询
const user = await db.user.findUnique({
where: { id },
include: {
posts: {
select: {
id: true,
title: true,
createdAt: true
},
orderBy: { createdAt: 'desc' },
take: 10
}
}
})
return user
}
async findManyWithPagination(page: number = 1, limit: number = 20) {
const skip = (page - 1) * limit
const [users, total] = await Promise.all([
db.user.findMany({
skip,
take: limit,
orderBy: { createdAt: 'desc' }
}),
db.user.count()
])
return {
users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
}
}
}测试策略
单元测试
typescript
// tests/services/userService.test.ts
import { test, expect, describe, beforeEach } from 'bun:test'
import { UserService } from '../../src/services/userService'
describe('UserService', () => {
let userService: UserService
beforeEach(() => {
userService = new UserService()
})
test('should create user with valid data', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com',
password: 'password123'
}
const user = await userService.create(userData)
expect(user.name).toBe(userData.name)
expect(user.email).toBe(userData.email)
expect(user.password).not.toBe(userData.password) // 应该被哈希
})
test('should throw error for duplicate email', async () => {
const userData = {
name: 'John Doe',
email: 'existing@example.com',
password: 'password123'
}
await expect(userService.create(userData)).rejects.toThrow('Email already exists')
})
})集成测试
typescript
// tests/integration/user.test.ts
import { test, expect, describe, beforeAll, afterAll } from 'bun:test'
import { Server } from 'vafast'
import { userRoutes } from '../../src/routes/user'
describe('User API Integration', () => {
let server: Server
beforeAll(() => {
server = new Server(userRoutes)
})
test('GET /user/profile should return user profile', async () => {
const req = new Request('http://localhost:3000/user/profile', {
headers: {
'Authorization': 'Bearer valid-token'
}
})
const response = await server.fetch(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data).toHaveProperty('id')
expect(data).toHaveProperty('name')
expect(data).toHaveProperty('email')
})
test('PUT /user/profile should update user profile', async () => {
const updateData = {
name: 'Updated Name',
email: 'updated@example.com'
}
const req = new Request('http://localhost:3000/user/profile', {
method: 'PUT',
headers: {
'Authorization': 'Bearer valid-token',
'Content-Type': 'application/json'
},
body: JSON.stringify(updateData)
})
const response = await server.fetch(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.name).toBe(updateData.name)
expect(data.email).toBe(updateData.email)
})
})环境配置
配置管理
typescript
// config/app.ts
import { z } from 'zod'
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.string().transform(Number).default('3000'),
DATABASE_URL: z.string(),
JWT_SECRET: z.string(),
REDIS_URL: z.string().optional(),
CORS_ORIGIN: z.string().default('*'),
LOG_LEVEL: z.enum(['error', 'warn', 'info', 'debug']).default('info')
})
export const config = envSchema.parse(process.env)
export const isDevelopment = config.NODE_ENV === 'development'
export const isProduction = config.NODE_ENV === 'production'
export const isTest = config.NODE_ENV === 'test'环境特定配置
typescript
// config/database.ts
import { config } from './app'
export const databaseConfig = {
url: config.DATABASE_URL,
ssl: isProduction ? { rejectUnauthorized: false } : false,
logging: isDevelopment ? ['query', 'error', 'warn'] : ['error'],
pool: {
min: isProduction ? 5 : 1,
max: isProduction ? 20 : 5
}
}日志记录
结构化日志
typescript
// utils/logger.ts
export class Logger {
private formatMessage(level: string, message: string, meta?: any) {
return JSON.stringify({
timestamp: new Date().toISOString(),
level,
message,
...meta
})
}
info(message: string, meta?: any) {
console.log(this.formatMessage('info', message, meta))
}
warn(message: string, meta?: any) {
console.warn(this.formatMessage('warn', message, meta))
}
error(message: string, meta?: any) {
console.error(this.formatMessage('error', message, meta))
}
debug(message: string, meta?: any) {
if (isDevelopment) {
console.log(this.formatMessage('debug', message, meta))
}
}
}
export const logger = new Logger()请求日志中间件
typescript
// middleware/log.ts
import { logger } from '../utils/logger'
export const logMiddleware = async (req: Request, next: () => Promise<Response>) => {
const start = Date.now()
const method = req.method
const url = req.url
const userAgent = req.headers.get('user-agent')
const ip = req.headers.get('x-forwarded-for') || 'unknown'
logger.info('Request started', {
method,
url,
userAgent,
ip,
timestamp: new Date().toISOString()
})
try {
const response = await next()
const duration = Date.now() - start
logger.info('Request completed', {
method,
url,
status: response.status,
duration,
timestamp: new Date().toISOString()
})
return response
} catch (error) {
const duration = Date.now() - start
logger.error('Request failed', {
method,
url,
error: error.message,
duration,
timestamp: new Date().toISOString()
})
throw error
}
}总结
遵循这些最佳实践将帮助您:
- ✅ 构建可维护的代码结构
- ✅ 实现安全的应用程序
- ✅ 优化性能和用户体验
- ✅ 建立可靠的测试策略
- ✅ 管理环境配置
- ✅ 实现有效的日志记录
