Monorepo 公共逻辑
要点
- 上一篇文章确定了 Monorepo 的整体架构:三个应用(server / web / admin)+ 三个共享包(shared / ui / config)
- 先看整体目录,再逐层解释设计意图
- 这是整个 Monorepo 中最重要的共享包
- web 和 admin 都是 Next.js 应用,在 UI 层面存在可复用的组件
- 配置包不包含运行时代码,只存放被其他项目 extends 的配置文件
内容
1. 从蓝图到施工图
上一篇文章确定了 Monorepo 的整体架构:三个应用(server / web / admin)+ 三个共享包(shared / ui / config),用 Turborepo + yarn workspaces 管理。
那篇文章回答的是「为什么这样做」。这篇文章要回答**「具体怎么做」**——目录怎么组织、公共逻辑怎么抽取、各子项目各自关注什么、开发和部署的工作流是什么样的。
2. 目录结构设计
先看整体目录,再逐层解释设计意图。
// structure.txt
ai-companion/
├── apps/
│ ├── server/ # Hono 服务端
│ │ ├── src/
│ │ │ ├── routes/ # 路由定义
│ │ │ ├── middleware/ # 中间件
│ │ │ ├── services/ # 业务逻辑
│ │ │ └── index.ts # 入口
│ │ ├── wrangler.toml # CloudFlare Workers 配置
│ │ ├── tsconfig.json
│ │ └── package.json
│ │
│ ├── web/ # Next.js 客户端
│ │ ├── src/
│ │ │ ├── app/ # App Router 页面
│ │ │ ├── components/ # 客户端专属组件
│ │ │ ├── hooks/ # 自定义 Hooks
│ │ │ ├── stores/ # Zustand stores
│ │ │ └── lib/ # 工具函数
│ │ ├── next.config.ts
│ │ ├── tsconfig.json
│ │ └── package.json
│ │
│ └── admin/ # Next.js 后台管理
│ ├── src/
│ │ ├── app/ # App Router 页面
│ │ ├── components/ # 后台专属组件
│ │ ├── hooks/
│ │ ├── stores/
│ │ └── lib/
│ ├── next.config.ts
│ ├── tsconfig.json
│ └── package.json
│
├── packages/
│ ├── shared/ # 共享类型、Schema、常量、工具
│ │ ├── src/
│ │ │ ├── schemas/ # Zod Schema
│ │ │ ├── types/ # TypeScript 类型
│ │ │ ├── constants/ # 业务常量与枚举
│ │ │ ├── utils/ # 工具函数
│ │ │ └── index.ts # 统一导出
│ │ ├── tsconfig.json
│ │ └── package.json
│ │
│ ├── ui/ # 共享 UI 组件
│ │ ├── src/
│ │ │ ├── components/ # 组件
│ │ │ ├── styles/ # 共享样式
│ │ │ └── index.ts
│ │ ├── tsconfig.json
│ │ └── package.json
│ │
│ └── config/ # 共享配置
│ ├── tsconfig.base.json # TypeScript 基础配置
│ ├── tsconfig.next.json # Next.js 项目配置
│ ├── tsconfig.node.json # Node/Workers 项目配置
│ └── biome.json # Biome lint/format 配置
│
├── turbo.json # Turborepo 任务配置
├── package.json # 根 package.json
└── yarn.lockapps/ 和 packages/ 的分界线。 apps/ 放可独立部署的应用,packages/ 放被应用依赖的共享包。这不是我们发明的约定——Turborepo、Nx、大多数 Monorepo 模板都遵循这个惯例。好处是一眼就能区分"产物"和"依赖"。
每个子项目都有独立的 package.json。 这是 yarn workspaces 的要求。每个子项目声明自己的依赖和 scripts,workspaces 负责将它们链接在一起。
packages/config 不包含运行时代码。 它只存放配置文件(tsconfig、biome),被其他项目通过 extends 引用。把配置集中管理,修改一处全局生效。
3. packages/shared——公共逻辑的核心
这是整个 Monorepo 中最重要的共享包。它不包含任何 UI 组件和运行时框架依赖,只包含纯 TypeScript 代码——类型定义、验证规则、常量、工具函数。这意味着它可以同时被 Hono 服务端和两个 Next.js 应用引用,不存在运行时兼容问题。
3.1 Zod Schema 与类型定义
Zod Schema 是 packages/shared 的核心产出。一份 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>三端如何消费这些 Schema:
// server-usage.ts
// apps/server —— 服务端用 Zod 做请求验证
import { zValidator } from '@hono/zod-validator'
import { chatRequestSchema } from '@ai-companion/shared'
app.post('/chat', zValidator('json', chatRequestSchema), async (c) => {
const body = c.req.valid('json')
// body 的类型自动推断为 ChatRequest,无需手动标注
})// 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'
const form = useForm<ChatRequest>({
resolver: zodResolver(chatRequestSchema),
})
// 表单验证规则与服务端完全一致,用户输入超过 2000 字时客户端就会拦截// admin-usage.tsx
// apps/admin —— 后台用类型定义渲染审计表格
import type { ChatResponse } from '@ai-companion/shared'
import { EmotionBadge } from '@ai-companion/ui'
const AuditTable = ({ records }: { records: ChatResponse[] }) => {
// records 的类型与服务端返回的结构完全一致
return records.map(r => (
<tr key={r.session_id}>
<td>{r.reply}</td>
<td><EmotionBadge emotion={r.emotion} /></td>
<td>{r.memories_used}</td>
</tr>
))
}一份 Schema,三种用法:服务端做运行时验证、客户端做表单验证、后台做类型约束。改一处,三端同步。
3.2 Hono RPC 类型导出
上一篇提到 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 } from 'hono/client'
import type { AppType } from '@ai-companion/server'
export const api = hc<AppType>(process.env.NEXT_PUBLIC_API_URL!)
// 使用时,所有参数和返回值都有完整的类型提示
const res = await api.chat.$post({
json: {
content: '今天过得怎么样?',
session_id: 'xxx-xxx-xxx',
}
})
const data = await res.json()
// data.reply → string
// data.emotion → 'happy' | 'sad' | 'neutral' | ...
// data.memories_used → number这里的关键是 import type { AppType } from '@ai-companion/server'。在 Monorepo 中,这个 import 直接解析到 apps/server/src/index.ts 的源码。类型推导链路是:路由定义 → zValidator 的 Schema → 返回值的 c.json() → 最终的 AppType。整条链路都是编译时推断,零运行时开销。
web 和 admin 都可以用同一个 AppType,但通常 admin 会访问额外的管理 API,所以 server 可能会导出两个类型:
// server-types.ts
// apps/server/src/index.ts
export type AppType = typeof app // 公共 API(web + admin 都能用)
export type AdminType = typeof admin // 管理 API(仅 admin 使用)3.3 认证与鉴权逻辑
认证相关的类型和工具函数是三端都需要的。
// auth.ts
// packages/shared/src/schemas/auth.ts
import { z } from 'zod'
export const roleSchema = z.enum(['user', 'admin', 'super_admin'])
export type Role = z.infer<typeof roleSchema>
// JWT payload 的类型定义
export const jwtPayloadSchema = z.object({
sub: z.string(), // 用户 ID
role: roleSchema,
iat: z.number(), // 签发时间
exp: z.number(), // 过期时间
})
export type JwtPayload = z.infer<typeof jwtPayloadSchema>
// 登录请求
export const loginRequestSchema = z.object({
email: z.string().email('无效的邮箱格式'),
password: z.string().min(8, '密码至少 8 位'),
})
export type LoginRequest = z.infer<typeof loginRequestSchema>
// 注册请求
export const registerRequestSchema = loginRequestSchema.extend({
nickname: z.string().min(2, '昵称至少 2 个字').max(20, '昵称最多 20 个字'),
})
export type RegisterRequest = z.infer<typeof registerRequestSchema>// auth-utils.ts
// packages/shared/src/utils/auth.ts
import type { Role } from '../schemas/auth'
// 权限检查——服务端中间件和客户端路由守卫都会用到
const ROLE_HIERARCHY: Record<Role, number> = {
user: 0,
admin: 1,
super_admin: 2,
}
export function hasPermission(userRole: Role, requiredRole: Role): boolean {
return ROLE_HIERARCHY[userRole] >= ROLE_HIERARCHY[requiredRole]
}
// Token 是否过期——客户端判断是否需要刷新 token
export function isTokenExpired(exp: number, bufferSeconds = 60): boolean {
return Date.now() / 1000 > exp - bufferSeconds
}服务端用这些类型定义中间件的鉴权逻辑,客户端用 isTokenExpired 判断是否需要静默刷新 token,后台用 hasPermission 做前端路由守卫。同一套权限模型,三端行为一致。
3.4 业务常量与错误码
// constants.ts
// packages/shared/src/constants/index.ts
// 记忆系统参数
export const MEMORY = {
MAX_EPISODIC: 1000, // 单用户最大情景记忆数
MAX_SEMANTIC: 200, // 单用户最大语义记忆数
IMPORTANCE_THRESHOLD: 0.3, // 低于此阈值的记忆可能被遗忘
RETRIEVAL_TOP_K: 5, // 语义检索返回的最大记忆数
EMBEDDING_DIMENSION: 768, // 向量维度(BGE base)
} as const
// 对话参数
export const CHAT = {
MAX_HISTORY_TURNS: 20, // 上下文窗口中保留的最大对话轮数
MAX_MESSAGE_LENGTH: 2000, // 单条消息最大长度
STREAM_CHUNK_SIZE: 10, // 流式传输的 chunk 大小(token 数)
} as const
// 情绪系统参数
export const EMOTION = {
DECAY_RATE: 0.1, // 情绪衰减速率(趋向 neutral 的速度)
TRANSITION_THRESHOLD: 0.6, // 触发情绪转移的最低概率
UPDATE_INTERVAL: 300, // 情绪状态更新间隔(秒)
} as const// errors.ts
// packages/shared/src/constants/errors.ts
export const ERROR_CODES = {
// 认证错误 1xxx
AUTH_INVALID_TOKEN: { code: 1001, message: 'token 无效或已过期' },
AUTH_INSUFFICIENT_ROLE: { code: 1002, message: '权限不足' },
AUTH_EMAIL_EXISTS: { code: 1003, message: '邮箱已注册' },
// 对话错误 2xxx
CHAT_CONTENT_TOO_LONG: { code: 2001, message: '消息内容超过长度限制' },
CHAT_SESSION_NOT_FOUND: { code: 2002, message: '会话不存在' },
CHAT_RATE_LIMITED: { code: 2003, message: '发送过于频繁,请稍后再试' },
CHAT_UNSAFE_CONTENT: { code: 2004, message: '消息内容不合规' },
// 记忆错误 3xxx
MEMORY_NOT_FOUND: { code: 3001, message: '记忆不存在' },
MEMORY_LIMIT_EXCEEDED: { code: 3002, message: '记忆数量已达上限' },
// 系统错误 5xxx
INTERNAL_ERROR: { code: 5001, message: '系统内部错误' },
LLM_TIMEOUT: { code: 5002, message: 'AI 响应超时' },
LLM_UNAVAILABLE: { code: 5003, message: 'AI 服务暂时不可用' },
} as const
export type ErrorCode = keyof typeof ERROR_CODES服务端根据错误码返回标准化的错误响应,客户端根据错误码展示对应的用户提示(CHAT_RATE_LIMITED → 显示"发送太快了"),后台根据错误码做错误分类统计和告警规则配置。错误码是三端的公共语言。
3.5 工具函数
// utils.ts
// packages/shared/src/utils/index.ts
import { nanoid } from 'nanoid'
// ID 生成——统一长度和字符集
export function generateId(prefix?: string): string {
const id = nanoid(16)
return prefix ? `${prefix}_${id}` : id
}
// 日期格式化——对话时间展示
export function formatChatTime(isoString: string): string {
const date = new Date(isoString)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMin = Math.floor(diffMs / 60000)
if (diffMin < 1) return '刚刚'
if (diffMin < 60) return `${diffMin} 分钟前`
const diffHour = Math.floor(diffMin / 60)
if (diffHour < 24) return `${diffHour} 小时前`
const diffDay = Math.floor(diffHour / 24)
if (diffDay < 7) return `${diffDay} 天前`
return date.toLocaleDateString('zh-CN')
}
// 文本截断——记忆预览、消息摘要
export function truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text
return text.slice(0, maxLength - 1) + '…'
}
// 安全的 JSON 解析——避免 LLM 返回格式异常时崩溃
export function safeJsonParse<T>(text: string, fallback: T): T {
try {
return JSON.parse(text) as T
} catch {
return fallback
}
}这些工具函数看似简单,但如果不集中管理,三个子项目会各自实现一套。时间格式不统一、ID 长度不一致、JSON 解析有的做了错误处理有的没做——这些微小的不一致会在后期成为 bug 的温床。
3.6 统一导出
// index.ts
// packages/shared/src/index.ts
// Schemas
export * from './schemas/chat'
export * from './schemas/emotion'
export * from './schemas/memory'
export * from './schemas/auth'
// Constants
export * from './constants'
export * from './constants/errors'
// Utils
export * from './utils'
export * from './utils/auth'使用时一行 import 搞定:
// example.ts
import {
chatRequestSchema,
type ChatRequest,
type Emotion,
ERROR_CODES,
MEMORY,
formatChatTime,
hasPermission,
} from '@ai-companion/shared'4. packages/ui——共享 UI 组件
web 和 admin 都是 Next.js 应用,在 UI 层面存在可复用的组件。packages/ui 存放这些共享组件。
设计原则:只放真正共享的组件。 如果一个组件只在 web 中使用,就放在 apps/web/src/components/ 里。只有当 web 和 admin 都需要同一个组件时,才提升到 packages/ui。避免过早抽象。
// emotion-badge.tsx
// packages/ui/src/components/emotion-badge.tsx
import type { Emotion } from '@ai-companion/shared'
const EMOTION_CONFIG: Record<Emotion, { label: string; color: string }> = {
happy: { label: '开心', color: 'bg-yellow-100 text-yellow-800' },
sad: { label: '难过', color: 'bg-blue-100 text-blue-800' },
neutral: { label: '平静', color: 'bg-gray-100 text-gray-800' },
excited: { label: '兴奋', color: 'bg-orange-100 text-orange-800' },
anxious: { label: '焦虑', color: 'bg-purple-100 text-purple-800' },
calm: { label: '放松', color: 'bg-green-100 text-green-800' },
curious: { label: '好奇', color: 'bg-cyan-100 text-cyan-800' },
angry: { label: '生气', color: 'bg-red-100 text-red-800' },
tender: { label: '温柔', color: 'bg-pink-100 text-pink-800' },
playful: { label: '调皮', color: 'bg-amber-100 text-amber-800' },
}
export const EmotionBadge = ({ emotion }: { emotion: Emotion }) => {
const config = EMOTION_CONFIG[emotion]
return (
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${config.color}`}>
{config.label}
</span>
)
}// chat-bubble.tsx
// packages/ui/src/components/chat-bubble.tsx
import { formatChatTime } from '@ai-companion/shared'
import { EmotionBadge } from './emotion-badge'
import type { Emotion } from '@ai-companion/shared'
interface ChatBubbleProps {
content: string
isAI: boolean
emotion?: Emotion
createdAt: string
}
export const ChatBubble = ({ content, isAI, emotion, createdAt }: ChatBubbleProps) => {
return (
<div className={`flex ${isAI ? 'justify-start' : 'justify-end'} mb-4`}>
<div className={`max-w-100 rounded-2xl px-4 py-3 ${
isAI
? 'bg-gray-100 dark:bg-gray-800 rounded-tl-none'
: 'bg-blue-500 text-white rounded-tr-none'
}`}>
<p className="text-sm leading-relaxed">{content}</p>
<div className="mt-1.5 flex items-center gap-2">
{isAI && emotion && <EmotionBadge emotion={emotion} />}
<span className="text-xs opacity-50">{formatChatTime(createdAt)}</span>
</div>
</div>
</div>
)
}web 用 ChatBubble 渲染聊天界面,admin 用同一个 ChatBubble 渲染对话审计的详情页。同一条消息在用户端和管理端的展示完全一致,减少"管理员看到的和用户看到的不一样"的认知偏差。
5. packages/config——共享配置
配置包不包含运行时代码,只存放被其他项目 extends 的配置文件。
// tsconfig.base.json
// packages/config/tsconfig.base.json
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}// tsconfig.next.json
// packages/config/tsconfig.next.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"jsx": "preserve",
"plugins": [{ "name": "next" }],
"allowJs": true,
"noEmit": true
}
}// tsconfig.node.json
// packages/config/tsconfig.node.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"outDir": "./dist"
}
}各子项目的 tsconfig 只需一行 extends:
// tsconfig.json
// apps/web/tsconfig.json
{
"extends": "@ai-companion/config/tsconfig.next.json",
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*", "next-env.d.ts"],
"exclude": ["node_modules"]
}升级 TypeScript 版本、修改 strict 规则、添加编译选项——改 packages/config 中的一个文件,所有子项目同步生效。
6. 各子项目的独立关注点
共享逻辑之外,每个子项目还有自己独特的技术关注点。
server 的独立关注点:
- 中间件编排。 安全检查、认证、限流、日志、耗时统计——中间件的顺序和组合是服务端特有的逻辑
- CloudFlare Bindings。
wrangler.toml中配置的 KV / D1 / Vectorize / AI 绑定,以及c.env的类型声明 - 流式响应。 SSE 连接管理、LLM 输出的逐 token 转发、异常断连的处理
- 后台任务。 记忆归纳(定期将情景记忆归纳为语义记忆)、情绪衰减(定期将情绪趋向 neutral)
web 的独立关注点:
- SSR / ISR 策略。 落地页用 SSG,对话页用纯客户端渲染,用户设置页用 SSR + 客户端水合
- 客户端状态管理。 Zustand store 管理对话状态、用户信息、SSE 连接状态
- 流式消息渲染。 接收 SSE 推送,逐字追加到消息气泡中,流畅的打字机效果
- 移动端适配。 响应式布局、触摸手势、虚拟键盘弹出时的界面调整
- 离线体验。 Service Worker 缓存关键资源,弱网环境下的消息队列和重试
admin 的独立关注点:
- 权限路由守卫。 根据管理员角色动态展示菜单项、拦截无权限的页面访问
- 数据可视化。 ECharts / Recharts 渲染各类统计图表
- 表格与表单。 大量数据的分页表格、复杂配置项的表单(Prompt 编辑器、参数调优面板)
- 操作审计日志。 管理员的每个操作(修改配置、删除记忆、查看对话)都需要记录
7. 开发与部署工作流
7.1 根 package.json 与 workspace 配置
根 package.json 的核心配置:
// package.json
{
"name": "ai-companion",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"typecheck": "turbo typecheck",
"lint": "turbo lint",
"lint:fix": "turbo lint:fix",
"format": "turbo format"
},
"devDependencies": {
"turbo": "^2.0.0",
"typescript": "^5.5.0"
},
"packageManager": "[email protected]"
}7.2 日常开发流程
// dev.bash
# 克隆仓库后,一条命令安装所有依赖
yarn install
# 一条命令启动所有子项目的开发服务器
yarn dev
# Turborepo 并行启动:
# server → wrangler dev (localhost:8787)
# web → next dev (localhost:3000)
# admin → next dev (localhost:3001)
# 只启动某个子项目
yarn dev --filter=@ai-companion/web
# 全仓库类型检查
yarn typecheck
# 全仓库 lint
yarn lintturbo dev 会并行启动三个开发服务器。修改 packages/shared 中的代码后,依赖它的 server、web、admin 会自动触发热更新。修改一份 Schema,三个终端同时刷新。
7.3 部署策略
三个应用部署到不同的平台,各自有独立的部署流程:
// deploy.txt
┌─────────────────────────────────────────────────┐
│ Git Push / PR Merge │
│ │ │
│ Turborepo 检测受影响的子项目 │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────────┐ ┌──────────┐ │
│ │ server │ │ web │ │ admin │ │
│ │ │ │ │ │ │ │
│ │wrangler│ │ Vercel / │ │ Vercel / │ │
│ │ deploy │ │ CF Pages │ │ 内部部署 │ │
│ │ │ │ │ │ │ │
│ │ ↓ │ │ ↓ │ │ ↓ │ │
│ │Workers │ │ CDN Edge │ │ 内网访问 │ │
│ └────────┘ └────────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘- server →
wrangler deploy部署到 CloudFlare Workers - web → Vercel 自动部署(检测到
apps/web变更时触发)或 CloudFlare Pages - admin → 部署到内部环境或带访问控制的 Vercel 项目(
vercel --scope internal)
Turborepo 的缓存在 CI 中尤其有价值:如果一次 PR 只修改了 apps/web 的代码,apps/server 和 apps/admin 的构建会直接命中缓存,CI 时间大幅缩短。
8. 总结
这篇文章把上一篇的架构蓝图转化成了具体的工程方案:
目录结构。 apps/ 放三个可独立部署的应用,packages/ 放三个共享包(shared / ui / config)。清晰的分层让每个子项目的职责边界一目了然。
packages/shared 是核心。 Zod Schema 一份定义、三端复用;Hono RPC 类型直接 import、端到端安全;错误码和业务常量统一管理、改一处全局同步。这个包的质量直接决定了整个 Monorepo 的协作效率。
packages/ui 克制共享。 只提升真正被多个应用使用的组件,避免过早抽象。
开发体验优先。 yarn dev 一键启动、修改 Schema 三端热更新、yarn typecheck 全仓库类型校验。开发者的日常体验决定了这套架构能不能被团队真正执行下去。
从下一篇文章开始,我们将离开规划阶段,进入真正的编码实现。第一步是搭建 Hono 服务端的基础骨架——路由结构、中间件管线、以及与 CloudFlare Workers 的部署联调。