Skip to content

2026-02-06 全栈类型统一改造深度评估与实施方案

1. 核心评估与战略定调

1.1 总体评估

经过对 2026-02-05-gemini-zod.md 提案的深度研读,结合当前项目的单体仓库(Monorepo)架构与业务路径(Business Path)规范,本方案被评估为“高价值、高必要性”的架构升级

它解决了长期困扰前后端分离架构的“类型断裂”问题,将 Drizzle (ORM)Zod (Validation)TypeScript (Types) 三者合而为一,实现了“定义一次,处处运行”的理想状态。

1.2 关键架构变动判定

本次改造不仅仅是代码搬运,而是 apps/type 项目性质的根本性变更

  • 当前状态apps/type 是一个 纯静态类型库。它只有 .ts 文件,只提供 interface/type,在编译后会被完全擦除,不包含任何运行时代码。
  • 目标状态apps/type 将升级为 全栈数据定义库 (Schema Registry)。它将导出 运行时对象(Zod Schemas, Drizzle Table Config),前端和后端都会在运行时引用它。

风险提示:这一变更要求 apps/type 必须具备运行时依赖(dependencies),而不仅仅是开发依赖(devDependencies)。这也意味着前端打包体积会增加(引入 Zod 库),但考虑到类型安全带来的收益,这是完全可接受的工程权衡。


2. 深度问答:实施细节与解决方案

以下针对你提出的 8 个关键问题进行深度的 UltraThink 模式解答。

Q2: 为了实现全栈化,需要对类型项目 (apps/type) 做怎么样的改造?

核心改造点:从“纯类型项目”转变为“同构逻辑库”。

  1. 依赖升级:需从 devDependencies 只有 typescript,变为拥有 drizzle-orm, zod, drizzle-zoddependencies
  2. 构建支持:虽然目前由 apps/admin 的 Vite 负责编译,但 apps/type 需要确保其导出的代码在 Node.js (后端) 和 Browser (前端) 环境下均可运行。这意味着:
    • 严禁在 schema 定义文件中引入 Node.js 专属模块(如 fs, path),或使用 drizzle-orm/node-postgres 等特定驱动。
    • 必须只使用 drizzle-orm/pg-core 这种纯 SQL 逻辑层的包。
  3. 目录结构维持:必须严格遵循CLAUDE.md规定的业务路径 (Business Path)
    • 不做:src/db/schemas/... (正如 Gemini 提案初版建议的那样,这是不符合规范的)
    • 要做src/business/property-manage/expense-manage/schemas.ts (将 Schema 下沉到具体的业务目录下)

Q3: 需要安装那些和 drizzle 相关的依赖?需要安装 zod tRPC 之类的全栈类型统一库么?

必须安装的依赖清单:

apps/type 中:

  • dependencies:
    • drizzle-orm: 核心 ORM 库,用于定义表结构。
    • zod: 核心校验库,用于运行时验证。
    • drizzle-zod: 桥接库,用于将 Drizzle 表定义自动转换为 Zod Schema。
  • 不需要 tRPC:TRPC 是通信层,本次改造关注的是 数据定义层。Nitro 本身配合 Zod 已经能达到极其优秀的类型安全效果。

apps/admin 中:

  • dependencies:
    • zod: 即使 apps/type 导出了 Schema,前端组件(如表单)可能还需要直接使用 Zod 的原语(如 z.infer 或自定义校验)。
    • drizzle-orm: 后端数据库连接仍需此库。

依赖安装命令参考:

bash
# apps/type (作为运行时提供者)
cd apps/type
pnpm add drizzle-orm zod drizzle-zod

# apps/admin (作为消费者)
cd ../admin
pnpm add zod
# 注意:drizzle-orm 和 drizzle-zod 在 admin 中可能已存在或作为间接依赖,建议显式安装 zod 即可

Q4: 数据库表 schemas 需要增加那些内容?如何暴露生成的业务类型?

Schema 文件将成为单一事实来源。一个标准的 Schema 文件应由三部分组成:部分 A (Table)部分 B (Zod)部分 C (Types)

示例文件apps/type/src/business/property-manage/community-manage/schema.ts

typescript
import { pgTable, serial, text, timestamp, integer } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { z } from "zod";

// ==========================================
// Part A: 数据库表定义 (Source of Truth)
// ==========================================
export const community = pgTable("community", {
	id: serial("id").primaryKey(),
	name: text("name").notNull(),
	address: text("address"),
	// 必须确保字段定义尽可能精确,例如 text 对应 string
	createTime: timestamp("create_time").defaultNow(),
});

// ==========================================
// Part B: Zod Runtime Schemas (暴露给运行时的校验规则)
// ==========================================

// B1. 插入时的校验 (User Input -> API)
// refine: 可以添加更强的业务校验逻辑
export const insertCommunitySchema = createInsertSchema(community, {
	name: (schema) => schema.min(2, "小区名称过短").max(50, "小区名称过长"),
	// 可以在这里覆盖自动生成的规则
}).omit({
	id: true,
	createTime: true,
}); // 插入时通常不需要 id 和 createTime

// B2. 查询/更新时的校验 (API -> Client)
export const selectCommunitySchema = createSelectSchema(community);

//B3. 更新时的校验 (User Input -> API)
export const updateCommunitySchema = insertCommunitySchema.partial().extend({
	id: z.number(), // 更新必须带 ID
});

// ==========================================
// Part C: Static Types (暴露给 TS 的静态类型)
// ==========================================
// 通过 z.infer 自动推导,永远不会与数据库定义脱节
export type Community = z.infer<typeof selectCommunitySchema>;
export type NewCommunity = z.infer<typeof insertCommunitySchema>;
export type UpdateCommunity = z.infer<typeof updateCommunitySchema>;

Q5: Schema 生成的全栈业务类型,该怎么组织封装到现在的类型项目中?

必须遵守 apps/typeIndex-Barrel (木桶) 导出原则。

文件组织结构示例:

text
apps/type/
└── src/
    └── business/
        └── property-manage/
            ├── index.ts  <-- 聚合导出
            └── community-manage/
                ├── index.ts <-- 聚合导出
                ├── schema.ts <-- **新增:包含 Table+Zod+Types**
                └── types.ts  <-- **废弃:原本的手动类型定义**

导出链条:

  1. apps/type/src/business/property-manage/index.ts:

    typescript
    export * from "./community-manage";
    // ...
  2. apps/type/src/business/property-manage/community-manage/index.ts:

    typescript
    // 直接导出 schema.ts 的所有内容 (Table, Schemas, Types)
    export * from "./schema";
    
    // 如果有不属于数据库的纯前端类型,依然可以保留在其他文件中并导出
    // export * from "./view-models";

Q6: 目前的类型项目要怎么完成改造,才能避免出现严重的破坏?

策略:Shadow Migration (影子迁移法)

这是最关键的一点。直接删除 apps/admin/server/db/schemas 会导致项目立即崩溃。

推荐的平滑迁移路径:

  1. 阶段一:基础设施就绪 (Infrastructure Ready)

    • apps/type 安装 drizzle-orm, zod, drizzle-zod
    • 配置 package.json 导出规则。
  2. 阶段二:影子共存 (Shadow Existence)

    • 不要动现有的 admin/server/db/schemas
    • apps/type/src/business/... 下新建 schema.ts,把表结构复制过去,并加上 Zod 定义。
    • 此时,数据库与代码实际上有两份定义(Admin 旧的,Type 新的),暂时互不干扰。
  3. 阶段三:后端核心切换 (Core Switch)

    • 修改 apps/admin/server/db/index.ts,将 schema 的引入源从内部文件改为 @01s-11comm/type
    • 修改 apps/admin/drizzle.config.ts,将 schema 扫描路径指向 apps/type 源码目录。
    • 验证:运行 drizzle-kit check 确保新定义的表结构与数据库实际结构完全一致。如果不一致,调整 apps/type 中的定义直到一致。
  4. 阶段四:业务层逐个击破 (Progressive Adoption)

    • 开发者在开发新功能时,直接使用 apps/typeinsertSchema
    • 对旧功能,按模块(如主要先改“小区管理”)进行重构,将 API 参数验证改为 readValidatedBody
  5. 阶段五:清理 (Cleanup)

    • 当确认所有模块都已切换,删除 apps/admin/server/db/schemas 目录。

Q7: 来自 schema 生成的业务类型,要怎么给 nitro 全栈接口使用?

Nitro (Nuxt) 的 h3 引擎完美支持 Zod。

标准模式:

typescript
// apps/admin/server/api/community/create.post.ts
import { insertCommunitySchema } from "@01s-11comm/type";

export default defineEventHandler(async (event) => {
	// 核心魔法:readValidatedBody
	// 1. 它读取 text/json body
	// 2. 它运行 Zod 校验
	// 3. 如果校验失败,自动抛出 400 Bad Request (含详细错误信息)
	// 4. 如果成功,returns 里的 body 已经是被 infer 推导出的 NewCommunity 类型
	const body = await readValidatedBody(event, insertCommunitySchema.parse);

	// body.name 在这里绝对存在且由类型保证
	const result = await db.insert(community).values(body).returning();
	return { data: result[0] };
});

Q8: 改造后的类型项目,要怎么确保前端的类型使用不会出现故障?

前端(Vue 组件)将获得双重保障:

  1. 编译时报错 (Static Check)

    • 如果后端修改了 Schema(例如 name 变为必填),前端如果传递了空对象,TS 编译直接报错。
    • import { type NewCommunity } from "@01s-11comm/type"
  2. 运行时表单校验 (Runtime Validation)

    • 前端表单可以直接复用 Zod Schema 进行校验,无需在前端重写一套 rules
    • typescript
      import { insertCommunitySchema } from "@01s-11comm/type";
      
      // 使用 form 库或手动校验
      const result = insertCommunitySchema.safeParse(formData.value);
      if (!result.success) {
      	console.log(result.error.format()); // 拿到具体的字段错误
      }
  3. 构建配置注意

    • 确保 Vite 配置能够正确解析 workspace 依赖。由于 apps/type 是源码引用,Vite 会自动处理 TS 编译,通常无需额外配置。但需确保 apps/type/package.jsonmain 字段指向入口文件。

3. 实施 Roadmap(全步骤清单)

为了确保万无一失,请按照以下 Checklist 执行:

3.1 准备工作 (Setup)

  • [ ] 在 apps/type/package.json 添加 drizzle-orm, zod, drizzle-zod
  • [ ] 在 apps/admin/package.json 确认/添加 zod
  • [ ] 确保 apps/typetsconfig.json 允许被外部项目引用(compositenoEmit: true 配合 source 引用)。

3.2 试点迁移 (Pilot)

  • [ ] 选择一个独立的、简单的业务模块(推荐:OperationTeam / 运维团队Dictionary / 字典)。
  • [ ] 在 apps/type 对应目录下创建 schema.ts
  • [ ] 编写 Drizzle Table, Zod Schemas, TS Types。
  • [ ] 导出到 index.ts
  • [ ] 尝试在 admin/server 创建一个测试 API 使用该 Schema。
  • [ ] 尝试在 admin/src 前端创建一个简单表单使用该 Schema。

3.3 数据库配置切换 (Switch)

  • [ ] 修改 apps/admin/drizzle.config.ts 指向 ../../apps/type/src/**/*.ts
  • [ ] 修改 apps/admin/server/db/index.ts 引入的新 Schema。
  • [ ] 运行 pnpm db:generate 检查是否有意外的 Drop Table 语句(如果有,说明路径或表名不匹配)。

3.4 批量迁移 (Rollout)

  • [ ] 按照业务路径,一个文件夹一个文件夹地搬运 Table 定义。
  • [ ] 这是一个纯体力的重构工作,可以使用 AI 辅助(Gemini)。

3.5 收尾 (Finalize)

  • [ ] 删除 apps/admin/server/db/schemas
  • [ ] 全局搜索项目,确保没有以前手动定义的 interface 残留。

4. 结论

本次评估认为:改造方案可行,且是现代化架构的必经之路。

虽然初期会有一定的“阵痛期”(需要搬运大量代码),但一旦完成,我们将获得一个全链路类型安全的坚固堡垒,极大地降低未来的维护成本和沟通成本(前后端不再需要对接口文档,代码即文档)。

建议立即启动试点迁移。

贡献者

The avatar of contributor named as ruan-cat ruan-cat
The avatar of contributor named as Claude Opus 4.6 Claude Opus 4.6

页面历史

最近更新