第二章 2.4:搭建 Monorepo 骨架 + 共享包 @autix/contracts

第二章 2.4:搭建 Monorepo 骨架 + 共享包 @autix/contracts

课程专题 :《AI Agents 开发实践》

章节 :第二章 搭建智能体的工程底座

小节 :2.4 搭建 Monorepo 骨架

适合人群:Java 开发者,已掌握 TypeScript 基础语法


本篇知识点

  1. 为什么前后端需要共享包
  2. Monorepo 是什么,Java 里有对应的做法吗
  3. 如何创建 @autix/contracts 共享包
  4. package.json 和 tsconfig.json 的配置要点
  5. 共享包应该放什么、不该放什么

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 应用开发。

写这个系列博客,有两个初衷:

  1. 督促自己:写不出来就是没学懂,逼自己把知识点吃透
  2. 寻找同路人:希望遇到志同道合的朋友,一起沟通、相互学习

联系方式:

  • 博客留言(欢迎交流讨论,一起成长)

系列文章导航

  • 上一篇:第二章 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)
  • 回到索引 :第二章博客索引
相关推荐
luoganttcc3 小时前
AI 编译器中的 Tiling Shape 推导与选择
人工智能
Smoothcloud润云3 小时前
PyTorch推理输入长度分桶与Padding优化实战:减少无效计算
人工智能·pytorch·python·深度学习·gpu算力
橙色云-智橙协同研发3 小时前
汽车零部件行业:如何应对主机厂的严苛审核?
大数据·运维·服务器·人工智能·安全
微三云云仔3 小时前
2026年10月微三云 GEO 优化技术拆解:AI引用机制、结构化部署与服务商评估框架
人工智能·深度学习·geo系统·微三云
努力变大白3 小时前
把“会说话的模型“变成“会干活的 Agent“
大数据·人工智能·机器学习
救救孩子把3 小时前
AI面试八股文-03_3-RAG 系统面试题
人工智能·redis·面试·agent·rag
lazy熊3 小时前
Codex 能聊天但读不到项目文件:通常是这 4 个地方没有对上
人工智能
wangchunyu1143 小时前
JDK8新特性实战(Lambda、Stream、Optional)
java·开发语言