【问题标题】:Angular circular dependency between 2 components2个组件之间的角度循环依赖
【发布时间】:2019-07-30 11:13:38
【问题描述】:

我正在开发一个 Angular 7 应用程序,它允许管理实体,例如汽车和学生。

应用程序组件可以用以下树来描述:

  • 汽车
  • 汽车/CreateCar(对话框)
  • 学生
  • 学生/创建学生(对话框)

CreateCar 对话框中创建汽车时,用户应该能够使用 CreateStudent 对话框创建并指定新学生作为汽车的所有者。

同样,在 CreateStudent 对话框中创建 Student 时,用户应该能够使用 CreateCar 对话框创建新车并将其分配为 Student 的属性.

编译时,Angular 显示:“检测到循环依赖中的警告”,我知道这应该发生。

我已尝试搜索模式来解决此问题,例如共享服务,但似乎没有人工作。

编辑:

两个对话框的构造函数的相关部分:

constructor(
  private readonly matDialog: MatDialog
) {
}

在 CreateStudent 对话框中,打开 CreateCar 对话框的方法:

createCar(): void {
  this.matDialog
    .open(CreateCarDialogComponent)
    .afterClosed().subscribe((car: Car) => {
      // Do something with car
    });
}

在 CreateCar 对话框中,打开 CreateStudent 对话框的方法:

createStudent(): void {
  this.matDialog
    .open(CreateStudentDialogComponent)
    .afterClosed().subscribe((student: Student) => {
       // Do something with student
     });
}

对解决这个问题有什么建议吗?

谢谢

编辑 2:

在这里演示 https://stackblitz.com/edit/angular-bbfs8k

(Stackblitz 似乎没有显示编译警告)

【问题讨论】:

  • CarsStudents 是独立的模块吗?
  • 你在使用桶文件吗?还有更多的错误信息吗?
  • 展示如何打开对话框很重要。我需要知道你是否在使用组件工厂,因为它告诉我如何解决这个问题。
  • @PankajPrakash 没有。它们在同一个模块中
  • @JoeyGough 没有。不再有这些警告。

标签: angular dialog circular-dependency


【解决方案1】:

建议:

  • 您可以添加一个标志来指示打开的对话框是父级还是子级。如果是孩子,则打开的对话框不会创建汽车/子对话框。它打断了圆圈。
  • 在创建汽车/学生对话框之前,您应该有一个可以打开这两个对话框的大师(分配管理器)。在对话框中没有打开另一个对话框的选项。

【讨论】:

  • 是的,中断循环以禁用无限对话的可能性是一个运行时问题,可以使用您所说的标志来解决。问题出在编译时,因为 CreateCar 和 CreateStudent 组件相互引用
  • 拥有分配管理器并不能解决循环依赖问题。 AssignmentManager 需要同时引用 CreateCar 和 CreateStudent 对话框才能打开它们。同样,CreateCar 和 CreateStudent 需要注入 AssignmentManager 才能使用它打开新对话框。
【解决方案2】:

create-car-dialog.component.ts 导入 create-student-dialog.component.ts,后者导入 create-car-dialog.component.ts

create-student-dialog.component.ts 导入 create-car-dialog.component.ts,后者导入 create-student-dialog.component.ts

这就是错误消息的含义,并且存在您的循环依赖关系。您将需要以某种方式纠正此问题。

即您需要停止您创建的这个导入圈。也许您需要第三个组件来导入这两个组件。可能有很多方法可以做到这一点。

【讨论】:

  • 如果第三个组件同时导入它们,那么第三个组件如何在没有循环依赖的情况下在 CreateCarComponent 或 CreateStudentComponent 中使用?
  • 有很多方法可以达到相同的目的。如果要将父组件注入子组件,请在构造函数中使用 @Host 装饰器。但不要那样做......在父组件中定义 createCar() 和 createPerson() 并将这些方法作为属性传递给子组件。
【解决方案3】:

MatDialog 不需要直接引用组件声明。您只需要传递一个ComponentType<any> 参数即可打开一个对话框。所以我们可以通过使用 Angular 依赖注入器来解决循环依赖(由 TypeScript 触发)。

创建一个名为create-card-token.ts 的文件并定义一个注入令牌。

export const CREATE_CAR_TOKEN: InjectionToken<ComponentType<any>> =
new InjectionToken<ComponentType<any>>('CREATE_CAR_TOKEN');

在您的模块中,将上述令牌的值定义为提供者。您可以在此处定义将用于MatDialog 的组件。

@NgModule({
    ....
    providers: [
        {provide: CREATE_CAR_TOKEN, useValue: CreateCarComponent}
    ]
}) export class MyModule {}

您现在可以在CarComponent 中注入此令牌并使用它来打开对话框。

@Component({...})
export class CarComponent {
     public constructor(@Inject(CREATE_CAR_TOKEN) private component: ComponentType<any>,
                        private matDialog: MatDialog) {}

     public createCar() {
         this.matDialog
            .open(this.component)
            .afterClosed().subscribe((car: Car) => {
                // Do something with car
            });
     }       
}

这将解决循环依赖,因为CarComponent 永远不需要知道CreateCarComponent 的类型说明。相反,它只知道ComponentType&lt;any&gt; 已被注入,而MyModule 定义了将使用的组件。

还有另一个问题。上面的示例使用any 作为将要创建的组件类型。如果您需要访问对话框实例,并直接从CarComponent 调用方法,那么您可以声明一个 interface 类型。关键是将 interface 保存在单独的文件中。如果你从CreateCarComponent 文件中导出接口,你会回到循环依赖。

例如;

  export interface CreateCarInterface {
       doStuff();
  }

然后您更新令牌以使用该界面。

export const CREATE_CAR_TOKEN: InjectionToken<ComponentType<CreateCarInterface>> =
new InjectionToken<ComponentType<CreateCarInterface>>('CREATE_CAR_TOKEN');

然后您可以像这样从汽车组件调用doStuff()

@Component({...})
export class CarComponent {
     public constructor(@Inject(CREATE_CAR_TOKEN) private component: ComponentType<CreateCarInterface>,
                        private matDialog: MatDialog) {}

     public createCar() {
         const ref = this.matDialog.open(this.component);
         ref.componentInstance.doStuff();
     }       
}

然后您可以在CreateCarComponent 中实现该接口。

@Component({..})
export class CreateCarComponent implements CreateCarInterface {
      public doStuff() {
         console.log("stuff");
      }
}

这种循环引用经常发生在MatDialog 和 CDK 门户中,因为我们经常需要有一个服务打开对话框,然后对话框需要为其他人使用相同的服务原因。这种情况我遇到过很多次了。

【讨论】:

  • 感谢您的详细解答。它确实看起来是一个有趣的解决方案,我可能会在其他地方使用它。但是,它并不能解决我的问题,因为我的情况不是您描述的那种:“我们经常需要让一个服务打开对话框,然后对话框由于其他原因需要使用相同的服务”
  • 我的问题是我有 2 个对话框组件需要相互打开。 CreateCarComponent 需要打开 CreateStudentComponent,反之亦然。使用您建议的解决方案,在我的 CreateCarComponent 中,我需要使用 CreateStudentComponent 的值提供 CREATE_STUDENT_TOKEN。同样,在我的 CreateStudentComponent 中,我需要使用 CreateCarComponent 的值提供 CREATE_CAR_TOKEN。因此,循环依赖仍然存在
  • @TomásLaw CREATE_STUDENT_TOKEN 永远不会提供CreateStudentComponent。这些令牌仅提供ComponentType 和一个可选接口。
【解决方案4】:

使用前向类引用 (forwardRef) 打破循环

在 TypeScript 中,类声明的顺序很重要。在定义类之前,您不能直接引用它。

这通常不是问题,特别是如果您遵守推荐的每个文件一个类的规则。但有时循环引用是不可避免的。当类“A”指代“B”类而“B”指代“A”时,你就陷入了困境。必须先定义其中一个。

Angular 的 forwardRef() 函数创建一个间接引用,Angular 可以稍后解析。

Parent Finder 示例充满了无法破坏的循环类引用。

当一个类引用它自己时,你就会面临这个困境,就像 AlexComponent 在它的提供者数组中所做的那样。 providers 数组是 @Component() 装饰器函数的一个属性,它必须出现在类定义的上方。

用 forwardRef 打破循环。

providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],

https://angular.io/guide/dependency-injection-in-action#break-circularities-with-a-forward-class-reference-forwardref

【讨论】:

    【解决方案5】:

    我不使用该材料,但我找到了使用 ng-bootstrap 的解决方案,您可以采用相同的想法并将其应用于该材料。

    基本上我在模态中定义了一个名为 backComponent 的变量,当我请求打开模态时我定义了哪个是 backComponent,在 ng-boostrap 中它会是这样的:

    open() {
       const createStudent = this.modalService.open(CreateStudentDialogComponent);
       createStudent.componentInstance.backComponent = CreateCarDialogComponent;
    }
    

    当我需要返回上一个组件时,例如使用按钮返回模态,我使用这个 backComponent 来返回:

    【讨论】:

      猜你喜欢
      • 2017-03-24
      • 1970-01-01
      • 2020-03-04
      • 2011-01-06
      • 2022-06-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多