第二章 2.4:搭建 Monorepo 骨架 + 共享包 @autix/contracts
课程专题 :《AI Agents 开发实践》
章节 :第二章 搭建智能体的工程底座
小节 :2.4 搭建 Monorepo 骨架
适合人群:Java 开发者,已掌握 TypeScript 基础语法
本篇知识点
- 为什么前后端需要共享包
- Monorepo 是什么,Java 里有对应的做法吗
- 如何创建
@autix/contracts共享包 package.json和tsconfig.json的配置要点- 共享包应该放什么、不该放什么
1. 共享包的设计理念
1.1 问题:前后端重复定义类型
想象一下这个场景 ------ 你正在做一个 AI 对话应用:
前端(Next.js) 后端(Node.js)
│ │
│ ChatRequest.json ──►│
│ │
│◄── ChatResponse.json │
│ │
传统做法 :前端自己定义一遍 ChatRequest,后端也自己定义一遍 ChatRequest。
如果有一天 ChatRequest 加了一个 model 字段,你需要:
- 去前端代码里找到 DTO,加上
model?: string - 去后端代码里找到 DTO,加上
model?: string - 如果只改了一边,类型就不一致了,运行时才报错
这就是我们要解决的问题。
1.2 Java 对照:Maven 多模块里的 commons
如果你用 Maven 做过多模块项目,一定见过这种结构:
my-platform/
├── pom.xml # 父 POM
├── commons-dto/ # 共享 DTO 模块
│ └── ChatRequest.java
├── api-service/ # 后端服务
│ └── 依赖 commons-dto
└── web-admin/ # 前端(如果用 Java 写的话)
└── 依赖 commons-dto
commons-dto 模块就是前后端共用的类型定义,只写一次,各处引用。
TypeScript Monorepo 的共享包就是这个思路,只不过更轻量。
1.3 Autix 的做法
Autix 把整个生产项目放在一个 Monorepo 里,核心结构如下:
autix/
├── packages/
│ ├── contracts/ # ← 就是我们这篇要搭的!
│ ├── domain/ # 业务领域层
│ ├── services/ # API 调用层
│ └── clients/ # 前端应用
├── apps/ # 各子应用
└── package.json # 根配置(定义 workspace)
packages/contracts 或 packages/domain 里的类型,前后端都直接引用 ------ 改一次,两边同步生效。
1.4 设计原则
共享包要保持"纯",记住三句话:
| 原则 | 说明 |
|---|---|
| 零外部依赖 | 不 import 任何 npm 包(除了 TypeScript 本身) |
| 类型稳定 | 字段一旦公开,不随便改名删字段 |
| 只放纯类型和常量 | 不放 API 调用、不放组件、不放数据库操作 |
2. 创建目录结构
我们开始动手。先从根目录创建 contracts 子包:
bash
# 进入 ch02 目录(Monorepo 根)
cd ch02
# 创建 contracts 包目录
mkdir -p packages/contracts/src
最终结构:
ch02/
├── package.json # 根 workspace 配置(已存在)
├── tsconfig.base.json # 根 TypeScript 配置(已存在)
└── packages/
└── contracts/ # 新建的共享包
├── package.json
├── tsconfig.json
└── src/
└── index.ts
3. package.json 完整代码
json
{
"name": "@autix/contracts",
"version": "0.1.0",
"private": true,
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"build": "tsc --project .",
"typecheck": "tsc --project . --noEmit"
}
}
3.1 关键字段解释
| 字段 | 值 | 说明 |
|---|---|---|
name |
@autix/contracts |
scope 包名。@autix 是 scope,contracts 是包名 |
private |
true |
不发布到 npm,仅在本 Monorepo 内部使用 |
main |
src/index.ts |
入口文件。注意:这里直接指向 TS 源码,不编译 |
types |
src/index.ts |
类型声明文件,同样直接指向源码 |
3.2 Java 对照
scope 包名 @autix/contracts 对应 Java 的全限定类名:
typescript
// TypeScript import
import type { ChatMessage } from '@autix/contracts';
java
// Java import
import com.example.contracts.ChatMessage;
本质一样:都是用一个"命名空间"来组织代码,避免同名冲突。
3.3 为什么 main 指向 .ts 而不是编译后的 .js?
这是 Monorepo 内部包的一个常见做法:
- 子项目直接
import其他子包的.ts源码 - 根目录统一编译(或运行时用
tsx直接执行 TS) - 省去了"改一个子包 → 必须先 build → 其他包才用到最新代码"的麻烦
如果你要把这个包发布到 npm 给外部用,那 main 就应该指向 dist/index.js,types 指向 dist/index.d.ts。但在 Monorepo 内部,直接引用源码更方便。
4. tsconfig.json 完整代码
json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"declaration": true
},
"include": ["src/**/*"]
}
4.1 关键配置
| 配置 | 值 | 说明 |
|---|---|---|
extends |
../../tsconfig.base.json |
继承根目录的共享配置,保持一致 |
outDir |
./dist |
编译输出目录 |
declaration |
true |
生成.d.ts 类型声明文件,其他子包可以引用 |
include |
["src/**/*"] |
只编译 src 目录下的文件 |
4.2 Java 对照
extends 就像 Maven 里子模块的 <parent> 标签:
xml
<!-- Maven 子模块 -->
<parent>
<groupId>com.example</groupId>
<artifactId>my-platform</artifactId>
<version>1.0.0</version>
</parent>
子模块自动继承父 POM 的依赖版本、插件配置,不用每个模块重复写一遍。
5. src/index.ts ------ 共享类型的核心
这个文件是整个共享包的灵魂,前后端所有类型都从这里导出。
typescript
// ============================================================
// 消息角色
// ============================================================
export type MessageRole = 'system' | 'user' | 'assistant';
// ============================================================
// 基础消息类型
// ============================================================
/** 一条对话消息 */
export interface ChatMessage {
role: MessageRole;
content: string;
}
// ============================================================
// 模型调用:请求与响应
// ============================================================
/** 模型调用请求(前后端约定:前端发这个给后端) */
export interface ChatRequest {
messages: ChatMessage[];
system?: string;
temperature?: number;
}
/** 模型调用响应 */
export interface ChatResponse {
content: string;
inputTokens?: number;
outputTokens?: number;
}
// ============================================================
// 服务健康检查
// ============================================================
/** 健康检查响应 */
export interface HealthResponse {
status: 'ok';
version: string;
timestamp: string;
}
// ============================================================
// 常量
// ============================================================
/** 应用名称(前后端展示用) */
export const APP_NAME = 'Autix Ch02';
5.1 逐行解读
| 导出项 | 类型 | 说明 |
|---|---|---|
MessageRole |
type |
字符串联合类型,对应 Java 的enum |
ChatMessage |
interface |
一条消息的数据结构 |
ChatRequest |
interface |
前端 → 后端的请求体 |
ChatResponse |
interface |
后端 → 前端的响应体 |
HealthResponse |
interface |
/api/health 接口的返回类型 |
APP_NAME |
const |
常量,前后端都可能用到 |
5.2 Java 对照代码
java
// ============================================================
// Java 传统做法:前后端各定义一遍 DTO
// ============================================================
// 后端:ChatRequest.java(在 api 模块)
package com.example.api.dto;
public class ChatRequest {
private List<ChatMessage> messages;
private String system;
private Double temperature;
// getter/setter/构造器...
}
// 前端:如果是另一个 Java 项目(比如用了 Vaadin)
// ChatRequest.java(在 web 模块,可能包名不同、字段略有差异)
// 问题:改一个字段要改两处,容易忘记!
// ============================================================
// Monorepo 共享包做法:定义一次,各处引用
// ============================================================
// 相当于 Java 把 DTO 放到独立的 commons 模块
// api 模块和 web 模块都依赖 commons-dto
// 改一个地方,两边自动同步 ✅
6. 验证共享包是否可用
6.1 安装依赖
Monorepo 的包管理器会自动识别 package.json 里的 workspaces 配置,把 packages/* 下的所有子包链接起来。
bash
# 从 ch02 根目录执行
cd ch02
bun install
6.2 构建 contracts
bash
# 只构建 contracts 这个子包
bun run build --filter @autix/contracts
如果一切正常,你会看到类似输出:
$ tsc --project .
6.3 类型检查
bash
# 只检查 contracts 的类型,不生成文件
bun run typecheck --filter @autix/contracts
没有输出就代表没有类型错误。
6.4 其他子包如何引用
假设我们后面会创建 packages/api 这个后端子包,它的 package.json 里只需要加一行依赖:
json
{
"dependencies": {
"@autix/contracts": "workspace:*"
}
}
然后就可以在代码里直接 import:
typescript
import type { ChatRequest, ChatResponse } from '@autix/contracts';
Java 对照:
xml
<!-- Maven 依赖 commons-dto -->
<dependency>
<groupId>com.example</groupId>
<artifactId>commons-dto</artifactId>
<version>1.0.0</version>
</dependency>
7. 共享包的边界 ------ 什么该放、什么不该放
这是新手最容易踩坑的地方。记住一张表:
| 内容 | 放不放 | 原因 |
|---|---|---|
interface ChatRequest(类型定义) |
✅ 放 | 纯类型,前后端共用 |
| `type MessageRole = 'user' | 'assistant'`(联合类型) | ✅ 放 |
const APP_NAME = 'Autix'(常量) |
✅ 放 | 纯值,没有副作用 |
export function formatTokens()(纯函数) |
✅ 放 | 只要不依赖外部服务就可以 |
async function callApi()(API 调用函数) |
❌ 不放 | 属于 services 包,会依赖 fetch/axios |
export function useChat()(React Hook) |
❌ 不放 | 属于前端,会依赖 react |
prisma.chat.findMany()(数据库操作) |
❌ 不放 | 属于后端,会依赖 Prisma/数据库 |
7.1 判断标准
问自己一个问题:"如果我在一个纯净的 Node.js 脚本里 import 这个模块,它能正常运行吗?"
- 能 → 可以放 contracts
- 不能(报错缺 react、缺 axios、缺 prisma)→ 放到对应的业务包里
8. 我们做了什么
来回顾一下这篇博客的产出:
| 步骤 | 内容 |
|---|---|
| 目录结构 | 创建了packages/contracts/src |
| package.json | 配置了@autix/contracts scope 包名、构建脚本 |
| tsconfig.json | 继承根配置,开启 declaration 输出 |
| src/index.ts | 定义了 5 个核心类型 + 1 个常量 |
| 验证 | 跑通了 install、build、typecheck |
最终文件一览
packages/contracts/
├── package.json # scope 包配置
├── tsconfig.json # 继承根配置
└── src/
└── index.ts # 5 个类型 + 1 个常量
9. 下一篇预告
第二章 2.5:初始化 Next.js 前端应用
下一篇我们会在 Monorepo 里加入 Next.js 前端子包,并演示它如何直接引用 @autix/contracts 里的类型。你会看到:
- 前端组件用
ChatRequest接口约束表单数据 - API 路由用
ChatResponse接口约束返回值 - 真正做到"一个类型定义,前后端共享"
关于作者
我是 Java 开发者,正在从零学习 TypeScript 和 AI 应用开发。
写这个系列博客,有两个初衷:
- 督促自己:写不出来就是没学懂,逼自己把知识点吃透
- 寻找同路人:希望遇到志同道合的朋友,一起沟通、相互学习
联系方式:
- 博客留言(欢迎交流讨论,一起成长)
系列文章导航
- 上一篇:第二章 2.1-2.3:Monorepo + 技术选型 + Bun 包管理器入门(./01-2.1-2.3 Monorepo + 技术选型 + Bun 包管理器入门.md)
- 本篇:第二章 2.4:搭建 Monorepo 骨架 + 共享包 @autix/contracts
- 下一篇:第二章 2.5:初始化 Next.js 客户端(./03-2.5 初始化 Next.js 客户端 ------ TypeScript 全栈的前端选择.md)
- 回到索引 :第二章博客索引