名称: 单仓库架构 用户可调用: false 描述: 在设计单仓库结构、组织包或迁移到单仓库架构时使用,包括管理依赖和可扩展工作空间配置的架构模式。 允许工具:
- 读取
- 写入
- 编辑
- Bash
- Glob
- Grep
单仓库架构技能
概述
这个技能提供关于设计和构建单仓库(monorepo)的全面指导,包括工作空间组织、依赖管理、版本控制策略和从小型项目到企业应用程序的架构模式。
单仓库 vs 多仓库
何时选择单仓库
单仓库在以下情况中是有益的:
- 代码共享频繁:多个项目共享公共库、工具或组件
- 需要原子变更:变更跨越多个包并需要一起部署
- 统一工具:所有项目受益于一致的代码检查、测试和构建流程
- 团队协作:团队跨项目边界工作,需要查看相关代码
- 版本同步:相关包应保持版本对齐
- 大规模重构:跨项目的大规模重构是常见的
- 单一事实来源:所有代码、文档和工具集中在一个位置
何时选择多仓库
多仓库在以下情况中是有益的:
- 独立发布周期:项目按完全不同的时间表部署
- 不同技术栈:项目使用不兼容的工具或语言
- 访问控制:不同团队需要隔离访问单独的代码库
- 仓库大小顾虑:合并的代码库太大,难以高效管理
- 外部依赖:项目由不同组织维护
- 简单项目结构:单仓库工具的开销超过益处
权衡
单仓库优势:
- 简化依赖管理
- 更容易跨边界重构
- 统一的工具和标准
- 更好的代码可发现性
- 跨项目的原子提交
- 单一的CI/CD管道
单仓库挑战:
- 仓库大小增长
- CI/CD复杂性
- 构建时间管理
- Git性能在规模下
- 工具要求
- 开发者的学习曲线
仓库结构模式
基于包的结构
按技术层或包类型组织。
我的单仓库/
├── 应用/
│ ├── 网页/ # Next.js 网页应用
│ ├── 移动端/ # React Native 应用
│ └── API/ # Node.js API 服务器
├── 包/
│ ├── 用户界面/ # 共享UI组件
│ ├── 工具/ # 共享工具
│ ├── 配置/ # 共享配置
│ └── 类型/ # 共享TypeScript类型
├── 服务/
│ ├── 身份验证/ # 身份验证服务
│ ├── 支付/ # 支付处理
│ └── 通知/ # 通知服务
└── 工具链/
├── eslint配置/ # ESLint 配置
└── tsconfig/ # TypeScript 配置
最适合:技术分离、共享库焦点、平台多样性。
基于域的结构
按业务域或功能组织。
我的单仓库/
├── 域/
│ ├── 用户/
│ │ ├── API/ # 用户API
│ │ ├── 网页/ # 用户网页UI
│ │ ├── 移动端/ # 用户移动端UI
│ │ └── 共享/ # 用户共享代码
│ ├── 计费/
│ │ ├── API/
│ │ ├── 网页/
│ │ └── 共享/
│ └── 分析/
│ ├── API/
│ ├── 网页/
│ └── 共享/
└── 共享/
├── 用户界面/ # 跨域UI组件
├── 工具/ # 跨域工具
└── 配置/ # 跨域配置
最适合:域驱动设计、团队按功能拥有、微服务架构。
混合结构
结合基于包和基于域的方法。
我的单仓库/
├── 应用/
│ ├── 客户门户/ # 面向客户的应用程序
│ └── 管理仪表板/ # 管理应用程序
├── 功能/
│ ├── 身份验证/ # 身份验证功能
│ ├── 结账/ # 结账功能
│ └── 库存/ # 库存功能
├── 包/
│ ├── 用户界面/ # 共享UI库
│ ├── API客户端/ # API客户端库
│ └── 分析/ # 分析库
└── 基础设施/
├── 数据库/ # 数据库工具
├── 消息传递/ # 消息队列
└── 部署/ # 部署配置
最适合:复杂组织、平衡技术和域关注。
工作空间配置
NPM 工作空间
使用原生NPM工作空间的基本设置。
{
"name": "我的单仓库",
"private": true,
"workspaces": [
"应用/*",
"包/*"
],
"scripts": {
"开发": "npm run 开发 --workspaces",
"构建": "npm run 构建 --workspaces",
"测试": "npm run 测试 --workspaces"
},
"devDependencies": {
"typescript": "^5.3.0",
"eslint": "^8.54.0"
}
}
关键特性:
- 原生NPM支持(v7+)
- 简单配置
- 自动工作空间链接
- 共享依赖提升
- 工作空间特定命令
Yarn 工作空间
Yarn的工作空间实现,具有额外功能。
{
"name": "我的单仓库",
"private": true,
"workspaces": {
"packages": [
"应用/*",
"包/*"
],
"nohoist": [
"**/react-native",
"**/react-native/**"
]
},
"packageManager": "yarn@3.6.4"
}
使用 .yarnrc.yml 用于Yarn Berry:
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
enableGlobalCache: true
compressionLevel: mixed
关键特性:
- 快速安装
- 插件系统(Yarn Berry)
- 高级工作空间命令
- 对特定依赖不提升
- 零安装能力
PNPM 工作空间
PNPM的高效工作空间实现。
# pnpm-workspace.yaml
packages:
- '应用/*'
- '包/*'
- '服务/*'
- '!**/测试/**'
使用工作空间特定的 .npmrc:
# .npmrc
shared-workspace-lockfile=true
link-workspace-packages=true
prefer-workspace-packages=true
strict-peer-dependencies=false
auto-install-peers=true
关键特性:
- 内容可寻址存储
- 严格的 node_modules 结构
- 比NPM/Yarn更快
- 高效的磁盘空间使用
- 内置单仓库支持
Cargo 工作空间(Rust)
Rust单仓库工作空间配置。
# Cargo.toml(根)
[workspace]
members = [
"crates/核心",
"crates/API",
"crates/CLI",
]
exclude = ["archived/*"]
[workspace.package]
version = "1.0.0"
edition = "2021"
license = "MIT"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.35", features = ["full"] }
单个crate:
# crates/核心/Cargo.toml
[package]
name = "我的核心"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
serde.workspace = true
tokio.workspace = true
我的API = { path = "../API" }
依赖管理
内部包依赖
指定对其他工作空间包的依赖。
{
"name": "@我的组织/网页应用",
"version": "1.0.0",
"dependencies": {
"@我的组织/用户界面": "workspace:*",
"@我的组织/工具": "workspace:^",
"@我的组织/API客户端": "1.2.3",
"react": "^18.2.0"
}
}
工作空间协议变体:
workspace:*- 工作空间中的任何版本workspace:^- 兼容版本(语义版本号插入符号)workspace:~- 补丁级别版本(语义版本号波浪号)- 特定版本 - 确切的工作空间版本
共享依赖提升
配置依赖如何提升到根目录。
{
"name": "我的单仓库",
"workspaces": {
"packages": ["包/*"],
"nohoist": [
"**/react-native",
"**/react-native/**",
"**/@babel/**"
]
}
}
提升策略:
- 完全提升:所有公共依赖在根目录(默认)
- 选择性提升:特定包提升,其他隔离
- 不提升:每个包有隔离的依赖
- 公共提升:仅公共依赖提升
版本同步
保持相关依赖在包之间同步。
{
"name": "我的单仓库",
"private": true,
"syncpack": {
"semverGroups": [
{
"range": "",
"dependencies": ["react", "react-dom"],
"packages": ["**"]
}
],
"versionGroups": [
{
"label": "React 生态系统必须匹配",
"dependencies": ["react", "react-dom"],
"dependencyTypes": ["prod", "dev"],
"pinVersion": "18.2.0"
}
]
}
}
使用工具如 syncpack 强制执行一致性:
# 检查版本不一致
pnpm syncpack list-mismatches
# 修复版本不一致
pnpm syncpack fix-mismatches
# 更新所有版本
pnpm syncpack update
单仓库中的对等依赖
正确处理工作空间包之间的对等依赖。
{
"name": "@我的组织/用户界面",
"version": "1.0.0",
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
},
"devDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
最佳实践:
- 在共享库中声明对等依赖
- 将对等依赖作为开发依赖用于测试
- 使用
peerDependenciesMeta用于可选对等依赖 - 记录对等依赖要求
- 测试最小和最大对等版本
代码组织
共享代码模式
组织共享代码以实现最大可重用性。
包/
├── 用户界面/
│ ├── 源代码/
│ │ ├── 组件/ # 可重用组件
│ │ ├── 钩子/ # 自定义React钩子
│ │ ├── 样式/ # 共享样式
│ │ └── 索引.ts # 公共API
│ └── package.json
├── 工具/
│ ├── 源代码/
│ │ ├── 字符串/ # 字符串工具
│ │ ├── 日期/ # 日期工具
│ │ ├── 验证/ # 验证函数
│ │ └── 索引.ts # 公共API
│ └── package.json
└── 配置/
├── eslint配置/
├── tsconfig/
└── prettier配置/
共享库设计:
- 通过桶导出清晰的公共API
- 最小化外部依赖
- 良好文档化的接口
- 全面的单元测试
- 语义版本控制
- 变更日志维护
跨包类型共享
高效共享TypeScript类型。
// 包/类型/源代码/用户.ts
export interface 用户 {
id: 字符串;
电子邮件: 字符串;
名称: 字符串;
角色: 用户角色;
}
export enum 用户角色 {
管理员 = 'admin',
用户 = 'user',
访客 = 'guest',
}
export type 创建用户输入 = Omit<用户, 'id'>;
export type 更新用户输入 = Partial<创建用户输入>;
// 包/类型/package.json
{
"name": "@我的组织/类型",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./用户": {
"types": "./dist/用户.d.ts",
"default": "./dist/用户.js"
}
}
}
配置共享
跨包共享构建和工具配置。
// 包/tsconfig/基础.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
// 应用/网页/tsconfig.json
{
"extends": "@我的组织/tsconfig/基础.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"],
"references": [
{ "path": "../../包/用户界面" },
{ "path": "../../包/工具" }
]
}
构建依赖
依赖图
理解和可视化包依赖关系。
{
"name": "@我的组织/网页",
"dependencies": {
"@我的组织/用户界面": "workspace:*",
"@我的组织/API客户端": "workspace:*"
}
}
生成依赖图:
# 使用pnpm
pnpm list --depth 10 --json > 依赖.json
# 使用Nx
nx graph
# 使用自定义脚本
node 脚本/生成-依赖图.js
构建顺序优化
确保包按正确的依赖顺序构建。
{
"name": "我的单仓库",
"scripts": {
"构建": "turbo run 构建",
"构建:顺序": "pnpm -r --workspace-concurrency=1 run 构建"
}
}
Turbo管道配置:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"构建": {
"dependsOn": ["^构建"],
"outputs": ["dist/**", ".next/**"]
},
"测试": {
"dependsOn": ["构建"],
"outputs": ["coverage/**"]
}
}
}
依赖解析:
^构建- 先构建依赖dependsOn- 显式任务依赖- 拓扑排序以确保正确顺序
- 安全时并行执行
循环依赖检测
预防和检测循环依赖。
// 脚本/检查-循环依赖.js
import madge from 'madge';
async function 检查循环依赖() {
const result = await madge('src', {
fileExtensions: ['ts', 'tsx'],
detectiveOptions: {
ts: { skipTypeImports: true }
}
});
const circular = result.circular();
if (circular.length > 0) {
console.error('检测到循环依赖:');
circular.forEach(cycle => {
console.error(cycle.join(' -> '));
});
process.exit(1);
}
}
检查循环依赖();
添加到CI管道:
# .github/workflows/ci.yml
- name: 检查循环依赖
run: pnpm 检查:循环
版本控制策略
独立版本控制
每个包有自己的版本,独立发布。
{
"name": "@我的组织/用户界面",
"version": "2.1.0"
}
{
"name": "@我的组织/工具",
"version": "1.5.3"
}
优势:
- 细粒度版本控制
- 独立发布周期
- 清晰的包成熟度
- 每个包的语义版本控制
适用场景:
- 包有不同的稳定性级别
- 发布频率差异显著
- 包用于不同目的
固定/锁定版本控制
所有包共享相同的版本号。
{
"name": "@我的组织/用户界面",
"version": "3.2.0"
}
{
"name": "@我的组织/工具",
"version": "3.2.0"
}
优势:
- 简化版本管理
- 清晰的发布协调
- 更容易跟踪兼容性
- 统一的变更日志
适用场景:
- 包紧密耦合
- 所有包一起发布
- 单个产品有多个包
- 版本同步是关键
单仓库中的语义版本控制
对工作空间包应用语义版本控制原则。
重大变更(主要):
- 更改公共API签名
- 移除导出函数
- 显著更改函数行为
- 更新有重大变更的对等依赖
新功能(次要):
- 添加新导出
- 添加可选参数
- 增强现有功能
- 添加新可选功能
错误修复(补丁):
- 修复错误而不更改API
- 更新文档
- 重构内部实现
- 更新依赖(非重大变更)
最佳实践
1. 清晰的包边界
为每个包定义明确的边界和职责。
实现:
- 记录包的用途和范围
- 明确定义公共API
- 使用桶导出(
index.ts) - 最小化跨包耦合
- 定期审查包边界
示例:
// 包/用户界面/源代码/索引.ts - 清晰的公共API
export { 按钮 } from './组件/按钮';
export { 输入 } from './组件/输入';
export type { 按钮属性, 输入属性 } from './类型';
// 不导出内部实现细节
// ./组件/按钮/按钮样式.ts
// ./工具/内部辅助工具.ts
2. 最小化包间耦合
减少包间的依赖以保持灵活性。
实现:
- 使用依赖注入
- 优先组合而非继承
- 定义清晰的接口
- 避免深度依赖链
- 使用事件/消息传递实现松散耦合
示例:
// 通过接口实现松散耦合
interface 日志记录器 {
日志(消息: 字符串): void;
}
class 支付服务 {
constructor(private 日志记录器: 日志记录器) {}
处理支付(金额: number) {
this.日志记录器.日志(`处理支付: ${金额}`);
// 实现
}
}
3. 共享工具配置
在所有包中维护一致的工具配置。
实现:
- 创建共享配置包
- 扩展基础配置
- 使用工作空间继承
- 记录配置决策
- 自动化配置验证
示例:
// 包/eslint配置/索引.json
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-console": "warn",
"@typescript-eslint/no-unused-vars": "error"
}
}
4. 一致的命名约定
跨包使用可预测的命名模式。
实现:
- 为所有包添加作用域(
@组织/包名称) - 使用短横线命名法命名包
- 为相关包使用一致的前缀
- 遵循语言/生态系统约定
- 记录命名标准
示例:
@我的组织/网页应用
@我的组织/移动端应用
@我的组织/用户界面组件
@我的组织/API客户端
@我的组织/工具日期
@我的组织/工具字符串
@我的组织/配置eslint
@我的组织/配置typescript
5. 文档标准
维护所有包的全面文档。
实现:
- 每个包都有的README
- API文档
- 使用示例
- 迁移指南
- 贡献指南
示例:
# @我的组织/用户界面
MyOrg应用的React组件库。
## 安装
`pnpm add @我的组织/用户界面`
## 使用
import { 按钮 } from '@我的组织/用户界面';
<按钮 onClick={处理点击}>点击我</按钮>
## API参考
查看[API.md](./API.md)获取详细文档。
6. 包所有权
为包分配明确的所有权和责任。
实现:
- CODEOWNERS文件
- 包维护者文档
- 变更审查流程
- 沟通渠道
- 所有权轮换计划
示例:
# CODEOWNERS
/包/用户界面/ @前端团队
/包/API客户端/ @API团队
/包/身份验证/ @安全团队
/服务/ @后端团队
7. API合约
定义和维护包间清晰的API合约。
实现:
- TypeScript接口
- OpenAPI规范
- JSON模式
- 合约测试
- 版本兼容性矩阵
示例:
// 包/API客户端/源代码/合约.ts
/**
* 用户API合约
* @version 1.0.0
*/
export interface 用户API {
获取用户(id: 字符串): Promise<用户>;
创建用户(data: 创建用户输入): Promise<用户>;
更新用户(id: 字符串, data: 更新用户输入): Promise<用户>;
删除用户(id: 字符串): Promise<void>;
}
8. 迁移策略
计划包更新和重大变更。
实现:
- 弃用警告
- 迁移指南
- 兼容层
- 自动化迁移(代码修改工具)
- 版本升级路径
示例:
// 弃用并提供迁移路径
/**
* @deprecated 使用 `获取用户` 代替
* 此函数将在v3.0.0中移除
*/
export function 获取用户旧(id: 字符串): Promise<用户> {
console.warn('获取用户旧已被弃用,请使用获取用户代替');
return 获取用户(id);
}
9. 安全边界
在包之间保持安全隔离。
实现:
- 分离敏感包
- 访问控制策略
- 按包进行安全扫描
- 依赖审计
- 秘密管理
示例:
{
"scripts": {
"安全:审计": "pnpm audit --audit-level=high",
"安全:检查": "pnpm dlx audit-ci --high"
}
}
10. 测试隔离
确保测试没有意外的依赖。
实现:
- 包级测试配置
- 模拟工作空间依赖
- 集成测试套件
- CI测试隔离
- 测试数据管理
示例:
// 包/用户界面/源代码/__测试__/按钮.测试.tsx
import { render, screen } from '@testing-library/react';
import { 按钮 } from '../按钮';
// 测试独立于其他包
describe('按钮', () => {
it('正确渲染', () => {
render(<按钮>点击我</按钮>);
expect(screen.getByText('点击我')).toBeInTheDocument();
});
});
常见陷阱
1. 包间紧密耦合
在包之间创建过多的依赖关系。
症状:
- 变更需要更新多个包
- 难以提取或移动包
- 长依赖链
- 循环依赖
解决方案:
- 使用依赖注入
- 定义清晰接口
- 实现事件驱动通信
- 定期重构以减少耦合
2. 包职责不明确
包的用途重叠或未定义。
症状:
- 功能重复
- 不确定在哪里添加功能
- 实现不一致
- 代码重复
解决方案:
- 清晰记录包的用途
- 单一职责原则
- 定期架构审查
- 重构以澄清边界
3. 不一致的依赖版本
不同包中使用相同依赖的不同版本。
症状:
- 构建错误
- 运行时冲突
- 对等依赖警告
- 捆绑包大小膨胀
解决方案:
- 使用工作空间协议
- 实现syncpack或类似工具
- 集中版本管理
- CI检查一致性
4. 构建优化差
未充分利用缓存和增量构建。
症状:
- 构建时间慢
- 重新构建未更改的包
- CI管道运行时间长
- 开发者挫败感
解决方案:
- 实现构建缓存(Turborepo, Nx)
- 使用受影响分析
- 配置增量构建
- 优化构建管道
5. 缺少文档
包和API的文档不足。
症状:
- 频繁询问用法问题
- 错误的用法模式
- 困难的上手
- 知识孤岛
解决方案:
- 每个包都有README
- API文档生成
- 使用示例
- 上手指南
6. 单仓库中的整体思维
将单仓库视为单一大型应用程序。
症状:
- 共享全局状态
- 代码紧密耦合
- 难以提取包
- 无清晰的包边界
解决方案:
- 设计包作为独立单元
- 最小化共享全局状态
- 清晰的关注点分离
- 定期边界审查
7. 过度共享代码
共享应保持私有的代码。
症状:
- 暴露实现细节
- 脆弱的依赖关系
- 难以重构
- 版本管理复杂性
解决方案:
- 使用桶导出作为公共API
- 保持内部私有
- 记录公共与私有
- 定期审查导出
8. 忽略包边界
从包内部导入而不是公共API。
症状:
- 脆弱的导入
- 重构时的重大变更
- 不清晰的依赖关系
- 类型错误
解决方案:
- 仅从包根目录导入
- 配置代码检查规则
- 使用TypeScript项目引用
- 代码审查强制执行
9. 无版本控制策略
缺乏清晰的版本控制方法。
症状:
- 兼容性不明确
- 无警告的重大变更
- 困难的回滚
- 消费者困惑
解决方案:
- 选择固定或独立版本控制
- 使用changesets或约定提交
- 语义版本控制纪律
- 自动化版本管理
10. 复杂依赖链
深度或循环依赖关系。
症状:
- 难以理解流程
- 构建顺序问题
- 循环依赖错误
- 维护困难
解决方案:
- 可视化依赖图
- 重构以减少深度
- 打破循环依赖
- 使用依赖注入
何时使用此技能
在以下情况下应用单仓库架构原则:
- 设计新的单仓库 - 设置结构和组织
- 重构现有仓库 - 改进架构和组织
- 迁移到单仓库 - 从多仓库移动到单仓库
- 扩展单仓库 - 从小型增长到大型单仓库
- 组织包 - 决定包边界和结构
- 管理依赖 - 处理内部和外部依赖
- 建立模式 - 创建架构标准
- 审查架构 - 评估现有单仓库结构
- 故障排除问题 - 解决依赖或组织问题
- 计划重构 - 重构包或依赖关系
资源
- Monorepo.tools - 单仓库工具和模式的全面比较
- Turborepo手册 - Turborepo架构的最佳实践
- Nx单仓库指南 - 架构概念和模式
- PNPM工作空间 - PNPM工作空间功能的文档
- Yarn工作空间 - Yarn工作空间实施指南
- Lerna文档 - 多包仓库管理
- Bazel文档 - 大型单仓库的构建和测试系统
- Rush文档 - 用于JavaScript的可扩展单仓库管理器