Skip to content

服务间通信方案

本文梳理在 use-vue-service 中,不同层级关系的服务之间如何进行通信。 核心原则:优先通过容器继承关系和 DI 注入解决通信问题,尽量避免使用 EventEmitter、mitt 等事件总线方案。


一、同一容器中的服务通信

A、B 两个服务注册在同一个容器中,可以直接互相注入,调用对方的方法或访问对方的响应式属性,没有任何限制。

ts
@Injectable()
class ServiceA {
  @Inject(ServiceB)
  private b!: ServiceB;

  doSomething() {
    this.b.someMethod();
  }
}

@Injectable()
class ServiceB {
  @Inject(ServiceA)
  private a!: ServiceA;

  doSomething() {
    this.a.someMethod();
  }
}

注意循环注入在技术上可行,但需要确保不会在构造函数中触发循环调用。


二、父子容器中的服务通信

2.1 子服务注入父服务(向上查找)

子容器中的服务可以直接注入父容器中的服务,这是 DI 容器继承链的自然能力。

ts
// 父容器中注册 ServiceA
declareProviders([ServiceA]);

// 子容器中注册 ServiceB,ServiceB 可以直接注入 ServiceA
@Injectable()
class ServiceB {
  @Inject(ServiceA)
  private a!: ServiceA;
}

2.2 父服务调用子服务(向下查找)

父容器中的服务无法直接注入子容器中的服务(子容器的生命周期短于父容器,长期持有子服务的引用是危险的)。

推荐使用 FIND_CHILD_SERVICE 进行瞬态查找:在需要调用子服务的函数内部实时查找,函数执行完毕后引用自动释放。

ts
@Injectable()
class ServiceA {
  @Inject(FIND_CHILD_SERVICE)
  private findChild!: FindChildService;

  handleClick() {
    // 在函数内部实时查找,不在函数外部长期持有引用
    const b = this.findChild(ServiceB);
    if (b) {
      b.someMethod();
    }
  }
}

关键约束:只能在函数内部使用,不能将查找结果保存到实例属性上长期持有,因为子服务所在组件随时可能被销毁。


三、平级容器中的服务通信

A、B 两个服务分别注册在两个不同的子容器中,无法直接互相注入。有以下两种解决方式:

3.1 共享状态提升

将需要共享的状态提升到公共父容器的某个服务中,A、B 各自注入该服务来读写共享状态。

ts
// 公共父容器中注册 SharedService
@Injectable()
class SharedService {
  sharedData = ref('');
}

// A 和 B 分别注入 SharedService 来共享状态
@Injectable()
class ServiceA {
  @Inject(SharedService)
  private shared!: SharedService;
}

@Injectable()
class ServiceB {
  @Inject(SharedService)
  private shared!: SharedService;
}

3.2 通过父容器中转方法调用

如果需要跨平级容器调用对方的方法,可以通过公共父容器中的中转服务来实现:A 调用父服务的方法,父服务通过 FIND_CHILD_SERVICE 找到 B,再调用 B 的方法。

ts
@Injectable()
class ParentService {
  @Inject(FIND_CHILD_SERVICE)
  private findChild!: FindChildService;

  triggerB() {
    const b = this.findChild(ServiceB);
    if (b) {
      b.someMethod();
    }
  }
}

@Injectable()
class ServiceA {
  @Inject(ParentService)
  private parent!: ParentService;

  handleClick() {
    this.parent.triggerB();
  }
}

四、为什么避免事件总线

EventEmitter、mitt 等事件总线方案虽然灵活,但存在以下问题:

  • 依赖关系隐式:通过字符串事件名通信,IDE 无法追踪调用链,重构困难
  • 类型安全弱:事件载荷的类型难以约束,容易出现运行时错误
  • 生命周期管理复杂:需要手动在组件/服务销毁时取消订阅,容易遗漏导致内存泄漏
  • 与 DI 体系割裂:事件总线是全局状态,与容器的作用域隔离机制冲突

通过 DI 注入和容器继承关系进行通信,依赖关系显式可追踪,类型安全,生命周期由容器统一管理,是更符合本库设计理念的通信方式。