返回首页

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. 渐进式迁移策略

  1. 分模块迁移: 一次迁移一个模块,确保系统稳定
  2. 并行开发: 新旧系统并行运行,逐步切换
  3. 充分测试: 每个迁移步骤都要有完整的测试
  4. 回滚准备: 准备快速回滚方案

2. 团队协作规范

  1. 代码审查: 所有迁移代码必须经过审查
  2. 文档同步: 及时更新相关文档
  3. 知识分享: 组织技术分享会议
  4. 培训支持: 为团队成员提供培训

3. 持续改进

  1. 性能监控: 迁移后监控系统性能
  2. 用户反馈: 收集用户使用反馈
  3. 迭代优化: 根据反馈持续优化
  4. 技术债务: 及时处理技术债务

相关文档