概述
Monorepo(Monolithic Repository)指的是把多个包(项目)的代码,统一放在同一个代码仓库里进行管理的一种仓库组织方式。适合多个项目强相关且有大量共享代码的中大型项目。使用 Monorepo 管理项目的好处非常明显,它不用发 npm 包,不用来回同步版本,改完公共库,所有项目立即可用,使得共享代码更简单。 Monorepo 是存放多个包的仓库,包之间的通信方式至关重要,接下来我们以一个交易项目为例,一步一步了解 Monorepo 各包间正确的通信方式。
交易平台项目简述
该项目是一个给用户提供股票交易服务的平台。项目包括交易(Trade)、资产(Portfolios)、行情(Quotes)、市场(Market)等模块。项目还有提供公共方法、UI、数据等公共模块。
交易平台各包之间的通信问题
包之间的依赖关系不合理
包之间出现循环依赖问题,业务包依赖业务包。
ts
├── @repo/shared ←→ @repo/trade (循环依赖)
├── @repo/account → @repo/trade (不合理)
├── @repo/quote → @repo/trade (不合理)
├── @repo/portfolios → @repo/trade (不合理)
└── ...
封装性破坏:直接访问包内部实现
直接导入其他包的内部路径
ts
// packages/shared/utils/order.ts
import { EOrderVerify } from '@repo/trade/data';
import { canFillSearch } from '@repo/trade/utils';
// packages/shared/reports/modules/trade.ts
import { ECondOrderVerifyParaKey } from '@pkg/trade/data/sensors';
import { getIsEntrustType } from '@pkg/trade/utils/type';
import { useIndexSession, useNightTradeSession } from '@repo/trade/utils/hooks';
紧耦合:直接依赖应用层 stores
ts
// packages/shared/utils/order.ts
import { useConfigStore } from '@/stores/config';
import { useMarketStore } from '@/stores/market.ts';
import { usePasswordStore } from '@/stores/password';
import { useTradeStore } from '@/stores/trade';
import { useUserStore } from '@/stores/user';
// packages/trade/utils/verify.ts
import { EStatementType } from '@/stores/quote-statement.ts';
// packages/portfolios/components/orders/index.vue
import { useMarketStore } from '@/stores/market';
import { useOrderStore } from '@/stores/order';
项目包间正确通信方式
一、依赖层次原则
1.1 依赖层次结构
ts
┌─────────────────────────────────────┐
│ apps/web (应用层) │
│ 依赖所有业务包,负责组装和路由 │
└─────────────────────────────────────┘
│
├─────────────────────────────────────┐
│ │
┌─────────────▼──────────────┐ ┌──────────────────▼──────────────┐
│ 业务包 (Business) │ │ 基础包 (Foundation) │
│ - trade │ │ - shared (不依赖其他业务包) │
│ - account │ │ - data-center │
│ - quote │ │ - ui │
│ - portfolios │ │ │
│ - company │ │ │
│ - market │ │ │
│ - ... │ │ │
└────────────────────────────┘ └──────────────────────────────────┘
1.2 依赖规则
✅ 允许的依赖方向
- 应用层 → 所有包
- 业务包 → 基础包(shared, data-center, ui)
- 基础包 → 只依赖更底层的基础包或外部库
- shared 包 → 不依赖任何业务包
❌ 禁止的依赖方向
- 基础包 → 业务包(如 shared → trade)
- 业务包 → 业务包(如 account → trade)
- 循环依赖(如 shared ↔ trade)
1.3 依赖层次示例
ts
// ✅ 正确:业务包依赖基础包
// packages/trade/package.json
{
"dependencies": {
"@repo/shared": "workspace: " ,
"@repo/data-center" : "workspace: ",
"@repo/ui": "workspace:*"
}
}
// ❌ 错误:基础包依赖业务包
// packages/shared/package.json
{
"dependencies": {
"@repo/trade": "workspace:*" // ❌ 不应该依赖业务包
}
}
二、通信模式
2.1 事件通信模式(Event Bus)
适用场景:
- 包间的松耦合通信
- 一对多的通知场景
- 异步事件通知
实现方式:
- 定义事件类型(在 shared 包中)
ts
// packages/shared/events/index.ts
import mitt from 'mitt';
export const enum EEventBusType {
ORDER_SUCCESS = 'orderSuccess',
USER_LOGIN_SUCCESS = 'userLoginSuccess',
// ...
}
export type TEvents = {
[EEventBusType.ORDER_SUCCESS]?: {
orderId: string;
orderType: string;
};
[EEventBusType.USER_LOGIN_SUCCESS]?: unknown;
};
export const eventBus = mitt<TEvents>();
export default eventBus;
- 发送事件(在 trade 包中)
ts
// packages/trade/order/index.vue
import eventBus, { EEventBusType } from '@repo/shared/events';
// 下单成功后发送事件
const handleOrderSuccess = () => {
eventBus.emit(EEventBusType.ORDER_SUCCESS, {
orderId: '123',
orderType: 'limit'
});
};
- 监听事件(在 portfolios 包中)
ts
// packages/portfolios/components/orders/index.vue
import eventBus, { EEventBusType } from '@repo/shared/events';
import { onMounted, onUnmounted } from 'vue';
const orderChangeCallBack = (data: any) => {
// 处理订单变化
refreshOrderList();
};
onMounted(() => {
eventBus.on(EEventBusType.ORDER_SUCCESS, orderChangeCallBack);
});
onUnmounted(() => {
eventBus.off(EEventBusType.ORDER_SUCCESS, orderChangeCallBack);
});
优点:
- 解耦:发送者和接收者不需要直接依赖
- 灵活:可以动态添加/移除监听器
- 支持一对多通信
注意事项:
- 事件类型定义应在 shared 包中,确保类型安全
- 及时清理事件监听器,避免内存泄漏
- 事件名称应清晰明确,避免命名冲突
2.2 服务模式(Singleton Service)
适用场景:
- 提供统一的数据访问接口
- 跨包共享服务实例
- 需要单例模式的服务
实现方式:
- 定义服务(在 data-center 包中)
ts
// packages/data-center/core/index.ts
import { Singleton } from '@repo/shared/utils/singleton';
export class DataCenter extends Singleton {
static tag = 'DataCenter';
async getStockInfo(code: string) {
// 获取股票信息
}
async getQuotes(codes: string[]) {
// 获取行情数据
}
}
// 2. 导出服务
export { DataCenter } from './core';
- 使用服务(在任何包中)
ts
// packages/trade/utils/stock.ts
import { DataCenter } from '@repo/data-center';
const getStockData = async (code: string) => {
const dataCenter = DataCenter.getInstance();
return await dataCenter.getStockInfo(code);
};
优点:
- 统一接口:所有包通过相同的接口访问服务
- 单例保证:确保全局只有一个实例
- 易于测试:可以 mock 服务接口
注意事项:
-
服务接口应稳定,避免频繁变更
-
服务应在 shared 或 data-center 等基础包中
-
避免在服务中直接依赖业务包
2.3 共享类型和接口
适用场景:
- 包间传递数据
- 定义公共接口
- 类型约束
实现方式:
- 定义共享类型(在 shared 包中)
ts
// packages/shared/types/order.ts
export interface IOrderForm {
stockCode: string;
stockName: string;
entrustType: EEntrustType;
// ...
}
export interface IMaxAvailableAsset {
cashAvailable: number;
marginAvailable: number;
// ...
}
- 使用共享类型(在 trade 包中)
ts
// packages/trade/order/index.vue
import type { IOrderForm } from '@repo/shared/types/order';
const form: IOrderForm = {
stockCode: 'AAPL',
stockName: 'Apple Inc.',
// ...
};
- 使用共享类型(在 portfolios 包中)
ts
// packages/portfolios/components/orders/index.vue
import type { IOrderForm } from '@repo/shared/types/order';
const displayOrder = (order: IOrderForm) => {
// 使用共享类型
};
优点:
- 类型安全:TypeScript 提供编译时类型检查
- 一致性:确保数据结构一致
- 可维护性:类型定义集中管理
注意事项:
- 共享类型应在 shared 包中定义
- 避免在业务包中定义共享类型
- 类型定义应向后兼容
2.4 公共 API 导出模式
适用场景:
- 包需要对外暴露功能
- 隐藏内部实现细节
- 提供稳定的接口
实现方式:
- 建立公共 API(在 trade 包中)
ts
// packages/trade/index.ts
// 导出常量
export { EOrderVerify, EOrderErrorCode } from './data/constant';
// 导出工具函数
export { canFillSearch, getOpenAccountUrl } from './utils';
// 导出类型
export type { IOrderForm, IAttachedOrder } from './types';
// 导出组件(如果需要)
export { default as OrderPanel } from './order/index.vue';
- 使用公共API(在其他包中)
ts
// ✅ 正确:通过公共API导入
import { getOpenAccountUrl } from '@repo/trade';
// ❌ 错误:直接访问内部实现
import { getOpenAccountUrl } from '@repo/trade/utils';
优点:
- 封装性:隐藏内部实现
- 稳定性:公共API相对稳定
- 可维护性:内部重构不影响外部使用
注意事项:
-
每个包都应建立 index.ts 作为公共 API 入口
-
只导出需要对外暴露的功能
-
避免导出内部实现细节
2.5 依赖注入模式
适用场景:
- 需要解耦 stores 依赖
- 提高可测试性
- 支持不同的实现
实现方式:
- 定义接口(在 shared 包中)
ts
// packages/shared/interfaces/stores.ts
export interface IUserStore {
logined: boolean;
customerInfo: any;
goLogin: () => void;
}
export interface IConfigStore {
urlsConfig: {
acOpen: string;
};
}
- 通过参数注入(在 shared 包中)
ts
// packages/shared/utils/order.ts
import type { IUserStore, IConfigStore } from '@repo/shared/interfaces/stores';
export const preVerify = async (
userStore: IUserStore,
configStore: IConfigStore
) => {
if (!userStore.logined) {
userStore.goLogin();
return false;
}
// ...
};
- 在应用层注入依赖(在 apps/web 中)
ts
// apps/web/src/views/trade/index.vue
import { preVerify } from '@repo/shared/utils/order';
import { useUserStore } from '@/stores/user';
import { useConfigStore } from '@/stores/config';
const userStore = useUserStore();
const configStore = useConfigStore();
const handlePreVerify = async () => {
await preVerify(userStore, configStore);
};
优点:
- 解耦:包不直接依赖 stores
- 可测试:可以轻松 mock 依赖
- 灵活:支持不同的实现
注意事项:
-
接口定义应在 shared 包中
-
依赖注入会增加调用复杂度
-
适合复杂场景,简单场景可直接使用
2.6 API Linker 模式
适用场景:
- 通过 URL 参数调用 API
- 跨页面通信
- 外部系统集成
实现方式:
- 注册 API(在 trade 包中)
ts
// packages/trade/utils/api-linker.ts
import apiLinker from '@repo/shared/app/router/api-linker';
apiLinker.registerLinkerApi({
openTradePanel: async (params: { stockCode: string }) => {
// 打开交易面板
},
submitOrder: async (params: IOrderForm) => {
// 提交订单
}
});
- 创建 API 链接(在其他包中)
ts
// packages/company/components/stock-card/index.vue
import apiLinker from '@repo/shared/app/router/api-linker';
const openTrade = (stockCode: string) => {
const url = apiLinker.createApiLink('/trade', {
apiNames: ['openTradePanel'],
apiOptions: {
openTradePanel: { stockCode }
}
});
window.open(url);
};
优点:
- 跨页面通信
- 支持外部系统集成
- 解耦页面间依赖
三、最佳实践
3.1 包内导入规则
✅ 正确:包内使用相对路径
ts
// packages/trade/utils/verify.ts
import { EOrderVerify } from '../data/constant';
import { canFillSearch } from './common';
❌ 错误:包内使用包名路径
ts
import { EOrderVerify } from '@repo/trade/data/constant';
// 讨论:是否可以使用 @pkg/trade
import { EOrderVerify } from ' @pkg/trade/data/constant';
✅ 正确:跨包使用包名路径
javascript
// packages/account/index.vue
import { getOpenAccountUrl } from '@repo/trade';
import { eventBus } from '@repo/shared/events';
3.2 类型定义规则
✅ 正确:共享类型在 shared 包中
ts
// packages/shared/types/order.ts
export interface IOrderForm {
// ...
}
✅ 正确:业务特定类型在业务包中
ts
// packages/trade/types/condition.ts
export interface IConditionOrder {
// ...
}
3.3 常量定义规则
✅ 正确:通用常量在 shared 包中
ts
// packages/shared/constant/order.ts
export enum EEntrustType {
LIMIT = 1,
MARKET = 2,
}
✅ 正确:业务特定常量在业务包中
ts
// packages/trade/data/constant.ts
export enum EOrderVerify {
NOT_LOGIN = 'not_login',
// ...
}
3.4 工具函数规则
✅ 正确:通用工具函数在 shared 包中
ts
// packages/shared/utils/order.ts
export const isTrue = (flag: any) => {
// 通用逻辑
};
✅ 正确:业务特定工具函数在业务包中
ts
// packages/trade/utils/verify.ts
export const verifyOrder = (order: IOrderForm) => {
// 业务特定逻辑
};
四、通信模式选择指南
| 场景 | 推荐模式 | 示例 |
|---|---|---|
| 通知其他包某个事件发生 | 事件通信 | 订单成功、登录成功 |
| 获取共享数据 | 服务模式 | 获取股票信息、行情数据 |
| 传递数据 | 共享类型 | 订单表单、用户信息 |
| 调用其他包功能 | 公共 API | 工具函数、组件 |
| 需要解耦 stores | 依赖注入 | 验证函数、工具函数 |
| 跨页面通信 | API Linker | 打开交易面板 |
五、重构建议
5.1 解决循环依赖
步骤1:识别共享内容
ts
// 找出 shared 包中使用的 trade 包内容
// packages/shared/utils/order.ts
import { EOrderVerify } from '@repo/trade/data'; // 需要移到shared
import { canFillSearch } from '@repo/trade/utils'; // 需要移到shared
步骤2:迁移到 shared 包
ts
// packages/shared/constant/order.ts
export enum EOrderVerify {
NOT_LOGIN = 'not_login',
// ...
}
// packages/shared/utils/order.ts
export const canFillSearch = async (params: any) => {
// 实现逻辑
};
步骤3:更新引用
ts
// packages/trade/utils/verify.ts
// 从 shared 包导入
import { EOrderVerify } from '@repo/shared/constant/order';
import { canFillSearch } from '@repo/shared/utils/order';
步骤4:移除依赖
ts
// packages/shared/package.json
{
"dependencies": {
// 移除 "@repo/trade": "workspace:*"
}
5.2 建立公共 API
步骤1:在各包的根目录创建 index.ts
ts
// packages/trade/index.ts
// 导出常量
export * from './data/constant';
// 导出工具函数
export { canFillSearch } from './utils/verify';
export { getOpenAccountUrl } from './utils/config';
// 导出类型
export type { IOrderForm } from './types';
// 导出组件(可选)
export { default as OrderPanel } from './order/index.vue';
步骤2:更新引用
ts
// packages/account/index.vue (account 包)
// 其他包通过 trade 包的公共 API 访问
import { getOpenAccountUrl } from '@repo/trade';
5.3 解耦 stores 依赖
步骤1:定义接口
ts
// packages/shared/interfaces/stores.ts
export interface IUserStore {
logined: boolean;
customerInfo: any;
goLogin: () => void;
}
步骤2:修改函数签名
ts
// packages/shared/utils/order.ts
import type { IUserStore } from '@repo/shared/interfaces/stores';
export const preVerify = async (userStore: IUserStore) => {
if (!userStore.logined) {
userStore.goLogin();
return false;
}
// ...
步骤3:在应用层注入
ts
// apps/web/src/views/trade/index.vue
import { preVerify } from '@repo/shared/utils/order';
import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
await preVerify(userStore);
六、检查清单
在添加新的包间通信时,请检查:
- 是否遵循依赖层次原则?
- 是否避免了循环依赖?
- 是否使用了合适的通信模式?
- 共享类型是否在 shared 包中?
- 是否建立了公共 API?
- 是否避免了直接访问内部实现?
- 是否避免了直接依赖 stores?
- 事件监听器是否及时清理?
七、总结
正确的包间通信应该是:
-
遵循依赖层次:基础包不依赖业务包
-
使用合适的通信模式:根据场景选择事件、服务、类型等
-
建立公共 API:通过 index.ts 导出,隐藏内部实现
-
共享类型和常量:在 shared 包中定义
-
解耦 stores:通过依赖注入或接口
-
及时清理资源:事件监听器等
遵循这些原则,可以确保包间的松耦合、高内聚,提高代码的可维护性和可测试性。