写作归档

Monorepo公共逻辑

类型
文章
日期
2026/10/16
阅读
5 分钟
字数
1,345

上一篇文章确定了 Monorepo 的整体架构:仓库中包含 server、web、admin 三个应用,以及 shared、ui、config 三个共享包,并使用 Turborepo 和 yarn workspaces 统一管理。

这篇文章继续把架构落实到具体的工程结构中。我们会依次确定目录如何组织、公共逻辑放在哪里、各个子项目分别关注什么,以及日常开发和部署流程如何配合。

2. 目录结构

我们先看完整目录,再逐层说明每个位置承担的职责。

apps/ 与 packages/ 之间有明确的职责边界。apps/ 存放可以独立部署的应用,packages/ 存放被这些应用引用的共享包。Turborepo、Nx 和多数 Monorepo 模板都采用类似约定,开发者可以据此区分最终部署的产物与内部依赖。

每个子项目都拥有独立的 package.json,这是 Yarn workspaces 组织工作区的基础。应用和共享包分别声明自己的依赖与 scripts,再由 workspaces 建立它们之间的链接。

packages/config 不包含运行时代码,只保存 tsconfig、Biome 等配置文件,其他项目通过 extends 引用。公共配置集中后,只需要修改一处,就能让各子项目使用相同的工程规则。

3. packages/shared 公共逻辑

packages/shared 负责保存类型定义、验证规则、业务常量和工具函数。它不包含 UI 组件,也不依赖具体的运行时框架,内部以 TypeScript 为主。只要 Zod、nanoid 等运行时依赖也兼容目标环境,Hono 服务端和两个 Next.js 应用就可以直接引用它。

Zod Schema 在共享包中同时承担两项职责。运行时,服务端用它校验请求参数,客户端用它检查表单输入;编译时,TypeScript 可以直接从 Schema 推导类型,不需要再维护一套对应的 interface。

schemas/chat.ts

// packages/shared/src/schemas/chat.ts
import { z } from 'zod'
import { emotionSchema } from './emotion'

// 对话消息 —— 用户发送
export const chatRequestSchema = z.object({
  content: z.string().min(1, '消息不能为空').max(2000, '消息过长'),
  session_id: z.string().uuid('无效的会话 ID'),
  emotion_hint: emotionSchema.optional(),
})

// 对话消息 —— AI 回复
export const chatResponseSchema = z.object({
  reply: z.string(),
  emotion: emotionSchema,
  memories_used: z.number().int().min(0),
  session_id: z.string().uuid(),
  created_at: z.string().datetime(),
})

// TypeScript 类型——从 Schema 推断,永远与验证规则同步
export type ChatRequest = z.infer<typeof chatRequestSchema>
export type ChatResponse = z.infer<typeof chatResponseSchema>

schemas/emotion.ts

// packages/shared/src/schemas/emotion.ts
import { z } from 'zod'

export const emotionSchema = z.enum([
  'happy',      // 开心
  'sad',        // 难过
  'neutral',    // 平静
  'excited',    // 兴奋
  'anxious',    // 焦虑
  'calm',       // 放松
  'curious',    // 好奇
  'angry',      // 生气
  'tender',     // 温柔
  'playful',    // 调皮
])

export type Emotion = z.infer<typeof emotionSchema>

// 情绪转移规则的类型定义
export const emotionTransitionSchema = z.object({
  from: emotionSchema,
  to: emotionSchema,
  trigger: z.string(),
  probability: z.number().min(0).max(1),
})

export type EmotionTransition = z.infer<typeof emotionTransitionSchema>

schemas/memory.ts

// packages/shared/src/schemas/memory.ts
import { z } from 'zod'

export const memoryTypeSchema = z.enum([
  'episodic',    // 情景记忆:具体事件
  'semantic',    // 语义记忆:归纳性知识
  'preference',  // 偏好记忆:用户喜好
])

export type MemoryType = z.infer<typeof memoryTypeSchema>

export const memorySchema = z.object({
  id: z.string(),
  user_id: z.string(),
  type: memoryTypeSchema,
  content: z.string(),
  importance: z.number().min(0).max(1),
  created_at: z.string().datetime(),
  last_accessed: z.string().datetime(),
  access_count: z.number().int().min(0),
})

export type Memory = z.infer<typeof memorySchema>

定义完成后,server、web 和 admin 可以按照各自的用途引用这些 Schema。

server-usage.ts

// apps/server —— 服务端用 Zod 做请求验证
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { chatRequestSchema } from '@ai-companion/shared'

const app = new Hono()

app.post('/chat', zValidator('json', chatRequestSchema), async (c) => {
  const body = c.req.valid('json')
  // body 的类型自动推断为 ChatRequest,无需手动标注
  return c.json({ reply: `收到: ${body.content}` })
})

web-usage.tsx

// apps/web —— 客户端用 Zod 做表单验证(配合 react-hook-form)
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { chatRequestSchema, type ChatRequest } from '@ai-companion/shared'

export const ChatForm = () => {
  const form = useForm<ChatRequest>({
    resolver: zodResolver(chatRequestSchema),
  })

  return <form onSubmit={form.handleSubmit(() => undefined)} />
}
// 表单验证规则与服务端完全一致,用户输入超过 2000 个字符时客户端就会拦截

admin-usage.tsx

// apps/admin —— 后台用类型定义渲染审计表格
import type { ChatResponse } from '@ai-companion/shared'
import { EmotionBadge } from '@ai-companion/ui'

export const AuditTable = ({ records }: { records: ChatResponse[] }) => {
  // records 的类型与服务端返回的结构完全一致
  return (
    <tbody>
      {records.map(r => (
        <tr key={r.session_id}>
          <td>{r.reply}</td>
          <td><EmotionBadge emotion={r.emotion} /></td>
          <td>{r.memories_used}</td>
        </tr>
      ))}
    </tbody>
  )
}

同一份 Schema 在服务端负责运行时验证,在客户端负责表单验证,在后台负责类型约束。规则发生变化后,三个应用在同一工作区重新检查或构建时,可以同步看到新的定义。

上一篇文章介绍了 Hono RPC 的端到端类型安全。实现时,服务端导出完整的路由类型,客户端再通过 hc 创建带有类型推断的调用实例。

server-routes.ts

// apps/server/src/routes/chat.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { chatRequestSchema } from '@ai-companion/shared'

const chat = new Hono()
  .post('/', zValidator('json', chatRequestSchema), async (c) => {
    const body = c.req.valid('json')
    // ... 对话管线处理
    return c.json({
      reply: '你好呀~',
      emotion: 'happy' as const,
      memories_used: 3,
      session_id: body.session_id,
      created_at: new Date().toISOString(),
    })
  })
  .get('/history/:sessionId', async (c) => {
    const sessionId = c.req.param('sessionId')
    // ... 获取对话历史
    return c.json({ messages: [] })
  })

export { chat }

server-index.ts

// apps/server/src/index.ts
import { Hono } from 'hono'
import { chat } from './routes/chat'
import { memory } from './routes/memory'
import { auth } from './routes/auth'

const app = new Hono()
  .route('/chat', chat)
  .route('/memory', memory)
  .route('/auth', auth)

export type AppType = typeof app
export default app

web-client.ts

// apps/web/src/lib/api.ts
import { hc 

订阅后解锁剩余内容

已免费试看前 30%,订阅小册即可阅读全文。

订阅小册
0 条

还没有评论。

发表评论