06-项目重构指南
分类:elysia
发布于:
阅读时间:69 分钟
Elysia 项目重构指南
概述
本文档提供了从现有项目迁移到 Elysia + Drizzle + Zod 架构的详细指南,包括重构步骤、最佳实践、常见问题解决方案,以及检查清单。
重构目标
主要目标
- 类型安全: 实现前后端类型同步
- 代码规范: 统一项目结构和代码风格
- 错误处理: 完善的错误处理机制
- API设计: RESTful接口设计规范
- 开发效率: 提高开发和维护效率
架构目标
- 四层类型系统架构
- Service层业务逻辑封装
- Controller层接口统一
- 统一响应格式
- 完善的文档体系
重构前准备
1. 现状分析
项目结构检查清单
# 检查当前项目结构
find src -type f -name "*.ts" | head -20
# 检查路由定义
ripgrep "app\.(get|post|put|delete|patch)" src/
# 检查数据库操作
ripgrep "(SELECT|INSERT|UPDATE|DELETE)" src/
# 检查类型定义
find src -name "*.types.ts" -o -name "*.d.ts"
# 检查错误处理
ripgrep "(throw|Error)" src/
技术栈分析
// 分析当前使用的技术栈
const currentStack = {
framework: 'Express.js / Fastify / 其他',
orm: 'Prisma / TypeORM / Sequelize / 原生SQL',
validation: 'Joi / class-validator / 手动验证',
types: 'TypeScript / JavaScript',
testing: 'Jest / Vitest / 无测试',
documentation: 'Swagger / 自定义 / 无文档',
};
2. 环境准备
依赖安装
# 安装 Elysia 核心依赖
pnpm add elysia @elysiajs/swagger @elysiajs/cors
# 安装 Drizzle ORM
pnpm add drizzle-orm drizzle-kit postgres
# 安装 Zod 和相关工具
pnpm add zod drizzle-zod
# 安装开发工具
pnpm add -D @types/node typescript biome pkgroll
# 安装测试工具(如果需要)
pnpm add -D vitest @vitest/ui
配置文件创建
// turbo.json
{
"$schema": "https://turborepo.com/schema.json",
"tasks": {
"build": { "outputs": ["dist/**"] },
"clean": {},
"check": { "dependsOn": ["^check"] },
"dev": { "persistent": true, "cache": false },
"type-check": "tsc --noEmit",
"format": "biome format --write .",
"lint": "biome check ."
}
}
// biome.json
{
"formatter": {
"enabled": true,
"lineWidth": 80,
"indentStyle": "space",
"indentSize": 2
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}
重构步骤
第1步:项目结构重组
创建新的目录结构
# 创建基础目录结构
mkdir -p src/modules
mkdir -p src/db/schema
mkdir -p src/utils
mkdir -p src/middleware
mkdir -p src/types
# 为现有模块创建目录
mkdir -p src/modules/users
mkdir -p src/modules/products
mkdir -p src/modules/orders
文件重命名规则
// 旧文件名 → 新文件名
user.routes.ts → users.controller.ts
user.service.ts → users.service.ts
user.model.ts → users.model.ts
user.schema.ts → users.schema.ts
user.types.ts → users.types.ts
user.validator.ts → users.zod.ts
第2步:数据库Schema迁移
从现有ORM迁移到Drizzle
示例:从Prisma迁移
// schema.prisma (旧)
model User {
id Int @id @default(autoincrement())
email String @unique
username String @unique
password String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// users.schema.ts (新)
import { pgTable, serial, varchar, boolean, timestamp } from "drizzle-orm/pg-core";
export const usersTable = pgTable("users", {
id: serial("id").primaryKey(),
email: varchar("email", { length: 100 }).notNull().unique(),
username: varchar("username", { length: 50 }).notNull().unique(),
password: varchar("password", { length: 255 }).notNull(),
isActive: boolean("is_active").default(true),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
关系迁移
// 旧关系定义
model User {
id Int @id @default(autoincrement())
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
}
// 新关系定义
export const usersTable = pgTable("users", { /* ... */ });
export const postsTable = pgTable("posts", {
id: serial("id").primaryKey(),
userId: integer("user_id").references(() => usersTable.id),
// ...
});
export const usersRelations = relations(usersTable, ({ many }) => ({
posts: many(postsTable),
}));
export const postsRelations = relations(postsTable, ({ one }) => ({
user: one(usersTable, {
fields: [postsTable.userId],
references: [usersTable.id],
}),
}));
第3步:类型系统重构
创建四层架构
第1层:Drizzle表定义
// users.schema.ts
export const usersTable = pgTable("users", {
// 表定义...
});
第2层:Zod Schema
// users.zod.ts
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
import { z } from "zod";
import { usersTable } from "./users.schema";
export const InsertUserSchema = createInsertSchema(usersTable);
export const SelectUserSchema = createSelectSchema(usersTable);
export const CreateUserSchema = InsertUserSchema.omit({
id: true,
createdAt: true,
updatedAt: true,
});
第3层:Elysia模型
// users.model.ts
import { t } from "elysia";
import { CreateUserSchema } from "./users.zod";
export const UsersModel = {
CreateUser: t.Object({
username: t.String({ minLength: 2, maxLength: 50 }),
email: t.String({ format: "email" }),
password: t.String({ minLength: 8 }),
}),
UserResponse: t.Object({
id: t.Number(),
username: t.String(),
email: t.String(),
isActive: t.Boolean(),
createdAt: t.Date(),
}),
};
第4层:TypeScript类型
// users.types.ts
export type CreateUserDto = typeof UsersModel.CreateUser.static;
export type UserResponseDto = typeof UsersModel.UserResponse.static;
export type UserEntity = z.infer<typeof SelectUserSchema>;
第4步:Service层重构
错误处理模式迁移
旧模式(返回null)
// 旧的Service方法
async getUserById(id: number): Promise<User | null> {
const user = await db.user.findUnique({ where: { id } });
return user; // 可能返回null
}
新模式(抛出错误)
// 新的Service方法
async getUserById(id: number): Promise<UserEntity> {
const [user] = await db
.select()
.from(usersTable)
.where(eq(usersTable.id, id))
.limit(1);
if (!user) {
throw new NotFoundError(`User with id ${id} not found`);
}
return user;
}
分页查询重构
旧分页实现
async getUsers(page: number, limit: number) {
const skip = (page - 1) * limit;
const users = await db.user.findMany({
skip,
take: limit,
});
return users; // 返回格式不统一
}
新分页实现
async getUserList(query: UserListQueryDto): Promise<{
items: UserEntity[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
};
}> {
const { page = 1, limit = 10 } = query;
const offset = (page - 1) * limit;
const [users, [{ total }]] = await Promise.all([
db.select()
.from(usersTable)
.limit(limit)
.offset(offset),
db.select({ total: count() }).from(usersTable)
]);
return {
items: users,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
第5步:Controller层重构
路由定义迁移
旧路由定义(Express风格)
// 旧的路由定义
app.get('/api/users/:id', async (req, res) => {
try {
const user = await userService.getUserById(req.params.id);
res.json({ success: true, data: user });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
新路由定义(Elysia风格)
// 新的路由定义
export const userController = new Elysia({ prefix: '/users' })
.get('/:id', async ({ params: { id }, userService }) => {
const user = await userService.getUserById(id);
return commonRes(user);
}, {
params: 'IdParam',
response: {
200: t.Object({
success: t.Boolean(),
data: 'UserResponse',
}),
404: 'ErrorResponse',
},
detail: {
summary: '获取用户详情',
tags: ['Users'],
},
});
统一响应格式
旧响应格式
// 各种不一致的响应格式
res.json({ success: true, data: user });
res.json({ code: 200, message: 'success', result: user });
res.json(user); // 直接返回数据
新响应格式
// 统一响应格式
import { commonRes } from "@/utils/response";
return commonRes(user); // { success: true, data: user }
return commonRes(result, 201); // 创建成功返回201
第6步:错误处理重构
创建自定义错误类
// utils/errors.ts
export class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public code: string = 'INTERNAL_ERROR'
) {
super(message);
this.name = this.constructor.name;
}
}
export class NotFoundError extends AppError {
constructor(message: string = 'Resource not found') {
super(message, 404, 'NOT_FOUND');
}
}
export class ValidationError extends AppError {
constructor(message: string = 'Validation failed') {
super(message, 400, 'VALIDATION_ERROR');
}
}
全局错误处理器
// middleware/error.ts
export const errorHandler = new Elysia()
.error({
NotFoundError: NotFoundError,
ValidationError: ValidationError,
DatabaseError: DatabaseError,
})
.onError(({ error, code, set }) => {
const statusCode = error.statusCode || 500;
set.status = statusCode;
return {
success: false,
error: {
message: error.message,
code: error.code || 'INTERNAL_ERROR',
},
timestamp: new Date().toISOString(),
};
});
迁移验证
1. 功能验证清单
基础功能验证
# 检查所有路由是否正常工作
curl -X GET http://localhost:3000/users
curl -X POST http://localhost:3000/users -d '{"username":"test","email":"test@example.com"}'
curl -X GET http://localhost:3000/users/1
# 检查错误处理
curl -X GET http://localhost:3000/users/9999 # 应该返回404
curl -X POST http://localhost:3000/users -d '{}' # 应该返回400
类型安全验证
// 检查类型推断是否正确
const createUser = async (data: CreateUserDto) => {
// TypeScript应该能正确推断参数类型
const user = await userService.createUser(data);
return user; // 返回类型应该被正确推断
};
2. 性能验证
数据库查询优化
// 检查N+1查询问题
const usersWithPosts = await db
.select({
user: usersTable,
posts: postsTable,
})
.from(usersTable)
.leftJoin(postsTable, eq(usersTable.id, postsTable.userId));
// 使用Promise.all并行查询
const [users, posts] = await Promise.all([
db.select().from(usersTable),
db.select().from(postsTable),
]);
3. 测试迁移
单元测试示例
// tests/services/users.service.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { UserService } from '@/modules/users/users.service';
import { NotFoundError } from '@/utils/errors';
describe('UserService', () => {
let userService: UserService;
beforeEach(() => {
userService = new UserService();
});
describe('getUserById', () => {
it('should return user when found', async () => {
const user = await userService.getUserById(1);
expect(user).toBeDefined();
expect(user.id).toBe(1);
});
it('should throw NotFoundError when user not found', async () => {
await expect(userService.getUserById(9999))
.rejects
.toThrow(NotFoundError);
});
});
});
常见问题解决
1. 类型转换问题
Decimal类型处理
// 旧代码中的Decimal处理
const price = product.price; // Decimal对象
// 新代码中的正确处理
export const CreateProductSchema = InsertProductSchema.extend({
price: z.string().transform((val) => new Decimal(val)),
cost: z.string().transform((val) => new Decimal(val)),
});
Date类型处理
// 统一Date类型处理
export const UserModel = {
UserResponse: t.Object({
createdAt: t.Date(), // Elysia会自动处理Date序列化
updatedAt: t.Date(),
}),
};
2. 关系查询问题
多表联查
// 旧代码中的关系查询
const userWithPosts = await db.user.findUnique({
where: { id },
include: { posts: true },
});
// 新代码中的关系查询
const userWithPosts = await db
.select({
user: usersTable,
posts: postsTable,
})
.from(usersTable)
.leftJoin(postsTable, eq(usersTable.id, postsTable.userId))
.where(eq(usersTable.id, id));
3. 中间件迁移
认证中间件迁移
// 旧认证中间件(Express)
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
// 验证逻辑...
next();
};
// 新认证中间件(Elysia)
export const authMiddleware = new Elysia()
.derive(async ({ headers }) => {
const token = headers.authorization?.replace('Bearer ', '');
if (!token) {
throw new UnauthorizedError('Authentication required');
}
const payload = jwt.verify(token, process.env.JWT_SECRET!);
const user = await userService.getUserById(payload.userId);
return { user };
});
重构检查清单
1. 代码质量检查
类型安全检查
- 所有函数都有明确的返回类型
- 没有使用
any类型 - Zod Schema 与 TypeScript 类型一致
- Elysia 模型定义正确
错误处理检查
- Service层不返回null,改为抛出错误
- 使用自定义错误类
- 全局错误处理器正确配置
- 错误响应格式统一
API设计检查
- 遵循RESTful设计原则
- 使用正确的HTTP方法
- 状态码使用规范
- 路由命名规范
2. 功能完整性检查
数据库操作检查
- 所有CRUD操作正常工作
- 关系查询正确
- 事务处理正确
- 分页查询格式统一
认证授权检查
- JWT认证正常工作
- 权限验证正确
- 敏感接口有保护
- 用户状态验证
文档检查
- Swagger文档自动生成
- 接口描述完整
- 示例数据正确
- 错误响应文档化
3. 性能优化检查
数据库优化
- 查询使用索引
- 避免N+1查询
- 分页查询高效
- 连接池配置合理
响应优化
- 响应格式统一
- 数据传输最小化
- 缓存策略合理
- 并发请求处理
4. 开发体验检查
工具配置
- Biome代码格式化正常
- TypeScript类型检查通过
- 热重载功能正常
- 测试环境配置完整
文档和注释
- 代码注释完整
- API文档准确
- README文档更新
- 部署文档清晰
最佳实践总结
1. 渐进式迁移策略
- 分模块迁移: 一次迁移一个模块,确保系统稳定
- 并行开发: 新旧系统并行运行,逐步切换
- 充分测试: 每个迁移步骤都要有完整的测试
- 回滚准备: 准备快速回滚方案
2. 团队协作规范
- 代码审查: 所有迁移代码必须经过审查
- 文档同步: 及时更新相关文档
- 知识分享: 组织技术分享会议
- 培训支持: 为团队成员提供培训
3. 持续改进
- 性能监控: 迁移后监控系统性能
- 用户反馈: 收集用户使用反馈
- 迭代优化: 根据反馈持续优化
- 技术债务: 及时处理技术债务