Monorepo 各包间正确的通信方式

1,120 阅读10分钟

概述

Monorepo(Monolithic Repository)指的是把多个包(项目)的代码,统一放在同一个代码仓库里进行管理的一种仓库组织方式。适合多个项目强相关且有大量共享代码的中大型项目。使用 Monorepo 管理项目的好处非常明显,它不用发 npm 包,不用来回同步版本,改完公共库,所有项目立即可用,使得共享代码更简单。 Monorepo 是存放多个包的仓库,包之间的通信方式至关重要,接下来我们以一个交易项目为例,一步一步了解 Monorepo 各包间正确的通信方式。

交易平台项目简述

该项目是一个给用户提供股票交易服务的平台。项目包括交易(Trade)、资产(Portfolios)、行情(Quotes)、市场(Market)等模块。项目还有提供公共方法、UI、数据等公共模块。

交易平台各包之间的通信问题

包之间的依赖关系不合理

包之间出现循环依赖问题,业务包依赖业务包。

├── @repo/shared ←→ @repo/trade (循环依赖)
├── @repo/account → @repo/trade (不合理)
├── @repo/quote → @repo/trade (不合理)
├── @repo/portfolios → @repo/trade (不合理)
└── ...

封装性破坏:直接访问包内部实现

直接导入其他包的内部路径

  // 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

// 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 依赖层次结构

┌─────────────────────────────────────┐
│         apps/web (应用层)            │
│  依赖所有业务包,负责组装和路由      │
└─────────────────────────────────────┘
              │
              ├─────────────────────────────────────┐
              │                                     │
┌─────────────▼──────────────┐  ┌──────────────────▼──────────────┐
│   业务包 (Business)        │  │   基础包 (Foundation)            │
│  - trade                   │  │  - shared (不依赖其他业务包)     │
│  - account                 │  │  - data-center                  │
│  - quote                   │  │  - ui                            │
│  - portfolios              │  │                                  │
│  - company                 │  │                                  │
│  - market                  │  │                                  │
│  - ...                     │  │                                  │
└────────────────────────────┘  └──────────────────────────────────┘

1.2 依赖规则

✅ 允许的依赖方向
  1. 应用层 → 所有包
  2. 业务包 → 基础包(shared, data-center, ui)
  3. 基础包 → 只依赖更底层的基础包或外部库
  4. shared 包 → 不依赖任何业务包
❌ 禁止的依赖方向
  1. 基础包 → 业务包(如 shared → trade)
  2. 业务包 → 业务包(如 account → trade)
  3. 循环依赖(如 shared ↔ trade)

1.3 依赖层次示例

// ✅ 正确:业务包依赖基础包
// 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)

适用场景

  1. 包间的松耦合通信
  2. 一对多的通知场景
  3. 异步事件通知

实现方式

  1. 定义事件类型(在 shared 包中)
// 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;
  1. 发送事件(在 trade 包中)
// packages/trade/order/index.vue
import eventBus, { EEventBusType } from '@repo/shared/events';

// 下单成功后发送事件
const handleOrderSuccess = () => {
  eventBus.emit(EEventBusType.ORDER_SUCCESS, {
    orderId: '123',
    orderType: 'limit'
  });
};
  1. 监听事件(在 portfolios 包中)
// 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);
});

优点

  1. 解耦:发送者和接收者不需要直接依赖
  2. 灵活:可以动态添加/移除监听器
  3. 支持一对多通信

注意事项

  1. 事件类型定义应在 shared 包中,确保类型安全
  2. 及时清理事件监听器,避免内存泄漏
  3. 事件名称应清晰明确,避免命名冲突

2.2 服务模式(Singleton Service)

适用场景

  1. 提供统一的数据访问接口
  2. 跨包共享服务实例
  3. 需要单例模式的服务

实现方式

  1. 定义服务(在 data-center 包中)
// 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';
  1. 使用服务(在任何包中)
// 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);
};

优点

  1. 统一接口:所有包通过相同的接口访问服务
  2. 单例保证:确保全局只有一个实例
  3. 易于测试:可以 mock 服务接口

注意事项

  1. 服务接口应稳定,避免频繁变更

  2. 服务应在 shared 或 data-center 等基础包中

  3. 避免在服务中直接依赖业务包

2.3 共享类型和接口

适用场景

  1. 包间传递数据
  2. 定义公共接口
  3. 类型约束

实现方式

  1. 定义共享类型(在 shared 包中)
// packages/shared/types/order.ts
export interface IOrderForm {
  stockCode: string;
  stockName: string;
  entrustType: EEntrustType;
  // ...
}

export interface IMaxAvailableAsset {
  cashAvailable: number;
  marginAvailable: number;
  // ...
}
  1. 使用共享类型(在 trade 包中)
// packages/trade/order/index.vue
import type { IOrderForm } from '@repo/shared/types/order';

const form: IOrderForm = {
  stockCode: 'AAPL',
  stockName: 'Apple Inc.',
  // ...
};
  1. 使用共享类型(在 portfolios 包中)
// packages/portfolios/components/orders/index.vue
import type { IOrderForm } from '@repo/shared/types/order';

const displayOrder = (order: IOrderForm) => {
  // 使用共享类型
};

优点

  1. 类型安全:TypeScript 提供编译时类型检查
  2. 一致性:确保数据结构一致
  3. 可维护性:类型定义集中管理

注意事项

  1. 共享类型应在 shared 包中定义
  2. 避免在业务包中定义共享类型
  3. 类型定义应向后兼容

2.4 公共 API 导出模式

适用场景

  1. 包需要对外暴露功能
  2. 隐藏内部实现细节
  3. 提供稳定的接口

实现方式

  1. 建立公共 API(在 trade 包中)
// 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';
  1. 使用公共API(在其他包中)
// ✅ 正确:通过公共API导入
import { getOpenAccountUrl } from '@repo/trade';

// ❌ 错误:直接访问内部实现
import { getOpenAccountUrl } from '@repo/trade/utils';

优点

  1. 封装性:隐藏内部实现
  2. 稳定性:公共API相对稳定
  3. 可维护性:内部重构不影响外部使用

注意事项

  1. 每个包都应建立 index.ts 作为公共 API 入口

  2. 只导出需要对外暴露的功能

  3. 避免导出内部实现细节

2.5 依赖注入模式

适用场景

  1. 需要解耦 stores 依赖
  2. 提高可测试性
  3. 支持不同的实现

实现方式

  1. 定义接口(在 shared 包中)
// packages/shared/interfaces/stores.ts
export interface IUserStore {
  logined: boolean;
  customerInfo: any;
  goLogin: () => void;
}

export interface IConfigStore {
  urlsConfig: {
    acOpen: string;
  };
}
  1. 通过参数注入(在 shared 包中)
// 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;
  }
  // ...
};
  1. 在应用层注入依赖(在 apps/web 中)
// 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);
};

优点

  1. 解耦:包不直接依赖 stores
  2. 可测试:可以轻松 mock 依赖
  3. 灵活:支持不同的实现

注意事项

  1. 接口定义应在 shared 包中

  2. 依赖注入会增加调用复杂度

  3. 适合复杂场景,简单场景可直接使用

2.6 API Linker 模式

适用场景

  1. 通过 URL 参数调用 API
  2. 跨页面通信
  3. 外部系统集成

实现方式

  1. 注册 API(在 trade 包中)
// 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) => {
    // 提交订单
  }
});
  1. 创建 API 链接(在其他包中)
// 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);
};

优点

  1. 跨页面通信
  2. 支持外部系统集成
  3. 解耦页面间依赖

三、最佳实践

3.1 包内导入规则

✅ 正确:包内使用相对路径
// packages/trade/utils/verify.ts
import { EOrderVerify } from '../data/constant';
import { canFillSearch } from './common';
❌ 错误:包内使用包名路径
import { EOrderVerify } from '@repo/trade/data/constant';

// 讨论:是否可以使用 @pkg/trade
import { EOrderVerify } from ' @pkg/trade/data/constant';
✅ 正确:跨包使用包名路径
// packages/account/index.vue
import { getOpenAccountUrl } from '@repo/trade';
import { eventBus } from '@repo/shared/events';

3.2 类型定义规则

✅ 正确:共享类型在 shared 包中
// packages/shared/types/order.ts
export interface IOrderForm {
  // ...
}
✅ 正确:业务特定类型在业务包中
// packages/trade/types/condition.ts
export interface IConditionOrder {
  // ...
}

3.3 常量定义规则

✅ 正确:通用常量在 shared 包中
// packages/shared/constant/order.ts
export enum EEntrustType {
  LIMIT = 1,
  MARKET = 2,
}
✅ 正确:业务特定常量在业务包中
// packages/trade/data/constant.ts
export enum EOrderVerify {
  NOT_LOGIN = 'not_login',
  // ...
}

3.4 工具函数规则

✅ 正确:通用工具函数在 shared 包中
// packages/shared/utils/order.ts
export const isTrue = (flag: any) => {
  // 通用逻辑
};
✅ 正确:业务特定工具函数在业务包中
// packages/trade/utils/verify.ts
export const verifyOrder = (order: IOrderForm) => {
  // 业务特定逻辑
};

四、通信模式选择指南

场景推荐模式示例
通知其他包某个事件发生事件通信订单成功、登录成功
获取共享数据服务模式获取股票信息、行情数据
传递数据共享类型订单表单、用户信息
调用其他包功能公共 API工具函数、组件
需要解耦 stores依赖注入验证函数、工具函数
跨页面通信API Linker打开交易面板

五、重构建议

5.1 解决循环依赖

步骤1:识别共享内容

// 找出 shared 包中使用的 trade 包内容
// packages/shared/utils/order.ts
import { EOrderVerify } from '@repo/trade/data';  // 需要移到shared
import { canFillSearch } from '@repo/trade/utils'; // 需要移到shared

步骤2:迁移到 shared 包

// packages/shared/constant/order.ts
export enum EOrderVerify {
  NOT_LOGIN = 'not_login',
  // ...
}

// packages/shared/utils/order.ts
export const canFillSearch = async (params: any) => {
  // 实现逻辑
};

步骤3:更新引用

// packages/trade/utils/verify.ts
// 从 shared 包导入
import { EOrderVerify } from '@repo/shared/constant/order';
import { canFillSearch } from '@repo/shared/utils/order';

步骤4:移除依赖

// packages/shared/package.json
{
  "dependencies": {
    // 移除 "@repo/trade": "workspace:*"
  }

5.2 建立公共 API

步骤1:在各包的根目录创建 index.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:更新引用

// packages/account/index.vue (account 包)
// 其他包通过 trade 包的公共 API 访问
import { getOpenAccountUrl } from '@repo/trade';

5.3 解耦 stores 依赖

步骤1:定义接口

// packages/shared/interfaces/stores.ts
export interface IUserStore {
  logined: boolean;
  customerInfo: any;
  goLogin: () => void;
}

步骤2:修改函数签名

// 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:在应用层注入

// 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?
  • 事件监听器是否及时清理?

七、总结

正确的包间通信应该是:

  1. 遵循依赖层次:基础包不依赖业务包

  2. 使用合适的通信模式:根据场景选择事件、服务、类型等

  3. 建立公共 API:通过 index.ts 导出,隐藏内部实现

  4. 共享类型和常量:在 shared 包中定义

  5. 解耦 stores:通过依赖注入或接口

  6. 及时清理资源:事件监听器等

遵循这些原则,可以确保包间的松耦合、高内聚,提高代码的可维护性和可测试性。